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.
Environment details
TL;DR
impersonate.CredentialsTokenSourcewithSubjectset signs a JWT viaiamcredentials.signJwtand then exchanges it athttps://oauth2.googleapis.com/tokenusing 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 signedwith 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 whatgoogle-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
signJwton works; a freshly-created SA reproduces it immediatelysince its key is new.
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/authhas the same legacy grant in its equivalent flow(user.go),
so it needs the same fix.
Is the legacy
grant_type=assertionpath being turned down server-side? Its validatorseems 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/jwtuses the moderngrant), or drop
Subjectif plain impersonation works for your case.