impersonate: JWT exchanged with legacy grant_type=assertion, now rejected with "invalid_grant: Invalid JWT Signature"

Open Beginner friendly
#3,671 0 comments 0 reactions 0 assignees View on GitHub

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

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

priority: p2 type: bug
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from googleapis/google-api-go-client

All issues in googleapis/google-api-go-client

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.