googleapis / googleapis/google-api-go-client
impersonate: JWT exchanged with legacy grant_type=assertion, now rejected with "invalid_grant: Invalid JWT Signature"
- Dominant language
- Go
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 32
Description
#### Environment details
- Programming language: Go
- OS: Linux and macOS (also reproducible with plain curl, see below)
- Language runtime version: go1.23
- Package version: v0.283.0 (the relevant code is unchanged on master)
#### TL;DR
`impersonate.CredentialsTokenSource` with `Subject` set signs a JWT via
`iamcredentials.signJwt` and then exchanges it at `https://oauth2.googleapis.com/token`
using the pre-RFC 7523 legacy grant:
https://github.com/googleapis/google-api-go-client/blob/v0.283.0/impersonate/user.go#L144-L148
Since around 2026-07-16, that exchange fails with
`{"error":"invalid_grant","error_description":"Invalid JWT Signature."}` for JWTs signed
with recently-rotated system-managed keys. The exact same JWT is accepted when exchanged
with `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, which is what
google-auth-library-python uses for this same flow
([`_client.py`](https://github.com/googleapis/google-auth-library-python/blob/main/google/oauth2/_client.py),
[`impersonated_credentials.py`](https://github.com/googleapis/google-auth-library-python/blob/main/google/auth/impersonated_credentials.py)).
Google rotates system-managed keys every ~2 weeks, so every user of this code path
breaks as soon as their SA's key rotates — with no client-side change involved. We hit
this through oauth2-proxy (it uses this package for Google Groups authorization) in four
unrelated GCP projects: in each one, failures started right after that SA's key
rotation, intermittent at first, 100% within a week. Config had been untouched for
months, and we verified every other hop (STS, `generateAccessToken`, `signJwt`) works.
#### Steps to reproduce
Any SA you can call `signJwt` on works; a freshly-created SA reproduces it immediately
since its key is new.
```bash
SA=@.iam.gserviceaccount.com
NOW=$(date +%s)
cat > claims.json < {"error":"invalid_grant","error_description":"Invalid JWT Signature."} (HTTP 400)
# same JWT, RFC 7523 grant:
curl -s https://oauth2.googleapis.com/token \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data-urlencode "assertion=$(cat signed.jwt)"
# -> {"access_token":"ya29...","expires_in":3599,"token_type":"Bearer"} (HTTP 200)
```
The only difference between the two requests is the grant format; the JWT is
byte-for-byte the same and was signed by Google minutes earlier, so this isn't a
malformed assertion or a key problem on the caller's side.
#### Expected behavior
The exchange should use the RFC 7523 grant like the Python library does. It's a
two-line change in `exchangeToken` — happy to send a PR.
Note that `cloud.google.com/go/auth` has the same legacy grant in its equivalent flow
([user.go](https://github.com/googleapis/google-cloud-go/blob/main/auth/credentials/impersonate/user.go)),
so it needs the same fix.
Is the legacy `grant_type=assertion` path being turned down server-side? Its validator
seems to no longer pick up newly-rotated keys, which is what makes the error so
misleading. If there's a decommission timeline it would be good to know, since affected
users will keep trickling in as their keys rotate.
Workaround meanwhile: use a SA JSON key (`golang.org/x/oauth2/jwt` uses the modern
grant), or drop `Subject` if plain impersonation works for your case.
Contributor guide
Research direction
Start in impersonate/user.go at exchangeToken and compare its token request with the RFC 7523 request shown in the reproduction and the Python library references. Verify the legacy grant is replaced for the Subject flow, then rerun the curl reproduction to confirm the exchanged JWT is accepted.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100