impersonate: JWT exchanged with legacy grant_type=assertion, now rejected with "invalid_grant: Invalid JWT Signature"
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 68/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- go
- Domain
- authentication
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.
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,
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.
SA=<sa>@<project>.iam.gserviceaccount.com
NOW=$(date +%s)
cat > claims.json <<EOF
{"iss":"$SA","sub":"$SA","aud":"https://oauth2.googleapis.com/token","scope":"https://www.googleapis.com/auth/admin.directory.group.member.readonly","iat":$NOW,"exp":$((NOW+3600))}
EOF
gcloud iam service-accounts sign-jwt claims.json signed.jwt --iam-account="$SA"
# what impersonate/user.go sends:
curl -s https://oauth2.googleapis.com/token \
-d grant_type=assertion \
--data-urlencode "assertion_type=http://oauth.net/grant_type/jwt/1.0/bearer" \
--data-urlencode "assertion=$(cat signed.jwt)"
# -> {"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),
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.
- Dominant language
- Go
- Stars
- 4.5k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 26
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from googleapis/google-api-go-client
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
googleapis/google-api-go-client#3665 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 78/100
googleapis/google-api-go-client#3739 · 1 comment · 1 reaction ·
-
priority: p3 type: question
Difficulty 3/5 1-2 days Newbie friendliness 55/100
googleapis/google-api-go-client#3727 ·
-
priority: p2 type: bug
Difficulty 4/5 3-5 days Newbie friendliness 48/100
googleapis/google-api-go-client#3510 · 2 comments ·
-
Support passing otelhttp.Option and otelgrpc.Option for OpenTelemetry instrumentation of clients Openpriority: p3 type: feature request
googleapis/google-api-go-client#3138 · 5 comments · 1 assignee ·
All issues in googleapis/google-api-go-client
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 84/100
-
enhancement needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
kind/cleanup
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
kubernetes-sigs/kueue#15947 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
sympozium-ai/sympozium#627 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100