Google OAuth2 Assertion Flow

Overview

As part of Google OAuth2 support, we're starting to experiment with a set of OAuth2 assertion flows to minimize the use of shared secrets. We have implemented assertion flows for the following three use cases:

  1. A Google AppEngine app calling a Google API on behalf of itself.

  2. A non-Google app calling Google API on behalf of itself.

  3. OAuth2 webserver flow with assertion.

Scenario 1: A Google AppEngine app calling a Google API on behalf of application itself

Several weeks ago, we announced a new Google App Engine API called app_identity API. Any GAE app can call this API to sign arbitrary string blob, and the signing secrets are maintained by Google App Engine. If GAE apps use the signing API to generate a JWT, that app can create a JWT which identifies the specific GAE app itself. That JWT will be automatically understood by any Google API endpoint for authenticating that GAE app. Here are the steps for how to use this flow:

  • Construct a JWT token for the GAE application (our current JWT token format largely follows community spec defined here, with some slight differences). The following payload fields are required:

    • iss: issuer of the token, GAE app can get this value by calling GAE app_identity API: app_identity.get_service_account_name().

    • aud: audience of the token, in this case, always put Google Token endpoint URL for this field: https://accounts.google.com/o/oauth2/token

    • scope: requested API scope string, for example 'https://spreadsheets.google.com/feeds/', multiple scopes can also be requested at the same time by using space to separate different scope strings, for example: 'https://spreadsheets.google.com/feeds/ https://docs.google.com/feeds/'

    • iat and exp: the two fields define lifetime of the token, currently our implementation only accepts short-lived JWT, the max lifetime of accepted JWT is 1 hour.

    • alg: currently RSA-SHA256 is the only supported algorithm, that's also the only supported algorithm in GAE app_identity API

    • kid: ignore it or set it to empty string

  • Generate signature: calls GAE app_identity API: app_identity.sign_for_app(string blob)

  • Exchange JWT for an OAuth2 access token, there's already a community spec to standardize this flow:

    • Do sn Http post to Google OAuth2 Token endpoint:

        • Post /o/oauth2/token

        • host: https://accounts.google.com

        • Content-Type: application/x-www-form-urlencoded

        • grant_type=http://oauth.net/grant_type/jwt/1.0/bearer&assertion=<JWT>

    • If everything goes fine, Google token endpoint should return OAuth2 access token to the client.

  • Use access token to access Google API on behalf of application

    • GAE app should be able to use the standard Google API library along with the access token issued by assertion flow. Google's GData client library support dropping an existing oauth token to the library, just call service.setAuthSubToken(access_token) in your app.

    • Theoretically the GAE app should be able to access any Google API on behalf of itself, but currently only a small subset of Google APIs supports this kind of access. Over time, we will have more and more APIs support this type of 'application access'.

Scenario 2: A non-Google app calling Google API on behalf of application itself

Just as Google maintains signing secret for all Google App Engine apps, any non-Google parties could maintain their own signing secrets, generate JWT using their own secrets, then access Google API on behalf of the application itself. A registration process is required for this scenario. Here are the steps:

  • Registration: We haven't opened this feature for broader public use, if you interested in trying this, send an email to me, with an HTTPS URL which contains the information of your public keys. Once your request is processed we will issue an Google service account name for your app.

  • Construct a JWT token, almost the same format as in the scenario 1:

    • iss: service account name got from Registration process.

    • aud: audience of the token, in this case, always put Google Token endpoint URL for this value: https://accounts.google.com/o/oauth2/token

    • scope: requested API scope string, for example 'https://spreadsheets.google.com/feeds/', multiple scopes can also be requested at the same time by using space to separate different scope string for example: 'https://spreadsheets.google.com/feeds/ https://docs.google.com/feeds/'

    • iat and exp: the two fields define lifetime of the token, currently our implementation only accepts short-lived JWT, the max lifetime of accepted JWT is 1 hour.

    • alg: currently RSA-SHA256 is the only supported algorithm.

    • kid: an unique string that is a hint for your signing key.

  • Exchange JWT for an OAuth2 access token, same format as scenario 1:

    • Do a http post to Google OAuth2 Token endpoint:

        • Post /o/oauth2/token

        • host: https://accounts.google.com

        • Content-Type: application/x-www-form-urlencoded

        • grant_type=http://oauth.net/grant_type/jwt/1.0/bearer&assertion=<JWT>

    • If everything goes fine, Google token endpoint should return OAuth2 access token to the client.

  • Use access token to access Google API, same as scenario 1.

  • Public key URL Format: This URL contains the information on which the trust relationship between your app and Google is built. Therefore, for security reasons, the URL must be HTTPS with valid server certification, and currently we accept two formats:

    • Specify X509 public certificates in PEM format:

      • a Json object: {<kid>:<x509 certificate in pem format>,<kid>:<x509 certificate in pem format>...}

      • Http response content-type: application/json

    • Specify raw RSA public keys, we use json web key define by community:

      • only RSA-SHA256 is supported

      • Http response content-type: application/json

  • Public key caching: The Public key URL could also set cache control headers to give Google a hint about how to cache public keys. We accept cache control header like this: Cache-Control: max-age=86400. Remember cache header is just a hint for Google, Google doesn't guarantee the cache behavior for public key URL. INVALID SIG


Scenario 3: OAuth2 webserver flow with Assertion

We modified our OAuth2 webserver flow implementation to accept assertions from the OAuth client, instead of sending client_id&client_secret. This is useful in some cases where long-lived client_id&client_secret are not considered strong enough. Here are the steps to use it:

  • Registration: Same, we haven't opened this feature for broader public use, if you interested in trying this, send email to me, with two things:

    • For an app that is not running on Google App Engine, an URL which contains the information of public keys

    • Get a client_id issued by Google OAuth2 developer console which is the a regular OAuth2 registration process. That client_id is the first thing to send via email

    • The public key URL (as described in scenario #2). Though for apps running on Google App Engine just provide the value of app_identity.get_service_account_name()

    • As an email registration response, we will issue a Google service account name.

  • Construct a JWT token:

    • iss: service account name from Registration process or app_identity.get_service_account_name() for GAE app.

    • aud: audience of the token, in this case, always put Google Token endpoint URL for this value: https://accounts.google.com/o/oauth2/token

    • scope: not required since it's already included in OAuth2 webserver flow.

    • iat and exp: the two fields define lifetime of the token, currently our implementation only accepts short-lived JWT, the max lifetime of accepted JWT is 1 hour.

    • alg: currently RSA-SHA256 is the only supported algorithm.

    • kid: an unique for your signing key, maybe empty string.

  • Obtain user's approval: follow regular OAuth2 web server flow.

  • Get refresh token&access token from authorization code:

    • Do http post to Google OAuth2 Token endpoint, instead of sending client_id&client_secret, send a parameter contains client assertion Post /o/oauth2/tokenhost: https://accounts.google.comContent-Type: application/x-www-form-urlencoded client_assertion=<ASSERTION>&redirect_uri=<REDIRECT_URI>&grant_type=authorization_code&code=<authentication code>

    • Google token endpoint should return OAuth2 access token&refresh token to the client

  • Access token refreshment:

    • Do a http post to Google OAuth2 Token endpoint, instead of sending client_id&client_secret, send a parameter contains client assertion Post /o/oauth2/tokenhost: https://accounts.google.comContent-Type: application/x-www-form-urlencoded client_assertion=<ASSERTION>&grant_type=refresh_token&refresh_token=<refresh token>

    • Server response: access token

  • For non-Google app, same public key URL format and cache policy as in scenario 2.

Libraries and Sample App

We also create a sample app for the assertion flows mentioned in this document: http://assertion-flow-demo.appspot.com

  • /doclist-google: it shows a deadly simple GAE app which uses Google DocList API to manage documents which belongs to the app itself.

  • /doclist-3rd: same functionality as /doclist-google, but the documents are owned by an Non-Google hosted app. AppEngine here becomes a neutral hosting platform(purely for convenience), the app could be hosted at anywhere.

  • /3lo-service-account: demonstrates a Non-Google hosted app uses OAuth2 webserver flow with signed assertion to access Google API. Same, AppEngine here becomes a neutral hosting platform(purely for convenience), the app could be hosted at anywhere.

Source code for those samples can be found here.

The JWT token library we used in sample app can be found here.

NOTES:

  • You may occasionally see timeout error, this is largely because this sample app is a low-traffic app, each HTTP request may cause a cold application startup. Just refresh your page.

  • You should use the latest App Engine SDK for the app_identity API.