WIF: token-exchange assertions are single-use (undocumented) — shared identity-token file + another consumer ⇒ opaque 401 with a misleading hint

Status Open
Reported on v2.1.241
Maintainer reply None cached
Activity 0 comments · opened Aug 24, 2026

Summary

POST /v1/oauth/token (Workload Identity Federation) enforces one-time use per assertion: exchanging the same OIDC JWT a second time returns 401 authentication_error: Authentication failed. This is not mentioned in the WIF docs, and the CLI's error hint ("Ensure your federation rule matches your identity token") points at rule matching, which sends users down the wrong path. Found while migrating GitHub Actions agent workflows to keyless auth; it cost a full day of forensics.

Reproduction (GitHub Actions, any repo with a working federation rule)

JWT=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" | jq -r .value)
# same body twice:
curl -s https://api.anthropic.com/v1/oauth/token -H content-type:application/json -d "{\"grant_type\":\"urn:ietf:params:oauth:grant-type:jwt-bearer\",\"assertion\":\"$JWT\",\"federation_rule_id\":\"fdrl_…\",\"organization_id\":\"…\",\"service_account_id\":\"svac_…\",\"workspace_id\":\"wrkspc_…\"}"
# -> 200, access_token
# (repeat the identical request)
# -> 401 {"error":{"type":"authentication_error","message":"Authentication failed"}}

Observed consistently on 2026-08-23 (JWT age 0–90s made no difference; body/headers/transport identical between the two calls).

How this bites Claude Code users

The WIF reference says the SDK "re-reads ANTHROPIC_IDENTITY_TOKEN_FILE on every exchange so that projected tokens that rotate on disk are always current", and Claude Code (claude-cli/2.1.241, headless -p) follows the same pattern. If anything else exchanges the assertion in that file first — in our case a setup step that validated the federation chain eagerly — Claude Code reads an already-consumed assertion and fails with:

API Error: Token exchange failed with status 401 (request-id …): {"error":{"type":"authentication_error","message":"Authentication failed"}} Ensure your federation rule matches your identity token.

It then retries the same in-memory assertion for ~3 minutes (every retry is a replay, so every retry fails) before giving up. Nothing in the message suggests replay.

Asks

  1. Document the single-use property in the WIF reference (validation rules / troubleshooting) and in the token-file guidance: one consumer per assertion; mint a separate assertion for any additional exchanger.
  2. Make the CLI hint replay-aware — e.g. "…or the identity token was already exchanged (assertions are single-use)". Ideally the CLI re-reads the token file between retries, so a rotating file self-heals instead of replaying the same burned assertion.
  3. (Nice to have) Surface replay/assertion_reused as a distinct deny reason where deny reasons are recorded.

Environment

  • @anthropic-ai/claude-code 2.1.241 (native binary), headless -p mode
  • GitHub Actions self-hosted runners (Linux x64), GitHub OIDC issuer, discovery-mode JWKS
  • Federation rule: exact-subject match, audience https://api.anthropic.com, single workspace, default token lifetime

View original on GitHub ↗