Google Apps Platform

Writing your First Marketplace App using Java

Note: There's a new Google Apps Marketplace experience! Beginning November 19, 2013, new listings may only be created using the new version: existing developers may need to create a new Chrome Web Store account to publish new listings. Refer to the new documentation for more information.

Contents

  1. Audience
  2. Build the App
  3. Integrate the App
    1. Single Sign On with OpenID
    2. OAuth-enabled access to Google Calendar
  4. Create the Listing
    1. Creating the Manifest
    2. Listing the App on the Marketplace
    3. Retrieving the OAuth Key and Secret
  5. Launching the App in Production
  6. Appendix - Advanced Options
    1. Redirecting to your site during Installation

This tutorial will walk you through launching your first application on the Google Apps Marketplace. The tutorial assumes you already can create a basic "Hello World" web application and builds on top of such an application to demonstrate several key pieces of functionality -- a link in the Google universal navigation, Single Sign On using OpenID and OAuth-enabled access to Google Apps data. Instead of saying "Hello World", this application will greet the Google Apps user by name and shows them their next Google Calendar appointment.

To see how Marketplace applications are installed by domain administrators and consumed by end-users, take a look at the application lifecycle.

Audience

This tutorial assumes that you are a developer with a high level understanding of web technologies such as XML and HTTP. The code for this application is in Java, but the concepts demonstrated can easily be adapted to other languages.

Build the App

You may already have a cloud application you'd like to launch on the Google Apps Marketplace, or you may be building one from scratch. This tutorial starts with a basic "Hello World" application and demonstrates how it can be integrated with Google Apps.

Here's the basic application which will be integrated with Google Apps:

    <%@ page contentType="text/html;charset=UTF-8" language="java"%>
    <head><title>Hello</title></head>
    <body>
      <h1>Hello World</h1>
    </body>
    <html>

Integrate the App

Although any language can be used to build Marketplace applications, using a language with available OpenID libraries supporting the Google Apps discovery extensions and available Google Data API client libraries makes developing integrations easier.

This application uses the following libraries to integrate with Google Apps:

Download:

  • Self-contained ZIP Download
  • To make it easier for you to get started, we've included the sample app and the above-mentioned libraries as a self-contained download. A readme file is included with instructions for building the web application. Building the example requires Ant 1.6 or later, Java 6, and a servlet container such as Tomcat. It can also be deployed to Google App Engine.

Single Sign On with OpenID

The OpenID protocol enables users to login to applications around the web without the need to create a new set of credentials on each site. Using the Single Sign On functionality availability to Marketplace applications, Google Apps users can seamlessly login to third-party web applications as if the applications are natively part of Google Apps.

Many Google Apps users login to Google Apps first thing in the morning to check their e-mail. With the universal navigation bar links created by Marketplace applications, users get fingertip access to all the applications they regularly use-- right from within their e-mail and the other Google Apps.

Discovery

OpenID discovery is the process of finding the correct OpenID endpoint to use for users on a given domain. To start the process, applications supporting OpenID typically ask the user for their identifier or allow them to pick from a set of popular providers. For users navigating from Google Apps, the information needed to start the discovery process can be supplied in the link to the application, making the process transparent to users. For example, if we define the link to our app in the manifest as http://www.googlecodesamples.com/helloworld?from=google&domain=${DOMAIN_NAME} and log in as john@example.com, then the URL would appear in the universal navigation bar as http://www.googlecodesamples.com/helloworld?from=google&domain=example.com. This allows the application to immediately make an OpenID authentication request without prompting the user.

Typically OpenID requires that each domain host metadata on a web server. Because Google Apps doesn't require every customer to host a website, a slightly modified version of the discovery prototocol is used for Google Apps domains.

Authentication and Whitelisting

After the library finds the correct Google Apps OpenID endpoint for the given domain, the "Hello World" application redirects the user's web browser to the endpoint. Since the user accessed the application from the Google universal navigation, they're already logged in to their Google Apps account. Google does a comparison of the openid.realm value specified in the request to those registered by applications installed on their domain. If the realm matches exactly, the user is immediately redirected back to the "Hello World" application seamlessly without any interstitial "access granting" page typically associated with OpenID.

Code

The code for the OpenID portion of "Hello World" is presented below. Additional supporting classes not shown here are available in the full download.

      /**
       * Servlet for handling OpenID logins.  Uses the Step2 library from code.google.com and the
       * underlying OpenID4Java library.
       */
      public class OpenIdServlet extends HttpServlet {

          protected ConsumerHelper consumerHelper;
          protected String realm;
          protected String returnToPath;
          protected String homePath;

          /**
           * Init the servlet.  For demo purposes, we're just using an in-memory version
           * of OpenID4Java's ConsumerAssociationStore.  Production apps, particularly those
           * in a clustered environment, should consider using an implementation backed by
           * shared storage (memcache, DB, etc.)
           *
           * @param config
           * @throws ServletException
           */
          @Override
          public void init(ServletConfig config) throws ServletException {
              super.init(config);
              returnToPath = getInitParameter("return_to_path", "/openid");
              homePath = getInitParameter("home_path", "/");
              realm = getInitParameter("realm", null);
              ConsumerFactory factory = new ConsumerFactory(new InMemoryConsumerAssociationStore());
              consumerHelper = factory.getConsumerHelper();
          }

          /**
           * Either initiates a login to a given provider or processes a response from an IDP.
           * @param req
           * @param resp
           * @throws ServletException
           * @throws IOException
           */
          @Override
          protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                  throws ServletException, IOException {
              String domain = req.getParameter("hd");
              if (domain != null) {
                  // User attempting to login with provided domain, build and OpenID request and redirect
                  try {
                      AuthRequest authRequest = startAuthentication(domain, req);
                      String url = authRequest.getDestinationUrl(true);
                      resp.sendRedirect(url);
                  } catch (OpenIDException e) {
                      throw new ServletException("Error initializing OpenID request", e);
                  }
              } else {
                  // This is a response from the provider, go ahead and validate
                  doPost(req, resp);
              }
          }

          /**
           * Handle the response from the OpenID Provider.
           *
           * @param req Current servlet request
           * @param resp Current servlet response
           * @throws ServletException if unable to process request
           * @throws IOException if unable to process request
           */
          @Override
          protected void doPost(HttpServletRequest req, HttpServletResponse resp)
                  throws ServletException, IOException {
              try {
                  UserInfo user = completeAuthentication(req);
                  req.getSession().setAttribute("user", user);
                  resp.sendRedirect(homePath);
              } catch (OpenIDException e) {
                  throw new ServletException("Error processing OpenID response", e);
              }
          }

          /**
           * Builds an auth request for a given OpenID provider.
           *
           * @param op OpenID Provider URL.  In the context of Google Apps, this can be a naked domain
           *           name such as "saasycompany.com".  The length of the domain can exceed 100 chars.
           * @param request Current servlet request
           * @return Auth request
           * @throws org.openid4java.OpenIDException if unable to discover the OpenID endpoint
           */
          AuthRequest startAuthentication(String op, HttpServletRequest request)
                  throws OpenIDException {
              IdpIdentifier openId = new IdpIdentifier(op);

              String realm = realm(request);
              String returnToUrl = returnTo(request);

              AuthRequestHelper helper = consumerHelper.getAuthRequestHelper(openId, returnToUrl);
              addAttributes(helper);

              HttpSession session = request.getSession();
              AuthRequest authReq = helper.generateRequest();
              authReq.setRealm(realm);

              UiMessageRequest uiExtension = new UiMessageRequest();
              uiExtension.setIconRequest(true);
              authReq.addExtension(uiExtension);

              session.setAttribute("discovered", helper.getDiscoveryInformation());
              return authReq;
          }

          /**
           * Validates the response to an auth request, returning an authenticated user object if
           * successful.
           *
           * @param request Current servlet request
           * @return User
           * @throws org.openid4java.OpenIDException if unable to verify response
           */

          UserInfo completeAuthentication(HttpServletRequest request)
                  throws OpenIDException {
              HttpSession session = request.getSession();
              ParameterList openidResp = Step2.getParameterList(request);
              String receivingUrl = currentUrl(request);
              DiscoveryInformation discovered =
                      (DiscoveryInformation) session.getAttribute("discovered");


              AuthResponseHelper authResponse =
                      consumerHelper.verify(receivingUrl, openidResp, discovered);
              if (authResponse.getAuthResultType() == AuthResponseHelper.ResultType.AUTH_SUCCESS) {
                  return onSuccess(authResponse, request);
              }
              return onFail(authResponse, request);
          }

          /**
           * Adds the requested AX attributes to the request
           *
           * @param helper Request builder
           */
          void addAttributes(AuthRequestHelper helper) {
              helper.requestAxAttribute(Step2.AxSchema.EMAIL, true)
                  .requestAxAttribute(Step2.AxSchema.FIRST_NAME, true)
                  .requestAxAttribute(Step2.AxSchema.LAST_NAME, true);
          }

          /**
           * Reconstructs the current URL of the request, as sent by the user
           *
           * @param request Current servlet request
           * @return URL as sent by user
           */
          String currentUrl(HttpServletRequest request) {
              return Step2.getUrlWithQueryString(request);
          }

          /**
           * Gets the realm to advertise to the IDP.  If not specified in the servlet configuration.
           * it dynamically constructs the realm based on the current request.
           *
           * @param request Current servlet request
           * @return Realm
           */
          String realm(HttpServletRequest request) {
              if (StringUtils.isNotBlank(realm)) {
                  return realm;
              } else {
                  return baseUrl(request);
              }
          }

          /**
           * Gets the <code>openid.return_to</code> URL to advertise to the IDP.  Dynamically constructs
           * the URL based on the current request.
           * @param request Current servlet request
           * @return Return to URL
           */
          String returnTo(HttpServletRequest request) {
              return new StringBuffer(baseUrl(request))
                      .append(request.getContextPath())
                      .append(returnToPath).toString();
          }

          /**
           * Dynamically constructs the base URL for the applicaton based on the current request
           *
           * @param request Current servlet request
           * @return Base URL (path to servlet context)
           */
          String baseUrl(HttpServletRequest request) {
              StringBuffer url = new StringBuffer(request.getScheme())
                      .append("://").append(request.getServerName());

              if ((request.getScheme().equalsIgnoreCase("http")
                      && request.getServerPort() != 80)
                      || (request.getScheme().equalsIgnoreCase("https")
                      && request.getServerPort() != 443)) {
                  url.append(":").append(request.getServerPort());
              }

              return url.toString();
          }

          /**
           * Map the OpenID response into a user for our app.
           *
           * @param helper Auth response
           * @param request Current servlet request
           * @return User representation
           */
          UserInfo onSuccess(AuthResponseHelper helper, HttpServletRequest request) {
              return new UserInfo(helper.getClaimedId().toString(),
                      helper.getAxFetchAttributeValue(Step2.AxSchema.EMAIL),
                      helper.getAxFetchAttributeValue(Step2.AxSchema.FIRST_NAME),
                      helper.getAxFetchAttributeValue(Step2.AxSchema.LAST_NAME));
          }

          /**
           * Handles the case where authentication failed or was canceled.  Just a no-op
           * here.
           *
           * @param helper Auth response
           * @param request Current servlet request
           * @return User representation
           */
          UserInfo onFail(AuthResponseHelper helper, HttpServletRequest request) {
              return null;
          }

          /**
           * Small helper for fetching init params with default values
           *
           * @param key Parameter to fetch
           * @param defaultValue Default value to use if not set in web.xml
           * @return
           */
          protected String getInitParameter(String key, String defaultValue) {
              String value = getInitParameter(key);
              return StringUtils.isBlank(value) ? defaultValue : value;
          }
      }
  

OAuth-enabled access to Google Calendar

When a user visits the "Hello World" application, they are greeted by name and presented with their next upcoming calendar appointment from Google Calendar using the Calendar Data API and 2-legged OAuth.

Each listing in the Marketplace has an OAuth key and secret which can be used to access the authorized data for any user in any domain which has installed the app, based on the granting of privileges by the domain administrator during the install process. This key and secret can be obtained after listing the app on the Marketplace. The key and secret pair authorize the application, but in order to specify which user's data the application needs access to, a query parameter called xoauth_requestor_id is added to all API requests indicating which user's data is needed.

Because of the potential sensitivity of this information, applications need to ensure that they're presenting the data to the correct user and thus it uses OpenID to determine the user's identity. Since Google does not allow a user to directly modify their e-mail address of record, the e-mail address retrieved via OpenID attribute exchange may be used, but you should be sure to read the best practices and security considerations which cover some precautions you should take. The general Google Data APIs documentation contains more information on 2-legged OAuth including samples in other languages.

Code

    /**
     * Very simple example of using the Google Data APIs with 2-legged OAuth.  This fetches the
     * current user's next calendar event and displays it.
     */
    public class CalendarServlet extends HttpServlet {

        CalendarService calendarService;

        /**
         * Initializes the calendar service using OAuth client key & secret from web.xml.  Please note
         * that in production applications the consumer secret should be treated the same as sensitive
         * data that should be protected from casual browsing, as would any password or encryption key.
         *
         * @param config Servlet config
         * @throws ServletException if unable to initialize
         */
        @Override
        public void init(ServletConfig config) throws ServletException {
            super.init(config);

            String consumerKey = getInitParameter("consumer_key");
            String consumerSecret = getInitParameter("consumer_secret");

            GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
            oauthParameters.setOAuthConsumerKey(consumerKey);
            oauthParameters.setOAuthConsumerSecret(consumerSecret);

            calendarService = new CalendarService("marketplace-hello");
            try {
                calendarService.setOAuthCredentials(oauthParameters, new OAuthHmacSha1Signer());
            } catch (OAuthException e) {
                throw new ServletException("Unable to initialize calendar service", e);
            }
        }

        /**
         * Fetches the next calendar event and renders JSP if the user is logged in.
         *
         * @param req Current servlet request
         * @param resp Current servlet response
         * @throws ServletException if unable to process request
         * @throws IOException
         */
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp)
                throws ServletException, IOException {
            UserInfo user = (UserInfo) req.getSession().getAttribute("user");
            if (user == null ) {
                resp.sendRedirect("/");
            } else {
                try {
                    CalendarEventEntry entry = nextEvent(user);
                    req.setAttribute("nextEvent", entry);
                } catch (Exception e) {
                    throw new ServletException("Unable to fetch next calendar event", e);
                }
                req.getRequestDispatcher("/WEB-INF/jsp/hello.jsp").forward(req, resp);
            }
        }

        /**
         * Handles the actual query for the Google Calendar API.  This is a simple query
         * that checks the user's default calendar for the next event (limit 1, sort by start
         * time).  It also uses 2-legged OAuth (xoauth_requestor_id) to impersonate the user
         * rather than using an individual access token.
         *
         * @param user User to request data for
         * @return Next calendar event, null if none found
         * @throws IOException If error retrieving data
         * @throws ServiceException If error retrieving data
         */
        private CalendarEventEntry nextEvent(UserInfo user) throws IOException, ServiceException {
            URL feedUrl = new URL("https://www.google.com/calendar/feeds/default/private/full");
            CalendarQuery query = new CalendarQuery(feedUrl);
            query.setMinimumStartTime(DateTime.now());
            query.setMaxResults(1);
            query.addCustomParameter(new Query.CustomParameter("orderby", "starttime"));
            query.addCustomParameter(new Query.CustomParameter("sortorder","a"));
            query.addCustomParameter(new Query.CustomParameter("singleevents","true"));
            query.addCustomParameter(new Query.CustomParameter("xoauth_requestor_id", user.getEmail()));

            CalendarEventFeed results = calendarService.query(query, CalendarEventFeed.class);
            if (!results.getEntries().isEmpty()) {
                return results.getEntries().get(0);
            } else {
                return null;
            }
        }
    }
    
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <html>
      <head><title>Hello</title></head>
      <body>
        <h1>Hello <c:out value="${user.firstName}"/></h1>

        <h2>Current or Next Calendar Event:</h2>
        <c:choose>
          <c:when test="${nextEvent != null}">
            <div>
                Title: <c:out value="${nextEvent.title.plainText}"/><br/>
                When: <c:out value="${nextEvent.times[0].startTime}"/>" -
                      <c:out value="${nextEvent.times[0].endTime}"/><br/>
                Where: <c:out value="${nextEvent.locations[0].valueString}"/><br/>
                Description: <c:out value="${nextEvent.plainTextContent}"/><br/>
          </c:when>
          <c:otherwise>
              You have no upcoming calendar events.
          </c:otherwise>
        </c:choose>
      </body>
    </html>    
    

Defining the App Structure

Creating the Manifest

An installable application the Marketplace is defined by the Marketplace listing and an application manifest. The Marketplace listing generally defines the marketing and business components of an app while the manifest describes the technical details and data access requirements. The manifest is an XML documented uploaded by the developer during the process of creating a listing.

Manifest Structure

A manifest is composed of meta-data about the application (including a name, description and URLs), one or more extensions (such as a navigation link or OpenID) and zero or more data access requirements.

This "Hello World" application is defined using the following elements:

  • Name: "Hello World"
  • Description: "Demonstrates a simple Google Apps Marketplace application."
  • Support URL: End-users can learn how to get support for this application at http://www.googlecodesamples.com/support.jsp
  • Universal navigation link: The application will be accessed from the Google universal navigation using the URL http://www.googlecodesamples.com/openid?hd=${DOMAIN_NAME}. The ${DOMAIN_NAME} variable is automatically replaced by the customer's domain name and this information is used to enable OpenID-based Single Sign On.
  • OpenID realm: The supplied OpenID realm will be automatically whitelisted during the install process so that the identity of the application user is automatically passed to the application without the typical OpenID authorization page being presented, as the domain administrator has approved the installation of the application. This realm must exactly match the openid.realm used in OpenID requests.
  • Data access scope: Since this application needs access to Google Calendar to present the next upcoming Calendar appointment, the scope for Google Calendar must be listed in the manifest, and referenced by one of the other extensions (in this case, the navigation link which represents the web application). A list of the valid scopes can be found in the manifest documentation. A <Reason /> is also supplied for each scope, indicating the reason the application needs access to the specified data. This is presented to the administrator during the install process.
<?xml version="1.0" encoding="UTF-8" ?>
<ApplicationManifest xmlns="http://schemas.google.com/ApplicationManifest/2009">
  <Name>Hello World</Name>
  <Description>Demonstrates a simple Google Apps Marketplace application</Description>

  <!-- Administrators and users will be sent to this URL for application support -->
  <Support>
    <Link rel="support" href="http://www.googlecodesamples.com/support.jsp" />
  </Support>

  <!-- Show this link in Google's universal navigation for all users -->
  <Extension id="navLink" type="link">
    <Name>Hello World</Name>
    <Url>http://www.googlecodesamples.com/openid?hd=${DOMAIN_NAME}</Url>
    <Scope ref="calendarAPI"/>
  </Extension>

  <!-- Declare our OpenID realm so our app is white listed -->
  <Extension id="realm" type="openIdRealm">
    <Url>http://www.googlecodesamples.com/</Url>
  </Extension>

  <!-- Need access to the Calendar API -->
  <Scope id="calendarAPI">
    <Url>https://www.google.com/calendar/feeds/</Url>
    <Reason>This app displays the user's next upcoming Google Calendar appointment.</Reason>
  </Scope>
</ApplicationManifest>

More information about application manifests, including additional configuration options, is available in the manifest documentation.

Listing the App

The Google Apps Marketplace is located at http://www.google.com/appsmarketplace. On the Marketplace, you can create private listings and use them for development and testing purposes.

After you visit the Marketplace, you'll need to Sign In and then Become a Vendor before creating your first listing. These links are available at the top-right of the home page. Every valid Google account and Google Apps account can create a vendor profile, but listings cannot be shared between accounts.

The next step is to create your first listing. Be sure to check the checkbox at the top that says "My product may be directly installed into Google Apps domains" as this (along with the application category) cannot be changed after the listing is created and listings cannot currently be deleted.

Copy and paste the manifest from above, modifying the http://www.googlecodesamples.com/ URLs to point to your server, and then click on the "Save and Preview" button and you're done!

Retrieving the OAuth Key and Secret

Every listing for an installable application on the Marketplace is associated with an OAuth key and secret. This key and secret is used to access the APIs specified as <Scope /> elements in the manifest file using 2-legged OAuth. The specified data for all users on all domains which have installed the application can be accessed, based on the permissions granted by the domain administrator during the install process. This enables your application to seamlessly integrate with a user's data to do things like create calendar appointments, utilize a user's Google Apps contacts within your application, upload documents, spreadsheets or PDFs to Google Docs and more.

The OAuth key and secret can be accessed by visiting your Vendor Profile using the link at the top right of the Marketplace. Each installable listing will have a separate OAuth key and secret. Please note where this information is accessed as you'll need to replace the key and secret with your generated value.

Launching

Launch your application is easy! Simply create a new listing on the Google Apps Marketplace, supplying the same information as you supplied in your development listing. After you complete the new listing form, you'll need to submit your listing for publishing. After it is reviewed and approved, it will be live on the Marketplace.

You should remember to grab the new OAuth key and secret for your production listing. This key and secret will be different from the key and secret used by your development listing.

Appendix

Redirecting to your site during Installation

The application install flow has an optional step 3 which allows your application to collect additional information from the administrator during the installation process. This can include contact information, billing information or other information required to setup the account for the business.

The optional 3rd step can be specified by supplying a link in the manifest file such as: <Link rel="setup" href="http://www.googlecodesamples.com/setup?hd=${DOMAIN_NAME}"> along with the other <Link /> elements. The ${DOMAIN_NAME} substitution is important as it enables the OpenID discovery process discussed above. An additional query parameter named callback will automatically be added to the end of your setup URL. After the administrator has completed the setup process in your application, you should redirect them to the callback URL so they can enable the application.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.