anthropics / anthropics/claude-code
WIF: token-exchange assertions are single-use (undocumented) — shared identity-token file + another consumer ⇒ opaque 401 with a misleading hint
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## 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)
```bash
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
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the WIF reference and token-file guidance, then inspect Claude Code's token-exchange retry and error-hint entry points. Reproduce the first successful exchange and subsequent 401 with the same assertion. Done should include documented single-use assertions and a replay-aware CLI path, with token-file re-reading considered between retries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github-actions
- Domain
- authentication, cli, documentation
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100