Google Apps Platform

Writing your First Marketplace App using Ruby

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 Ruby, 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:

require 'rubygems'
require 'sinatra'

get '/' do
  "Hello World"
end

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:

The sample app uses the Sinatra framework and must also be installed.

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 ruby 1.8.6 or later.

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.

require 'rubygems'
require 'sinatra'

use Rack::Session::Cookie
enable :sessions

require 'gapps_openid'
require 'rack/openid'

use Rack::OpenID

helpers do
  def require_authentication
    redirect '/login' unless authenticated?
  end

  # Check if user authenticated
  def authenticated?
    !session[:openid].nil?
  end

  # Constructs an absolute URL to a path in the app
  def url_for(path)
    url = request.scheme + "://"
    url << request.host

    scheme, port = request.scheme, request.port
    if scheme == "https" && port != 443 ||
        scheme == "http" && port != 80
      url << ":#{port}"
    end
    url << path
    url
  end
end

# Handle login form & navigation links from Google Apps
get '/login' do
  if params["openid_identifier"].nil?
    # No identifier, just render login form
    erb :login
  else
    # Have provider identifier, tell rack-openid to start OpenID process
    headers 'WWW-Authenticate' => Rack::OpenID.build_header(
      :identifier => params["openid_identifier"],
      :required => ["http://axschema.org/contact/email",
                    "http://axschema.org/namePerson/first",
                    "http://axschema.org/namePerson/last"],
      :return_to => url_for('/openid/complete'),
      :method => 'post')
    halt 401, 'Authentication required.'
  end
end

# Handle the response from the OpenID provider
post '/openid/complete' do
  resp = request.env["rack.openid.response"]
  if resp.status == :success
    session[:openid] = resp.display_identifier
    ax = OpenID::AX::FetchResponse.from_success_response(resp)
    session[:user_attributes] = {
      :email => ax.get_single("http://axschema.org/contact/email"),
      :first_name => ax.get_single("http://axschema.org/namePerson/first"),
      :last_name => ax.get_single("http://axschema.org/namePerson/last")
    }
    redirect '/cal'
  else
    "Error: #{resp.status}"
  end
end

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

require 'google_util'

CONSUMER_KEY = ENV['CONSUMER_KEY']
CONSUMER_SECRET = ENV['CONSUMER_SECRET']

# Display upcoming calendar appointments
get '/cal' do
  require_authentication

  oauth_consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET)
  access_token = OAuth::AccessToken.new(oauth_consumer)
  client = Google::Client.new(access_token, '2.0');
  feed = client.get('https://www.google.com/calendar/feeds/default/private/full', {
    'xoauth_requestor_id' => @user_attrs[:email],
    'orderby' => 'starttime',
    'singleevents' => 'true',
    'sortorder' => 'a',
    'start-min' => Time.now.strftime('%Y-%m-%dT%H:%M:%S')
  })
  throw :halt, [500, "Unable to query calendar feed"] if feed.nil?
  @events = []
  feed.elements.each('//entry') do |entry|
    @events << {
      :title => entry.elements["title"].text,
      :content => entry.elements["content"].text,
      :start_time => entry.elements["gd:when"].attribute("startTime").value,
      :end_time => entry.elements["gd:when"].attribute("endTime").value
    }
  end
  erb :events
end
require 'oauth'
require 'rexml/document'

# Simple helper around the ruby oauth library for making Google Data API requests
module Google
  class Client
    attr_accessor :oauth_token
    attr_accessor :version

    def initialize(token, version = '1.0')
      @token = token
      @version = version
    end

    def get(base, query_parameters)
      make_request(:get, url(base, query_parameters))
    end

    def make_request(method, url)
      response = @token.request(method, url, { 'GData-Version' => version })
      if response.is_a?(Net::HTTPFound)
        url = response['Location']
        return make_request(method, response['Location'])
      end
      return unless response.is_a?(Net::HTTPSuccess)
      REXML::Document.new(response.body)
    end

    private

    def url(base, query_parameters={})
      url = base
      unless query_parameters.empty?
        url += '?'
        query_parameters.each { |key, value|
          url += "#{CGI::escape(key)}=#{CGI::escape(value)}&"
        }
        url.chop!
      end
      url
    end
  end
end
<section>
    <p>
    Upcoming calendar events:
    <ul id="event_list">
        <% @events.each do |event| %>
        <li class="event">
            <span class="event_attr">Title: <%= event[:title] %></span>
            <span class="event_attr">Content: <%= event[:content] %></span>
            <span class="event_attr">Start Time: <%= event[:start_time] %></span>
        </li><% end %>
    </ul>
    </p>
    <% if @events.empty? %>
      <p>You have no scheduled events.</p>
    <% end %>
</section>

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
  • Universal navigation link: The application will be accessed from the Google universal navigation using the URL http://www.googlecodesamples.com/login?openid_identifier=${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" />
  </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_identifier=${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?openid_identifier=${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.