Google Cloud Using one OAuth client to get ID tokens for another client

To obtain an access token or ID token for a user, we need a Client ID and secret. Using these client credentials, we can initiate an OAuth code flow to let the user authorize our app, and as a result, we get an access token. If we use the right parameters, we also receive an ID token and refresh token.

If we look at the tokens we receive, we can see which Client ID they were issued to:

  • The ID token contains an azp (authorized party) and aud (audience) claim, and they both contain the client ID that we used in the OAuth code flow:

    {
      "alg": "RS256",
      "kid": "5962…",
      "typ": "JWT"
    }.{
      "iss": "https://accounts.google.com",
      "azp": "CLIENT.apps.googleusercontent.com",
      "aud": "CLIENT.apps.googleusercontent.com",
      "sub": "1108426257…",
      "at_hash": "kqOOLEjZ…",
      "iat": 1676504495,
      "exp": 1676508095
    }.[Signature] 
    
  • Access tokens are opaque, so we need to use the tokeninfo endpoint to obtain information about them. But again, we can see that our client ID is listed in issued_to and audience.

    {
      "issued_to": "CLIENT.apps.googleusercontent.com",
      "audience": "CLIENT.apps.googleusercontent.com",
      "scope": "https://www.googleapis.com/auth/tasks",
      "expires_in": 2935,
      "access_type": "offline"
    }
    
  • Refresh tokens are also opaque, and there’s no way to introspect them. But refresh tokens are also tied to client credentials – in fact, they’re bound to client credentials. That means we can’t use a refresh token without its corresponding set of client credentials.

At least in the Google ecosystem, we sometimes need to use ID tokens to make backend calls. For example, we use ID tokens to make programmatic calls to IAP-protected resources, or we might have a custom app that uses ID tokens to authenticate users. But backends expect ID tokens to contain a certain audience.

What can we do if we have a set of tokens issued to one OAuth client, but we need an ID token with its audience set to another OAuth client? We could start over and let the user perform another OAuth code flow – but that’d be inconvenient for the user. Is there a way to use our existing tokens to request another set of tokens for a different client ID?

As it turns out, there is: If we have client credentials and a refresh token for one OAuth client, we can use the token endpoint to request an ID token for another OAuth client. To do that, we just have to pass the target OAuth client’s ID in the audience parameter.

Suppose we have client credentials and a refresh token for the client ID CLIENT.apps.googleusercontent.com, then we can request tokens for TARGET-CLIENT.apps.googleusercontent.com like so:

POST https://oauth2.googleapis.com/token HTTP/1.1
…
client_id=CLIENT.apps.googleusercontent.com&
client_secret=CLIENT-SECRET& 
refresh_token=CLIENT-REFRESH-TOKEN….&
grant_type=refresh_token&
audience=TARGET-CLIENT.apps.googleusercontent.com

Which returns an ID token and access token:

{
  "access_token": "ya29.a0AVvZVsr…",
  "expires_in": 3599,
  "scope": "openid https://www.googleapis.com/auth/tasks",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSU…"
}

Notice that we didn’t need the client secret for TARGET-CLIENT.apps.googleusercontent.com.

If we decode the resulting ID token, we can see that it still lists CLIENT.apps.googleusercontent.com as authorized party ( azp), but now contains TARGET-CLIENT.apps.googleusercontent.com as audience (aud):

{
  "alg": "RS256",
  "kid": "5962e7a…",
  "typ": "JWT"
}.{
  "iss": "https://accounts.google.com",
  "azp": "CLIENT.apps.googleusercontent.com",
  "aud": "TARGET-CLIENT.apps.googleusercontent.com",
  "sub": "11084262…",
  "at_hash": "DRbJx…",
  "iat": 1676504699,
  "exp": 1676508299
}.[Signature]

We can now use this ID token to make calls to IAP-protected resources or applications that expect ID tokens with audience TARGET-CLIENT.apps.googleusercontent.com.

The access token however hasn’t changed – it still lists CLIENT.apps.googleusercontent.com in both, issued_to and audience:

{
  "issued_to": "CLIENT.apps.googleusercontent.com",
  "audience": "CLIENT.apps.googleusercontent.com",
  "user_id": "110842625731007582420",
  "scope": "openid https://www.googleapis.com/auth/tasks",
  "expires_in": 3400,
  "access_type": "offline"
}

Using one OAuth client to get an ID token for another client only works if both clients are in the same project. If the target client is in a different project, the token call fails:

{
  "error": "invalid_audience",
  "error_description": 
    "The audience client and the client need to be in the same project."
}

This makes sense: When the user performed the OAuth code flow, they provided consent based on the OAuth consent screen of our app. The consent screen isn’t specific to a single client ID, but covers all client IDs in the same project.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home