This page collects the configuration surfaces, validation constraints, and error mappings for Workload Identity Federation. For setup walkthroughs, see the provider guides.
POST /v1/oauth/token returns a standard OAuth 2.0 token response (RFC 6749 §5.1):
| Field | Type | Description |
|---|---|---|
access_token | string | The short-lived Anthropic token, prefixed sk-ant-oat01-.... Pass it as Authorization: Bearer <token>. |
token_type | string | Always Bearer. |
expires_in | integer | Seconds until the token expires. |
scope | string | The OAuth scope granted by the matched rule. |
The SDK reads these variables to perform a federated token exchange with no constructor arguments.
| Variable | Required | Description | Example |
|---|---|---|---|
ANTHROPIC_FEDERATION_RULE_ID | Yes | Tagged ID of the federation rule to evaluate. | fdrl_... |
ANTHROPIC_ORGANIZATION_ID | Yes | UUID of your Anthropic organization. Find it in the Claude Console under Settings > Organization. | 00000000-0000-0000-0000-000000000000 |
ANTHROPIC_IDENTITY_TOKEN_FILE | One of _TOKEN_FILE or _TOKEN | Filesystem path to the JWT issued by your identity provider (IdP). The SDK re-reads this file on every exchange so that projected tokens which rotate on disk are always current. | /var/run/secrets/anthropic.com/token |
ANTHROPIC_IDENTITY_TOKEN | One of _TOKEN_FILE or _TOKEN | The literal JWT as a string. Use when your platform injects the token as an environment variable rather than a file. | eyJhbGciOiJSUzI1NiIs... |
ANTHROPIC_SERVICE_ACCOUNT_ID | Yes | Tagged ID of the target Anthropic service account that the issued access token acts as. | svac_... |
ANTHROPIC_PROFILE | No | Name of a configuration profile to load. Takes precedence over the federation environment variables in this table. | staging-profile |
The direct environment-variable federation path activates only when ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, and one of ANTHROPIC_IDENTITY_TOKEN_FILE or ANTHROPIC_IDENTITY_TOKEN are all set.
A variable that is set to an empty string still occupies its slot in the credential precedence chain. If ANTHROPIC_API_KEY="" is exported, the SDK selects the API-key path with an empty key rather than falling through to federation. Unset unused credential variables rather than blanking them.
The SDK resolves credentials in this order. The first source that yields a credential wins.
| Order | Source | Notes |
|---|---|---|
| 1 | Constructor argument (api_key=, auth_token=, credentials=) | Always overrides everything else. |
| 2 | ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN | Shadows federation entirely. Unset these when migrating from API keys. |
| 3 | ANTHROPIC_PROFILE | Loads <config_dir>/configs/<name>.json. A missing named profile is an error, not a fall-through. |
| 4 | Federation environment variables | ANTHROPIC_FEDERATION_RULE_ID + ANTHROPIC_ORGANIZATION_ID + ANTHROPIC_SERVICE_ACCOUNT_ID + ANTHROPIC_IDENTITY_TOKEN[_FILE]. |
| 5 | Active profile | Resolved via <config_dir>/active_config, falling back to a profile named default. |
When a profile is loaded, environment variables fill any fields the profile omits but never override fields the profile sets explicitly.
A profile is a named configuration file that the SDK and the ant CLI both read. Profiles let you ship federation parameters with your container image or switch between environments without changing code.
The SDK locates the configuration directory in this order:
$ANTHROPIC_CONFIG_DIR~/.config/anthropic on Linux and macOS%APPDATA%\Anthropic on WindowsThe active profile name resolves in this order:
$ANTHROPIC_PROFILE<config_dir>/active_config (a one-line file written by ant profile activate <name>)defaultClaude Code and the Claude Agent SDK honor this same resolution order, so a federation profile configured here also authenticates those tools without additional setup.
| Path | Contents | Sensitivity |
|---|---|---|
<config_dir>/configs/<profile>.json | version, the authentication block, organization_id, workspace_id, and base_url. | Non-secret. Safe to commit or bake into an image. |
<config_dir>/credentials/<profile>.json | version, the cached access_token, expires_at, and (for interactive login) refresh_token. | Secret. Written by the SDK with mode 0600. |
Both the config file and the credentials file carry a top-level string version field in major.minor format (currently "1.0"). The SDK writes this field automatically so future releases can detect and migrate older formats; omit it when authoring a config by hand and the SDK treats the file as the current version.
{
"version": "1.0",
"authentication": {
"type": "oidc_federation",
"federation_rule_id": "fdrl_...",
"service_account_id": "svac_...",
"identity_token": {
"source": "file",
"path": "/var/run/secrets/anthropic.com/token"
}
},
"organization_id": "00000000-0000-0000-0000-000000000000",
"workspace_id": "wrkspc_...",
"base_url": "https://api.anthropic.com"
}If authentication.identity_token is omitted, the SDK falls back to ANTHROPIC_IDENTITY_TOKEN_FILE or ANTHROPIC_IDENTITY_TOKEN from the environment.
The oauth_scope you set on a federation rule determines which Claude API endpoints the minted access token can call.
| Scope | Grants access to |
|---|---|
workspace:developer | All non-administrative Claude API endpoints in the rule's workspace: Messages (including streaming and token counting), Models, Managed Agents and their sessions, Files, and Skills. This matches the access an API key issued for the same workspace has. |
A request to an endpoint outside the token's scope returns HTTP 403. Finer-grained scopes (per resource, or read versus write) are not currently available.
Anthropic enforces these constraints when you create or update issuers and rules, and when verifying an incoming JWT at exchange time.
| Field | Constraint |
|---|---|
Issuer, rule, and service account name | Must match ^[a-z0-9-]+$, length 1 to 255 characters. |
workspace_id | The workspace (wrkspc_...) whose quota, billing, and rate limits apply to tokens minted under this rule. Must be a workspace in the same organization, and the target service account must be a member of that workspace. |
token_lifetime_seconds | Integer between 60 and 86400 (1 minute to 24 hours). Default 3600. Values outside this range are rejected at request time. See Token lifetime and refresh. |
The issuer_url, discovery_base, and jwks_url fields are validated:
| Constraint | Detail |
|---|---|
| Scheme | Must be https. |
| Port | Must be 443 (explicit or default). |
| Host | Must be a public DNS host name for your OIDC provider. Must resolve to public IP addresses; IP literals are not accepted. |
URL validation failures return 400 invalid_request_error with the field name as a prefix on the error message (for example, issuer_url: url must use https scheme).
URL constraints apply only to URLs that Anthropic dials. In explicit_url and inline JWKS modes the issuer_url is compared against the JWT iss claim as a string and is never fetched, so it may reference an internal hostname or non-standard port.
| Constraint | Detail |
|---|---|
| Maximum size | The assertion JWT must be at most 16 KiB. |
| Signing algorithm | Only asymmetric algorithms (RSA and ECDSA families: ES256, ES384, RS256, RS384, PS256, PS384) are accepted. HMAC (HS256, HS384, HS512) and none are rejected. |
| Required claims | sub must be present. exp must be in the future. |
| Clock skew | A 30-second leeway is applied to exp, nbf, and iat. |
A federation rule's match block determines whether an incoming JWT is accepted. All populated fields are evaluated with AND semantics: the JWT must satisfy every populated matcher. At least one of subject_prefix, claims, or condition must be set; a match block that contains only audience (or no matchers at all) is rejected. This guards against rules that would accept every token from an issuer.
| Matcher | Type | Semantics |
|---|---|---|
subject_prefix | string | Exact match against the JWT sub claim. A trailing * makes it a prefix match (the sub value must begin with the characters before the *). Case-sensitive. |
audience | string | The JWT aud claim must contain this exact string. When aud is an array, any element matching exactly satisfies the check. |
claims | map<string, string> | Each key is a top-level claim name and each value is the required exact string value. For nested, numeric, boolean, or complex claims like lists and maps, use condition with a CEL expression instead. |
condition | string (CEL) | A CEL expression that must evaluate to true. |
The condition expression has access to a single variable:
| Variable | Type | Contents |
|---|---|---|
claims | map | The full decoded JWT claim set. Nested objects are accessible as nested maps. |
Example:
claims.sub.startsWith("repo:acme-corp/") && claims.ref in ["refs/heads/main", "refs/heads/release"]CEL conditions are security boundaries. An expression that evaluates to true for more inputs than intended grants broader access than intended. Prefer the static matchers when they express your constraint.
POST /v1/oauth/token returns errors in the standard API error shape. The SDK wraps exchange failures in a typed FederationExchangeError (or language equivalent) that exposes the HTTP status, the response body, and the request_id.
| Status | Error | Cause | Resolution |
|---|---|---|---|
| 400 | invalid_request | federation_rule_id is malformed or a required request field is missing. | Verify the fdrl_ ID and that the request body includes all required fields. |
| 400 | invalid_grant | The JWT iss claim does not equal the registered issuer_url exactly. | Compare byte-for-byte, including trailing slashes and scheme: jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .iss' <<< "$JWT". |
| 400 | invalid_grant | JWKS fetch failed, JWKS is stale, or the JWT was signed with a key not in the JWKS. | For inline mode, update the issuer with the rotated keys. For discovery and explicit_url, confirm the JWKS endpoint is reachable on port 443. |
| 400 | invalid_grant | The JWT exp claim is in the past (beyond the 30-second skew window). | Confirm your identity provider is projecting a fresh token and the SDK is re-reading the token file. |
| 400 | invalid_grant | The JWT was verified but its claims do not satisfy the rule's match block. | Decode the JWT and compare each claim against the rule. subject_prefix is case-sensitive. audience requires an exact element match. |
| 400 | invalid_grant | The federation_rule_id does not exist, is archived, or the JWT is not authorized for it (consolidated to prevent enumeration). | Confirm the rule ID in the Claude Console and that the rule has not been archived. |
All invalid_grant failures return HTTP 400; the specific cause is logged server-side only and not exposed in the response.
| Symptom | Cause | Resolution |
|---|---|---|
| SDK reports "no credentials" instead of exchanging | One of ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, or ANTHROPIC_IDENTITY_TOKEN[_FILE] is unset and no profile is active. | Set all four variables, or configure a profile. |
| SDK authenticates with an API key instead of federating | ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is set and wins precedence. | Unset the key or token variable. |
FileNotFoundError on first request | The path in ANTHROPIC_IDENTITY_TOKEN_FILE does not exist. The SDK opens the file lazily at exchange time. | Confirm the projected-token volume is mounted and the path matches. |
| Token exchange succeeds but a Claude API request returns 403 | The minted token's scope does not grant access to that endpoint. | Check the rule's oauth_scope against OAuth scopes. |
| Authentication fails with empty credential | A credential environment variable is exported but set to an empty string. Empty values still win their precedence slot. | Unset the variable with unset VAR rather than VAR="". |
A 400 invalid_grant response is intentionally opaque; the specific cause is logged server-side only.
Start with the authentication history page in the Claude Console. Recent exchange attempts surface the issuer and rule that were evaluated, the JWT claims that were inspected, and which validation step failed, which usually short-circuits the following checks.
If you still need to debug from the JWT itself, work through these checks in order:
Decode the JWT
Decode the assertion you sent so you can compare each claim against your issuer and rule configuration:
jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' <<< "$JWT"Check iss matches the issuer
The decoded iss claim must equal the registered issuer_url byte for byte, including scheme, port, and any trailing slash. A mismatch on a single character fails verification.
Check aud matches the rule
The decoded aud claim must contain the rule's audience value as an exact match. When aud is an array, one element must match exactly.
Check sub and each claims entry
Compare sub against the rule's subject_prefix (case-sensitive; a trailing * is a prefix match, anything else is exact). Compare every key in the rule's claims map against the same-named top-level claim.
Check exp, nbf, and iat
exp must be in the future and nbf/iat must be in the past, within the 30-second skew window. If the workload host's clock has drifted, an otherwise valid token is rejected.
Check JWKS reachability
For discovery mode, fetch <issuer_url>/.well-known/openid-configuration over public HTTPS on port 443 and confirm jwks_uri resolves. For explicit_url, fetch the JWKS URL directly. For inline, confirm the issuer's signing key has not rotated since you registered the keys.
When you register a federation issuer, the jwks_source field controls how Anthropic obtains the public keys used to verify JWT signatures from that issuer.
| Mode | Required fields | Behavior | Use when |
|---|---|---|---|
discovery (default) | issuer_url (optionally discovery_base) | Anthropic fetches <issuer_url>/.well-known/openid-configuration, reads jwks_uri from the discovery document, and fetches the JWKS from there. | Your IdP serves a standard OIDC discovery document on the public internet. Most managed providers (EKS, GKE, Cloud Run, GitHub Actions, Entra ID) support this. |
explicit_url | issuer_url, jwks_url | Anthropic fetches the JWKS directly from jwks_url. The issuer_url is used only for string comparison against the JWT iss claim and is never dialed. | Your IdP does not serve a discovery document, or discovery is internal-only but the JWKS is publicly reachable. |
inline | issuer_url, jwks_keys | You supply the array of JWK objects inline (the keys array from the JWKS document, not the wrapper object). Anthropic makes no outbound request. The issuer_url is used only for iss comparison. | Air-gapped environments, self-managed Kubernetes clusters with cluster-internal issuer URLs, or when you want explicit control over key rotation. |
The companion fields are mutually exclusive: setting jwks_url with jwks_source: "discovery" is rejected.
In inline mode there is no automatic key refresh. When your identity provider rotates its signing keys, you must update the issuer configuration with the new JWKS or all token exchanges will fail signature verification.
Was this page helpful?