Google makes it pretty easy to use a service account with domain-wide delegation through their SDKs (spoiler alert, you just download the service account credentials file from Google, point the SDK at it, and then call a setSubject('whoever@example.com')
method).
But how do you do this without their SDKs (which are usually thoroughly undocumented) getting in the way?
The answer turned out to actually be pretty simple, first you just create and sign a JWT (with algorithm RS256!), and then you use that to request a bearer token, which you can use in further requests. (Why can’t you use the JWT directly? Good question.)
- the key for signing is the private_key in the service account JSON as
private_key
- the
scope
is the URL-string scopes, as a space-separated list - the
iss
is the service account email (client_email
in the JSON) - the
sub
is the impersonated user - the
aud
is https://oauth2.googleapis.com/token - the
exp
is time()+1800 (they specify a “60 minute timeframe”) - the
iat
is time()-1800
{
"scope": "https://www.googleapis.com/auth/drive",
"email_verified": false,
"iss": "blablabla@blablabla-123456.iam.gserviceaccount.com",
"sub": "john.doe@example.com",
"aud": "https://oauth2.googleapis.com/token",
"exp": 1726783361,
"iat": 1726779761
}
jwt.io makes this pretty easy, although I prefer to do it locally. Make sure to use the correct algorithm (up above the textboxes on jwt.io), and then paste the above into the “payload” box. You can leave the public key blank (jwt.io will complain about an “Invalid Signature” because it can’t verify it, but it works fine), and paste the private key in. You should turn the key into raw text instead of a JSON string, so jq -r .private_key credentials.json
.
Then just POST the JWT to https://oauth2.googleapis.com/token
:
curl https://oauth2.googleapis.com/token \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
-d assertion=$JWT
The (JSON) response will contain a bearer token in access_token
. (And an expiry in expires_in
, looks like 3600)
This bash command will return just the bearer token, given the JWT:
jq -r .access_token <(curl https://oauth2.googleapis.com/token \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer -d assertion=$JWT)
And here’s an example of putting it all together for an API request:
curl --get 'https://www.googleapis.com/drive/v3/files' \
-d corpora=user -d orderBy=recency \
--data-urlencode "q='jane.smith@example.com' in owners" \
-H "Authorization: Bearer $(jq -r .access_token <(curl https://oauth2.googleapis.com/token -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer -d assertion=$JWT))" \
-H 'Accept: application/json'
Addendum: you can retrieve the public key from Google if you really want to verify the signature yourself.
curl -q https://www.googleapis.com/service_accounts/v1/metadata/x509/$(jq -r .client_email credentials.json) \
| jq -r .$(jq .private_key_id credentials.json)
(This returns an X509-style certificate; jwt.io claims to also accept JWKs, and Google provides the JWK from another URL, but it doesn’t work. 🤷♂️)
https://developers.google.com/identity/protocols/oauth2/service-account#httprest