Inside the Android Play Service's Magic OAuth Flow

I was looking into how the Google Play library actually obtains OAuth tokens on behalf of apps, as the actual nuts and bolts remain undocumented. Its implementation offers some interesting insights into how Google handles issues that crop up when using OAuth in an Android app. The rest of this post presumes you know a bit about Android, OAuth as well as how to use the Google Play Services library to obtain access tokens. Sorry if all this is down in the weeds, but that's where the fun bits are!

To get a disclaimer out of the way. These are just my observations after poking at various dex files and network traffic and I've undoubtedly missed many things; but I do hope to show you the broad outline of how it works.

There are three points of interest.

1. Critical code runs only within a Google signed app (eg: the com.google.android.gms/.auth.GetToken service.)

2. This service approves apps locally, and obtains access tokens using a locally stored master token. It effectively replaces the traditional web-based OAuth approval flow.

3. Third-party apps are identified by their signature and package name, rather than an application-key/application-secret.

separation of responsibilities

The green areas run "trusted" code. Your app uses the Google Play Services client library; but the library itself doesn't run critical code. It instead forwards calls to services and activities running within a separately installed app ("Google Play Services".)

Here are some interesting bits of the manifest file for com.google.android.gms (the "Google Play services" app) so you get a feel for how it is set up.

<manifest
  android:sharedUserId="com.google.uid.shared"
  package="com.google.android.gms"
  ...>
  <service
    android:name=".auth.GetToken"
    android:exported="true"
    android:process="com.google.process.gapps"/>

  <activity
    android:name=".auth.login.LoginActivity"
    android:exported="true"
    android:process="com.google.android.gms.ui"
    .../>

As an app writer, you typically call GoogleAuthUtil.getToken() from your application. The Play Services library first verifies that the required Google apps (com.android.vending and com.google.android.gms) are installed, and also that they have an acceptable version and signature. The acceptable version numbers and signature are embedded directly within the library. Your requested OAuth scope is passed to the service, and now we're running inside the trusted Play Services app.

Things start to get interesting within the get-token service.

This service first retrieves the app package name and signature of the caller. This pair (caller_package_name,caller_signature) is used to identify the caller. This mutual identification/verification by the service and the calling library takes place right at the outset; and presumably makes it more difficult for either the caller or a rogue "Play Service" to spoof their identity to the other.

The service directly manages app approval, shows dialogs to the user as needed, and creates access tokens for requested scopes. In other words, it performs the app-approval that would otherwise typically be done by the web-site.

This approach does have some advantages. By using a locally running service in a trusted Google app, Google can take advantage of some of the security features within Android.

For example, by using the package signature to identify your application, it eliminates the need to embed application ids and secrets in your apk (which can often be extracted out of a downloaded application; allowing the bad guys to sign requests as if they came from your app.) Package signatures are much harder to spoof - you'll need the private signing key which is (hopefully!) never revealed; so this is a better way to identify an app.

Further, all the access approval UI and token acquisition logic is sandboxed inside the play app rather than being left to the app-writer. Presumably, this reduces the "attack-surface", and also allows bugs to be addressed quickly by updating this single app.

You might now be imagining the flip side of such a powerful Android service, and you'd be right. This service has to be secure and correctly alert the user during approval; for once provisioned, it is capable of creating access tokens on behalf of any app, and with any scope.

The get-token service does all this using what I call a master token that it obtains from an undocumented authentication endpoint at https://android.clients.google.com/auth. Here's how it works.

When you first add an account to the device (say during device setup) the service posts your password, which is additionally encrypted with, I believe, a public key whose private counterpart is available to the web endpoint.

POST https://android.clients.google.com/auth
Parameters
----------
accountType:      HOSTED_OR_GOOGLE
Email:            xxx@gmail.com
has_permission:   1
add_account:      1
EncryptedPasswd:  <some base64 encoding>
service:          ac2dm
source:           android
androidId:        <deviceid>
device_country:   us
operatorCountry:  us
lang:             en
sdk_version:      17

A successful login returns back a bunch of user information and tokens, one per line.

SID=...
LSID=...
Auth=...
services=hist,mail,lh2,talk,oz
Email=...
Token=1/zMASTERTOKEN
GooglePlusUpgrade=1
PicasaUser=...
RopText=
RopRevision=1
firstName=...
lastName=...

Note the Token field - this is the one master token to rule them all.

The master token is stored on the device using the AccountManager. You should be aware that in most device configurations, AccountManager stores this token in an unencrypted sqlite database (accounts.db - usually somewhere under /data/system.) Protection is primarily through the basic linux file-system access controls - the directories are accessible only to system processes.

My understanding of the Android Security Team's position is that anything else is fundamentally security theatre. Encrypting the data or the filesystem is a tricky subject and solutions are often contentious. At any rate; it means rooted devices (or devices that can be rooted through an OS/driver weakness) are at risk of exposing the master token - so be aware.

Next, a set of core google services request OAuth tokens for their scopes. This also reveals how the get-token service generates access tokens using the master token. Here for example, is how it creates a token for one of the scopes requested by the market app.

POST https://android.clients.google.com/auth
Parameters
----------
accountType:      HOSTED_OR_GOOGLE
Email:            ...
has_permission:   1
Token:            1/zMASTERTOKEN
service:          sierra
source:           android
androidId:        <deviceid>
app:              com.android.vending
client_sig:       38918a453d07199354f8b19af05ec6562ced5788
device_country:   us
operatorCountry:  us
lang:             en
sdk_version:      17

and sure enough - it gets back:

SID=...
LSID=...
Auth=<auth_token>
issueAdvice=auto
services=hist,mail,lh2,talk,oz

Indeed, all it takes is to add the has_permission=1 flag to a request containing the master token, and down comes an access token for the desired scope. I also believe this permission is automatically added if the service notices that the requestor signature is the same as the google app signature; which is in fact the SHA value you see above.

What happens when you request a token from your own app via GoogleUtils.getToken() for the userinfo.profile scope?

POST https://android.clients.google.com/auth
Headers
-------
device:          <deviceid>
app:             <app-package-name>
Parameters
----------
device_country:                us
operatorCountry:               us
lang:                          en_US
sdk_version:                   17
google_play_services_version:  4132532
accountType:                   HOSTED_OR_GOOGLE
Email:                         <email>
source:                        android
androidId:                     <device_id>
app:                           <app-package-name>
client_sig:                    <app-sha-signature>
service:                       oauth2:https://www.googleapis.com/auth/userinfo.profile
Token:                         1/zMASTERTOKEN

Note the absence of the has_permission=1 flag, and that the client_sig is now the signature of the calling app.

The response is:

issueAdvice=consent
Permission=View+basic+information+about+your+account
ScopeConsentDetails=%0AView+your+name%2C+public+profile+URL%2C+and+photo%0AView+your+gender%0AView+your+country%2C+la
nguage%2C+and+timezone%0A
ConsentDataBase64=...

The user-interface is controlled by the issueAdvice flag in the response. Automatically approved apps get the issueAdvice=auto flag and an access token. issueAdvice=consent causes the service to return an Intent that if launched, shows a suitable consent dialog. (The Play Services client library bundles this Intent into a UserRecoverableAuthException.)

What happens when you approve a consent dialog? Nothing much - the service merely adds the has_permission=1 flag to a similar request and gets back an access token. It really can create access tokens for any and all scopes.

By the way - this also indicates how the verified app call mechanism likely works. If you specify a audience:server:client_id scope, the token service passes it as usual with the (caller_package,caller_signature) pair to the server. The server checks if this matches the information you separately registered for that app, and returns a JSON Web Token asserting this fact.

Naturally, all this assumes the basic Android system, as well as the "trusted" Play Services app can securely identify the calling package; and that nobody other than the trusted app has access to the master token.

Given those assumptions, it's a nice technique. The Play Services App contains an omnipotent "local-oauth-service"; playing the role of the web-based approval flow but with an Android flavor. Third-party apps are identified directly by their app signature, removing the need to embed app secrets within the apk file.

Most users need (and should) enter their google password only when setting up their device. Apps no longer use the inherently insecure Webview approach to trigger the approval flow; nor do they need to use the awkward and tedious flow via a browser Intent. The app never sees anything other than the access token itself. Critical code runs only in the Play Services app, so bugs can be fixed by just updating one app.

Downsides?

Be aware there's a master token stored on your Android device which has the latent ability to grant access to services you might not even be accessing from it. If that token is ever exposed, you should assume that all data associated with the account is up for grabs. Use the Android Device Manager to reduce the window of opportunity if your device is stolen, or manage this master token from your security settings. Or, use a low-value account just for your android devices; and keep critical documents in a separate account.