Google Apps Platform

Authentication Best Practices

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.

This document provides authentication best practices for an application developed for the Google Apps Marketplace or an in-house application using the Google Apps extensions console.

Contents

OAuth

Security considerations with 2-legged OAuth

2-Legged OAuth for installed applications is a power feature that eases integration and enhances the user experience. To quote FDR (or Spiderman, take your pick), power must be linked with responsibility — the ability to impersonate users must not be abused by either the application or its users.

Unlike ClientAuth/Basic authentication that requires you possess the credentials of the user who's data you're accessing, 2LO allows your application to impersonate any user on a domain that authorizes your application using a single application-wide key and secret. Typically applications use 2LO where they set the xoauth_requestor_id parameter based on the currently authenticated user and never expose any mechanisms for users to directly manipulate that parameter.

Allowing the user to manipulate the xoauth_requestor_id opens up serious security vulnerabilities. Not only would the user be able to access the data of any user in their domain, but they'd be able to access the data of any user in any domain that installed your application!

Some tips:

  • Always set xoauth_requestor_id to the ID of the currently logged-in user. There are exceptions, such as with user delegation features and "userless" APIs like the user & group feeds.
  • Never allow a user to manipulate xoauth_requestor_id parameters, and actively guard against it. This value must be restricted to the current user's ID or a pre-authorized set of accounts approved by an administrator. In either case, it must always be restricted to the domain of the customer.
  • Never allow a user to set the domain name part of a feed URL (as in the apps admin APIs). This must always be restricted to the domain of the customer.
  • Protect your environment and guard your key and secret well. A compromise of your server not only puts your own data at risk, but also customer data stored at Google. Treat the OAuth key and secret as you would any other sensitive cryptographic data.
  • While the potential for harm is greater with 2LO, similar guidelines apply for 3-legged OAuth well.

OpenID Single Sign-On

OpenID itself has proven to be a well-vetted and secure protocol for SSO. However, as with all technologies, security vulnerabilities can still occur due to misuse or misunderstanding of the technology. We'd like to share a few important notes to help make sure your adoption of OpenID remains problem free.

Use existing libraries

If the Relying Party (RP) doesn't implement OpenID correctly, it puts its users at risk. Making a mistake in your OpenID implementation is like having a password-based login, but not checking the password. A correct OpenID implementation has to cover checking of cryptographic signatures, checking of nonces, Yadis discovery (where it matters whether you do or don't follow HTTP redirects), etc.

We strongly discourage Relying Parties from implementing OpenID from scratch. Instead, look for existing libraries that have been tested and vetted by the larger community.

Libraries supporting the OpenID Google Apps discovery extensions are available in a number of languages:

Support for discovery extensions is also available when using MyOneLogin, Ping Connect, and RPX.

Claimed identifiers vs. email addresses

Many OpenID Identity Providers (IDP), Google included, use opaque identifiers as the user's identity instead of email addresses or other human-recognizable IDs. There are several reasons for this approach:

  • Privacy. OpenID is a decentralized authentication system. Any RP can issue a login request to an IDP without any prior relationship that establishes trust. Using opaque identifiers avoids inadvertently sharing sensitive user information while still giving users the benefits of a SSO. Users can remain anonymous or chose to share their identity with those sites they trust. Some IDPs go so far as providing different IDs for each relying party so user accounts can't be correlated across sites.
  • Security and reliable identification. Using identifiers that are unique to the account instead of an email address allows RPs to reliably identify a unique account holder without having to worry about the implications of recycled or out-of-date email addresses. Identifiers like email addresses don't provide sufficient information to identify individuals in the long term because users may change their email address or lose access to it. Additionally, email service providers may close a user's email account or recycle the username. Basically, the johndoe@example.com you knew 2 years ago might not be the same johndoe@example.com today, which is why using synthetic identifiers is so important to security.

Your application might ask for personal information like names and email address when requesting login, but it is up to the users' discretion whether or not they share this information.

There is a more important implication from this. The claimed ID returned by the IDP is the one and only piece of information that you can use to securely identify user accounts. While you may fetch and store other attributes like email addresses, names, etc., these other attributes should not be used as keys to look up accounts during login. Nor should attributes like email address be used to determine if a set of users belong to the same organization.

Can user attributes from simple registration or attribute exchange be trusted?

In short, no.

The result of an OpenID login is very simple — the user has securely demonstrated they "own" the claimed identifier. Beyond that that there are no guarantees. The protocol does not dictate what the identifier means or how ownership is determined other than which IDP is responsible for making that determination. There are no guarantees when it comes to user attributes retrieved via simple registration (SREG) and attribute exchange (AX) extensions. Unless you have an explicit trust relationship with a particular IDP, you must treat these values as arbitrary and untrusted user input.

To illustrate this, consider two different logins: one to the user's normal IDP (good-idp.com) and the 2nd to another less legitimate one (bad-idp.com).

From a protocol perspective, both logins to the two IDPs are legitimate and the attributes returned by the 2nd IDP appear identical — the same email address, name, and so on. The only thing that differs between the two requests is the user's claimed identity. In fact, relying parties are required to verify that IDPs only return identities that they're authoritative to prevent a rogue IDP from asserting identities from other providers. But nothing prevents a rogue IDP from asserting attributes like names and email address that may not be truthful.

Even with legitimate providers this is possible as users can have identities at several IDPs. Even so, relying parties should not assume that two users with the same email address are in fact the same person unless the user can simultaneously prove ownership of the two identities.

As mentioned, you can't trust attributes unless you first establish a trust relation. There are ways to establish various levels of trust that can help you decide whether or not you want to take user attributes at face value. Take email addresses, for example. A simple check that can increase confidence in the validity of an email address is enforcing a same-origin policy that requires the email address domain to match the domain of the claimed ID. While not a guarantee, it a least limits IDPs to asserting email address within their own domain. For Google Apps domains, users can not change their own email address (but domain administrators can!)

Handling OpenID GET and POST responses

Applications must be able to handle OpenID responses from Google using both GET and POST requests. The OpenID endpoint changes methods based on the length of the response (see step 8 here), using POST once the length exceeds a certain threshold. Applications that use attribute exchange to fetch user attributes are likely to generate responses that exceed this threshold. It is important to handle both GET and POST requests to ensure all authentication requests are successful.

Provisioning accounts

Ad-hoc

The simplest case for provisioning accounts with OpenID is to use it from the start and create accounts as needed as the result of an OpenID login. Simple Registration and AX extensions can make this process very simple and painless for users.

Federating with existing accounts

Applications with existing user accounts and passwords may chose to offer users the option to federate with another account using OpenID. For reasons mentioned earlier, it's important that this is done securely, requiring users to prove ownership of both accounts simultaneously. The basic approach for federating is:

  • Log in using existing username & password
  • Perform an OpenID login to get the user's claimed ID
  • Save the claimed ID for the user's record
  • Subsequent logins can then be performed using OpenID and the correct user record located using the stored claimed identifier

If you choose to accept OpenID logins directly from your web application, consider checking Google's user experience guidelines for federated login. Account Chooser offers a reference implementation using the Google Identity Toolkit.

Pre-created accounts without password

In some cases, particularly paid-for business applications, we don't want to allow just anyone to log in and create an account. Instead, we'd like to allow the administrator for a domain to say ahead of time who should be allowed in. For applications integrating with Google Apps, using the provisioning API to retrieve the list of users in the domain can make this process easy for the administrator.

But admins don't typically know the identity of a user as it exists in OpenID, nor does the Google Apps provisioning API provide the value. If we have limited information and unreliable identifiers like email addresses, how do we correctly associate accounts when the user first logs in?

One solution is to use the email address from the AX extension to locate the user. Despite earlier advice to the contrary, with adequate safeguards it is feasible. Safeguards to consider include:

  • Only accept the email address if the domain matches the claimed ID (e.g. http://acme.com/openid?id=12345 can assert jdoe@acme.com but not jane@competitor.com) The main goal of this is to prevent users from rogue IDPs from claiming one of those accounts. An IDP that allowed users to change their reported email address at will would still present a problem. For Google Apps integration this is not a concern as users can not modify these values.
  • Require users prove ownership of an email address by acknowledging a message sent to the address. This is a common technique used for web apps and can be useful one-time authentication technique to verify the user owns both accounts. The welcome/authorization message could also be sent at the time the administrator first authorizes the account to login. Upon receiving the message and clicking through a specialized URL, the OpenID login can be performed to securely associate the account and identifier. Effectively this is the same as the previous case of federating with an existing account, using a one-time authentication token sent via email to prove ownership of the existing the account.

Bonus material for Java developers

If you're using the Step2 library, it returns additional information in the claimed ID that indicates whether or not discovery was done securely. The discovery process for Google Apps has added security over traditional OpenID methods and adds cryptographic signatures to discovery documents. When you get the claimed id out of an OpenID assertion (which is of type Identifier), the getIdentifier() method and the getUrl() method return two different strings. The getUrl() method returns the claimed id asserted in the OpenID assertion. The getIdentifier() method, however, returns a string that (1) uniquely identifies the user based on his claimed id and (2) uses two different namespaces for claimed ids that were discovered using signed XRDSs vs. non-signed XRDSs. For example, if the claimed id for a user is http://example.com/bob and the discovery on that URL yields signed XRDS documents whose signatures all check out, then getIdentifier() on this ID would return secure:http://example.com/bob (while getUrl() would simply return http://example.com/bob).

Users of the step2 library should use the strings returned by AuthResponseHelper().getClaimedId().getIdentifier() as the index into their database to further take advantage of the signed-XRDSs that we use.

For developers in other languages, rest assured that those libraries are still validating signatures!

Single sign-on within gadgets

Out of the box, gadgets do not support OpenID. However, gadgets do provide a unique identifier per-user that can be correlated with a user's OpenID. Once correlation is complete, you can feel free to use the gadget-provided ID as a means to authenticate the users of your gadget.

Performing OpenID correlation via a gadget

When the user first loads the gadget, the gadget will send a signed osapi.http request to the remote server (controlled by the developer). This request, as it passes through the container site (Gmail, Calendar, Sites), will append both the user's ID and a signature. When the request reaches the remote server, the server will verify the signature (client libraries may also be used, which support both HMAC-SHA1 as well as RSA), and then lookup the user ID.

If the user ID is already paired with an OpenID, no further action is required and the request can return the appropriate signal to the gadget that the user is properly authenticated. If the user ID is not paired, the server will create a one-time-use session, and return a token, that can be used to access this session, back to the gadget.

Once back in the gadget, the gadget displays a link to 'Sign In'. Once clicked, this link will create a popup window to a page on the remote server, passing in the one-time-use token (which the server will use to pair the next steps with the appropriate gadget-provided ID) as well as the domain of the currently signed-in user (to begin OpenID discovery). This page will then perform OpenID SSO. After SSO is complete, which should require no interaction from the user by virtue of already being logged-in to Google Apps, the OpenID will be paired with the user ID from the gadget and stored in the database. The popup window will then close itself. The gadget will periodically check whether or not the popup window is closed. Once it is determined that the window is closed, the gadget will repeat the initial request to the server, to verify the correlation took place and that the user did not close the window prior to the completion of OpenID.

Sample gadget code is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<Module>
  <ModulePrefs
    title="OpenID flow">
    <Require feature="opensocial-0.9" />
    <Require feature="osapi" />
  </ModulePrefs>
  <Content type="html" view="home,canvas,profile"><![CDATA[
    <script type="text/javascript">

    function init() {
      // Hit the server, passing in a signed request (and OpenSocial ID), to see if we know who the user is.
      osapi.http.get({
        'href' : 'http://yourserver.com',
        'format' : 'json',
        'authz' : 'signed'
      }).execute(handleLoadResponse);
    }

    function handleLoadResponse(data) {
      // User exists, OpenID must have occurred previously.
      if (data.content.user_exists) {
        document.getElementById('output').innerHTML = 'user exists';
      // User doesn't exist, need to do OpenID to match user ID to OpenID.
      } else {
        var url_root = data.content.popup;
        // Retrieve the domain of the current user. gadgets.util.getUrlParameters()['parent'] returns a value
        // of of the form: http(s)://mail.google.com/mail/domain.com/html for Gmail (other containers are similar).
        // The example below shows a regular expression for use with Gmail. For Calendar, use this regular
        // expression instead: /calendar\/hosted\/([^\/]+)/
        var domain = gadgets.util.getUrlParameters()['parent'].match(/.+\/a\/(.+)\/html/)[1];

        var url = url_root + '?domain=' + domain;

        var button = document.createElement('a');
        button.setAttribute('href', 'javascript:void(0);');
        button.setAttribute('onclick', 'openPopup("' + url + '")');

        var text = document.createTextNode('Sign in');
        button.appendChild(text);

        document.getElementById('output').appendChild(button);
      }
    }

    function openPopup(url) {
      var popup = window.open(url, 'OpenID','height=200,width=200');

      // Check every 100 ms if the popup is closed.
      finishedInterval = setInterval(function() {
        // If the popup is closed, we've either finished OpenID, or the user closed it. Verify with the server in case the
        // user closed the popup.
        if (popup.closed) {
          osapi.http.get({
            'href' : 'http://yourserver.com',
            'format' : 'json',
            'authz' : 'signed'
          }).execute(handleLoadResponse);

          clearInterval(finishedInterval);
        }
      }, 100);
    }

    gadgets.util.registerOnLoadHandler(init);
    </script>
    <div id="output"></div>
  ]]></Content>
</Module>

The example expects the following JSON format for success:

{'user_exists' : true}

For failure:

{'user_exists' : false, 'popup' : 'http://yourserver.com/openid/popup.html'}

These formats can be changed to suit your purposes.

Using OAuth

If the resources that your gadget retrieves happen to be exposed via an API that supports OAuth, you may be interested in using the OAuth proxy. When using the OAuth proxy, the gadget user will be directed to the remote site on first load, prompted to log in, and then prompted to grant access to the requested feeds of data. On subsequent loads, osapi.http requests to those resources will successfully return data.

Authentication required

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

Signing you in...

Google Developers needs your permission to do that.