Google Apps Platform

Writing your First Marketplace App using .NET

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 .NET, 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:

<html>
<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 library 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. The solution was created with Visual Studio 2008 and targets .NET Framework 2.0. It is also configured to be deployed on IIS, but this behavior can be changed in the project's properties to match your test environment.

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:

  • Default.aspx is the page the user first visits when accessing the application from the universal navigation bar. This page kicks off the OpenID discovery process and login flow based on the domain query parameter.
  • Default.aspx.cs is the code-behind file for the Default.aspx page.
  • Marketplace.Master is the master page used by all other pages to have a consistent layout.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Marketplace.Default"
    MasterPageFile="~/Marketplace.Master" %>

<asp:Content ID="Content1" ContentPlaceHolderID="Main" runat="Server">
    <asp:Label runat="server" Text="OpenID Login" />
    <asp:TextBox ID="openIdBox" runat="server" />
    <asp:Button ID="loginButton" runat="server" OnClick="loginButton_Click" Text="Login" />
    <br />
    <asp:Label ID="loginFailedLabel" runat="server" EnableViewState="False" Text="Login failed"
        Visible="False" />
    <asp:Label ID="loginCanceledLabel" runat="server" EnableViewState="False" Text="Login canceled"
        Visible="False" />
</asp:Content>
  
using System;
using System.Web;
using System.Web.Security;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.RelyingParty;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OAuth;

namespace Marketplace
{
    public partial class Default : System.Web.UI.Page
    {
        private static readonly OpenIdRelyingParty relyingParty;

        static Default()
        {
            HostMetaDiscoveryService googleAppsDiscovery = new HostMetaDiscoveryService
            {
                UseGoogleHostedHostMeta = true,
            };

            relyingParty = new OpenIdRelyingParty();
            relyingParty.DiscoveryServices.Insert(0, googleAppsDiscovery);
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            IAuthenticationResponse authResponse = relyingParty.GetResponse();
            if (authResponse != null)
            {
                switch (authResponse.Status)
                {
                    case AuthenticationStatus.Authenticated:
                        FetchResponse fetch = authResponse.GetExtension<FetchResponse>();
                        if (fetch != null)
                        {
                            // Save user details in session variables
                            Session["email"] = fetch.GetAttributeValue(WellKnownAttributes.Contact.Email);
                            Session["firstName"] = fetch.GetAttributeValue(WellKnownAttributes.Name.First);
                            Session["lastName"] = fetch.GetAttributeValue(WellKnownAttributes.Name.Last);
                        }

                        // Set the authentication cookie for the user
                        FormsAuthentication.SetAuthCookie(authResponse.ClaimedIdentifier, false);
                        Response.Redirect("~/Return.aspx");
                        break;
                    case AuthenticationStatus.Canceled:
                        loginCanceledLabel.Visible = true;
                        break;
                    case AuthenticationStatus.Failed:
                        loginFailedLabel.Visible = true;
                        break;
                }
            }
            else if (Request["domain"] != null)
            {
                IAuthenticationRequest request = relyingParty.CreateRequest(Request["domain"]);
                sendGoogleRequest(request);
            }
        }

        private void sendGoogleRequest(IAuthenticationRequest request)
        {
            // Request access to e-mail address, first name and last name
            // via OpenID Attribute Exchange (AX)
            FetchRequest fetch = new FetchRequest();
            fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, true));
            fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.First, true));
            fetch.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.Last, true));
            request.AddExtension(fetch);

            // Send your visitor to their Provider for authentication.  
            request.RedirectToProvider();
        }

        protected void loginButton_Click(object sender, EventArgs e)
        {
            IAuthenticationRequest request = relyingParty.CreateRequest(openIdBox.Text);
            sendGoogleRequest(request);
        }
    }
}
  
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Marketplace.master.cs"
    Inherits="Marketplace.Marketplace" %>

<%@ Register Assembly="DotNetOpenAuth" Namespace="DotNetOpenAuth.OpenId.RelyingParty"
    TagPrefix="rp" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Google Marketplace Sample</title>
    <asp:ContentPlaceHolder ID="Head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ContentPlaceHolder ID="Main" runat="server">
        </asp:ContentPlaceHolder>
    </div>
    </form>
</body>
</html>

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

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Return.aspx.cs" Inherits="Marketplace.Return"
 MasterPageFile="~/Marketplace.Master" %>

<asp:content id="Content1" contentplaceholderid="Main" runat="Server">
    <div id="welcomeSection" runat="server" visible="false">
    <asp:Label ID="welcomeMessage" runat="server" /><br />
    Current or Next Calendar Event: <br />
    <asp:Label ID="eventLabel" runat="server" />
    </div>

    <div id="unathorizedSection" runat="server">
    <asp:Label ID="Label1" runat="server" Text="You are not authorized to see this page" /><br />
    <a href="Default.aspx">Login</a>
    </div>
</asp:content>
using System;
using Google.GData.Client;
using Google.GData.Calendar;
using Google.GData.Extensions;

namespace Marketplace
{
    public partial class Return : System.Web.UI.Page
    {
        const string APPLICATION_NAME = "yourCompany-YourAppName-v1";

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Request.IsAuthenticated)
            {
                // Show the link to the login page
                return;
            }

            welcomeMessage.Text = string.Format("Hello {0} {1} ({2})", Session["firstName"], Session["lastName"], Session["email"]);
            welcomeSection.Visible = true;
            unathorizedSection.Visible = false;

            if (Session["email"] != null)
            {
                // Get the next calendar event for the user that logged in
                EventFeed events = getCalendarEvents(Session["email"].ToString(), "full", true, 1);
                if (events.Entries.Count > 0)
                {
                    EventEntry entry = (EventEntry)events.Entries[0];
                    eventLabel.Text += "Title: " + entry.Title.Text + "<br />";
                    eventLabel.Text += "When: " + entry.Times[0].StartTime + "<br />";
                    eventLabel.Text += "Where: " + entry.Locations[0].ValueString + "<br />";
                }
            }
        }

        // Get the specified calendar events via the Calendar API
        private EventFeed getCalendarEvents(string requestorId, string projection, bool singleEvents, int maxResults)
        {
            // Create an OAuth factory to use
            GOAuthRequestFactory requestFactory = new GOAuthRequestFactory("cl", APPLICATION_NAME);

            // Available from vendor profile page (next to each application)
            // when logged into the Marketplace 
            requestFactory.ConsumerKey = "REPLACETHIS.apps.googleusercontent.com";
            requestFactory.ConsumerSecret = "REPLACETHIS";

            // Create the CalendarService and set its RequestFactory
            CalendarService service = new CalendarService(APPLICATION_NAME);
            service.RequestFactory = requestFactory;

            // Query the service with the parameters passed to the function
            EventQuery query = new EventQuery();
            query.Uri = new Uri("https://www.google.com/calendar/feeds/default/private/" + projection);
            query.OAuthRequestorId = requestorId;
            query.NumberToRetrieve = maxResults;
            query.SingleEvents = singleEvents;
            query.StartTime = DateTime.Now;
            query.SortOrder = CalendarSortOrder.ascending;

            EventFeed resultFeed = service.Query(query);
            return resultFeed;
        }
    }
}

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/helloworld
  • Universal navigation link: The application will be accessed from the Google universal navigation using the URL http://www.googlecodesamples.com/helloworld?from=google&domain=${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/helloworld" />
  </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/helloworld?from=google&domain=${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.aspx?domain=${DOMAIN}"> 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.