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) andaud
(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
andaudience
.{ "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.