from line 4
44description: Environment variables, validation rules, profile configuration, and error reference for Workload Identity Federation.
55---
66
7This page collects the configuration surfaces, validation constraints, and error mappings for [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). For setup walkthroughs, see the [provider guides](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#identity-providers).
7This page collects the configuration surfaces, validation constraints, and error mappings for [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) (WIF). For setup walkthroughs, see [Identity providers](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#identity-providers).
88
99## Token exchange request
1010
from line 17
1717| `federation_rule_id` | Yes | Tagged ID (`fdrl_...`) of the federation rule to evaluate. |
1818| `organization_id` | Yes | UUID of your Anthropic organization. |
1919| `service_account_id` | Yes | Tagged ID (`svac_...`) of the target service account. |
20| `workspace_id` | Conditional | Tagged ID (`wrkspc_...`) of the workspace to scope the minted token to. Required when the rule is enabled for more than one workspace. When omitted, the server selects the rule’s sole enabled workspace. The literal `default` also works for the organization’s Default Workspace but is deprecated; use that workspace’s `wrkspc_...` ID. |
20| `workspace_id` | Conditional | Tagged ID (`wrkspc_...`) of the workspace to scope the minted token to. Required when the rule is enabled for more than one workspace. When omitted, the server selects the rule’s sole enabled workspace. The literal `default` also works for the organization’s Default Workspace but is deprecated. Use that workspace’s `wrkspc_...` ID. |
2121
2222## Token exchange response
2323
from line 34
3434
3535The SDK reads these variables to perform a federated token exchange with no constructor arguments.
3636
37| Variable | Required | Description | Example |
38| ------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
39| `ANTHROPIC_FEDERATION_RULE_ID` | Yes | Tagged ID of the federation rule to evaluate. | `fdrl_...` |
40| `ANTHROPIC_ORGANIZATION_ID` | Yes | UUID of your Anthropic organization. Find it on the [Organization](https://platform.claude.com/settings/organization) page (`platform.claude.com/settings/organization`) in the Claude Console. | `00000000-0000-0000-0000-000000000000` |
41| `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 that rotate on disk are always current. | `/var/run/secrets/anthropic.com/token` |
42| `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...` |
43| `ANTHROPIC_SERVICE_ACCOUNT_ID` | Yes | Tagged ID of the target Anthropic service account that the issued access token acts as. | `svac_...` |
44| `ANTHROPIC_WORKSPACE_ID` | Conditional | Tagged ID of the workspace to scope the minted token to. Required when the federation rule is enabled for more than one workspace; optional when the rule is bound to a single workspace. The minted token is scoped to this workspace at exchange time, so switching workspaces requires a new exchange. Without a profile, the `ant` CLI does not pass this variable into the exchange, so set `workspace_id` in a federation profile and set `ANTHROPIC_PROFILE` to its name. The literal `default` also works for the Default Workspace but is deprecated; use its `wrkspc_...` ID. | `wrkspc_...` |
45| `ANTHROPIC_PROFILE` | No | Name of a [configuration profile](https://platform.claude.com/docs/en/manage-claude/wif-reference#profile-configuration-file) to load. Takes precedence over the federation environment variables in this table. | `staging-profile` |
37| Variable | Required | Description | Example |
38| ------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
39| `ANTHROPIC_FEDERATION_RULE_ID` | Yes | Tagged ID of the federation rule to evaluate. | `fdrl_...` |
40| `ANTHROPIC_ORGANIZATION_ID` | Yes | UUID of your Anthropic organization. Find it on the [Organization](https://platform.claude.com/settings/organization) page (`platform.claude.com/settings/organization`) in the Claude Console. | `00000000-0000-0000-0000-000000000000` |
41| `ANTHROPIC_IDENTITY_TOKEN_FILE` | One of `_TOKEN_FILE` or `_TOKEN` | Filesystem path to the JWT issued by your identity provider (IdP). The SDK rereads this file on every exchange so that projected tokens that rotate on disk are always current. | `/var/run/secrets/anthropic.com/token` |
42| `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...` |
43| `ANTHROPIC_SERVICE_ACCOUNT_ID` | Yes | Tagged ID of the target Anthropic service account that the issued access token acts as. | `svac_...` |
44| `ANTHROPIC_WORKSPACE_ID` | Conditional | Tagged ID of the workspace to scope the minted token to. Required when the federation rule is enabled for more than one workspace. Optional when the rule is bound to a single workspace. The exchange scopes the minted token to this workspace, so switching workspaces requires a new exchange. Without a profile, the `ant` CLI does not pass this variable into the exchange. With the `ant` CLI, set `workspace_id` in a federation profile and set `ANTHROPIC_PROFILE` to its name. The literal `default` also works for the Default Workspace but is deprecated. Use its `wrkspc_...` ID. | `wrkspc_...` |
45| `ANTHROPIC_PROFILE` | No | Name of a [configuration profile](https://platform.claude.com/docs/en/manage-claude/wif-reference#profile-configuration-file) to load. Takes precedence over the federation environment variables in this table. | `staging-profile` |
4646
47The 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. `ANTHROPIC_WORKSPACE_ID` is read alongside but does not gate activation.
47The 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. The SDK also reads `ANTHROPIC_WORKSPACE_ID`, but this path activates with or without it.
4848
4949<Warning>
50 A variable that is set to an empty string can still occupy its slot in the credential precedence chain. Unset unused credential variables rather than blanking them.
50 A variable that is set to an empty string can occupy its slot in the credential precedence chain. Unset unused credential variables rather than blanking them.
5151</Warning>
5252
5353### Credential precedence
from line 54
5454
5555The SDK resolves credentials in this order. The first source that yields a credential wins.
5656
57| Order | Source | Notes |
58| ----- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
59| 1 | Constructor argument (`api_key=`, `auth_token=`, `credentials=`) | Always overrides everything else. |
60| 2 | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` | Shadows federation entirely. Unset these when migrating from API keys. |
61| 3 | `ANTHROPIC_PROFILE` | Loads `<config_dir>/configs/<name>.json`. A missing named profile is an error, not a fall-through. |
62| 4 | Federation environment variables | `ANTHROPIC_FEDERATION_RULE_ID` + `ANTHROPIC_ORGANIZATION_ID` + `ANTHROPIC_SERVICE_ACCOUNT_ID` + `ANTHROPIC_IDENTITY_TOKEN[_FILE]`. |
63| 5 | Active profile | Resolved from `<config_dir>/active_config`, falling back to a profile named `default`. |
57| Order | Source | Notes |
58| ----- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
59| 1 | Constructor argument (API key, auth token, or credentials) | Always overrides everything else. |
60| 2 | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` | Overrides federation entirely. Unset these when migrating from API keys. |
61| 3 | `ANTHROPIC_PROFILE` | Loads `<config_dir>/configs/<name>.json`. If the named profile is missing, the SDK reports an error instead of trying the next source. |
62| 4 | Federation environment variables | `ANTHROPIC_FEDERATION_RULE_ID` + `ANTHROPIC_ORGANIZATION_ID` + `ANTHROPIC_SERVICE_ACCOUNT_ID` + `ANTHROPIC_IDENTITY_TOKEN[_FILE]`. |
63| 5 | Active profile | Resolved from `<config_dir>/active_config`, falling back to a profile named `default`. |
6464
6565In the `ant` CLI, the settings for [Claude Models on Google Cloud (Anthropic managed)](https://platform.claude.com/docs/en/build-with-claude/claude-models-on-google-cloud/anthropic-managed#authentication) rank after the federation environment variables and before the active profile. The settings are the project, the workspace ID, and the location. You set them with the `--google-cloud-*` flags or the `ANTHROPIC_GOOGLE_CLOUD_*` variables. The CLI uses these settings when any one of them is set. With these settings, the CLI gets its credential from Google Application Default Credentials.
6666
from line 90
9090
9191### File layout
9292
93| Path | Contents | Sensitivity |
94| ----------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------- |
95| `<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. |
96| `<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`. |
93| Path | Contents | Sensitivity |
94| ----------------------------------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------- |
95| `<config_dir>/configs/<profile>.json` | `version`, the `authentication` block, `organization_id`, `workspace_id`, and `base_url`. | Non-secret. Safe to commit or build into an image. |
96| `<config_dir>/credentials/<profile>.json` | `version`, the cached `access_token`, `expires_at`, and (for interactive login) `refresh_token`. | Secret. The SDK writes this file with mode `0600`. |
9797
9898Both the config file and the credentials file carry a top-level string `version` field in `major.minor` format, such as `"1.0"`. The SDK writes this field automatically. If you omit it when authoring a config by hand, the SDK treats the file as the current version.
9999
from line 117
117117}
118118```
119119
120If `authentication.identity_token` is omitted, the SDK falls back to `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` from the environment.
120If you omit `authentication.identity_token`, the SDK falls back to `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` from the environment.
121121
122122## OAuth scopes
123123
from line 128
128128| `workspace:developer` | All non-administrative Claude API endpoints in the rule’s workspace: [Messages](https://platform.claude.com/docs/en/api/messages) (including streaming and token counting), [Models](https://platform.claude.com/docs/en/api/models/list), [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) and their sessions, [Files](https://platform.claude.com/docs/en/build-with-claude/files), and [Skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide). This matches the access a workspace API key in the same workspace has. |
129129| `workspace:inference` | The inference endpoints in the rule’s workspace: [Messages](https://platform.claude.com/docs/en/api/messages) (including streaming and token counting), [Models](https://platform.claude.com/docs/en/api/models/list), and the [OpenAI-compatible chat endpoint](https://platform.claude.com/docs/en/cli-sdks-libraries/libraries/openai-sdk). Use this for workloads that only need to call Claude and never need to manage Files, Skills, or other resources. |
130130| `workspace:manage_tunnels` | The [MCP tunnels API](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#tunnels-api): create, list, and get tunnels, register and archive CA certificates, reveal and rotate the tunnel token, and archive tunnels. The Console’s create-tunnel modal window locks this scope when you create a rule from it. |
131| `org:admin` | Full access to the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) (organization members, invites, workspaces, API keys, and the rest). An OAuth `org:admin` token can only create or modify rules scoped to `workspace:developer` or `workspace:inference`, and cannot update an issuer that backs a rule with any other scope; see [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). |
131| `org:admin` | Full access to the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) (organization members, invites, workspaces, API keys, and the rest). An OAuth `org:admin` token can only create or modify rules scoped to `workspace:developer` or `workspace:inference`. It cannot update an issuer that backs a rule with any other scope. See [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). |
132132
133133A request to an endpoint outside the token’s scope returns HTTP 403. Finer-grained scopes (by resource, or read versus write) are not available.
134134
135135### Permission boundaries
136136
137A federation rule’s `oauth_scope` is a ceiling: the minted token can never exceed it. The target service account’s `organization_role` (`developer` or `admin`) determines which scopes are grantable, so a rule that grants `org:admin` must target a service account with `organization_role=admin`. Effective permissions are the intersection of the rule’s scope and the service account’s role.
137A federation rule’s `oauth_scope` is a ceiling: the minted token can never exceed it. The target service account’s `organization_role` (`developer` or `admin`) determines which scopes a rule can grant. A rule that grants `org:admin` must target a service account with `organization_role=admin`. Effective permissions are the intersection of the rule’s scope and the service account’s role.
138138
139139| Rule `oauth_scope` | Service account `organization_role` | Effective permissions |
140140| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141141| `workspace:developer` | `admin` | Claude API access in the rule’s workspace only. The scope caps the token below the role. |
142| `org:admin` | `admin` | Full Admin API access (organization members, invites, workspaces, API keys, and the rest), minus the OAuth-caller exceptions; see [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). |
142| `org:admin` | `admin` | Full Admin API access (organization members, invites, workspaces, API keys, and the rest), minus the OAuth-caller exceptions. See [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). |
143143
144144## Validation rules
145145
146Anthropic enforces these constraints when you create or update issuers and rules, and when verifying an incoming JWT at exchange time.
146Anthropic enforces these constraints when you create or update issuers and rules, and when it verifies an incoming JWT at exchange time.
147147
148148For complete parameter details and response schemas, see the [Service accounts API reference](https://platform.claude.com/docs/en/api/organization/service_accounts), [Federation issuers API reference](https://platform.claude.com/docs/en/api/organization/federation/issuers), and [Federation rules API reference](https://platform.claude.com/docs/en/api/organization/federation/rules).
149149
from line 153
153153| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
154154| Issuer, rule, and service account `name` | Must match `^[a-z0-9-]+$`, length 1 to 255 characters. |
155155| `workspace_id` | Required on create unless `applies_to_all_workspaces` is true. 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. |
156| `applies_to_all_workspaces` | Boolean. Set `true` to enable the rule in every workspace in the organization instead of naming one; either this or `workspace_id` is required on create. |
157| `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](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#token-lifetime-and-refresh). |
156| `applies_to_all_workspaces` | Boolean. Set `true` to enable the rule in every workspace in the organization instead of naming one. Either this or `workspace_id` is required on create. |
157| `token_lifetime_seconds` | Integer between `60` and `86400` (1 minute to 24 hours). Default `3600`. Anthropic rejects values outside this range at request time. See [Token lifetime and refresh](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#token-lifetime-and-refresh). |
158158
159159### URL fields
160160
161The `issuer_url`, `jwks.discovery_base`, and `jwks.url` fields are validated:
161Anthropic validates the `issuer_url`, `jwks.discovery_base`, and `jwks.url` fields:
162162
163| Constraint | Detail |
164| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
165| Scheme | Must be `https`. |
166| Port | Must be `443` (explicit or default). |
167| Host | Must be a public DNS hostname for your OIDC provider. Must resolve to public IP addresses; IP literals are not accepted. |
163| Constraint | Detail |
164| ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
165| Scheme | Must be `https`. |
166| Port | Must be `443` (explicit or default). |
167| Host | Must be a public DNS hostname for your OIDC provider. Must resolve to public IP addresses. Anthropic does not accept IP literals. |
168168
169169URL 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`).
170170
171171<Note>
172 URL constraints apply only to URLs that Anthropic dials. In `explicit_url` and `inline` JWKS modes, and in `discovery` mode when `jwks.discovery_base` is set, 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.
172 URL constraints apply only to URLs that Anthropic connects to. Anthropic never fetches the `issuer_url` in `explicit_url` and `inline` JSON Web Key Set (JWKS) modes, or in `discovery` mode when `jwks.discovery_base` is set. It only compares that URL against the JWT `iss` claim as a string, so the URL may reference an internal hostname or non-standard port.
173173</Note>
174174
175175### JWT verification
176176
177| Constraint | Detail |
178| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179| Maximum size | The `assertion` JWT must be at most 16 KiB. |
180| Signing algorithm | Only asymmetric algorithms (RSA and ECDSA families: ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512) are accepted. HMAC (`HS256`, `HS384`, `HS512`) and `none` are rejected. |
181| Key ID | The JWT header must carry a `kid` that matches a key in the issuer’s JWKS. Tokens without `kid` are rejected. |
182| Required claims | `sub` must be present. `iat` must be present and not in the future. `exp` must be present and in the future. |
183| Single use | An assertion that carries a `jti` claim can be exchanged only once per issuer: repeating an exchange with the same `jti` is rejected as a replay. The issuer’s `check_jti` field (enabled by default) controls this check; assertions without a `jti` claim are not subject to it. See the [Federation issuers API reference](https://platform.claude.com/docs/en/api/organization/federation/issuers). |
184| Maximum lifetime | The token’s lifetime (`exp` minus `iat`) must not exceed the issuer’s configured maximum (1 hour by default, configurable for each issuer in the Claude Console). |
185| Clock skew | A 30-second leeway is applied to `exp`, `nbf`, and `iat`. |
177| Constraint | Detail |
178| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179| Maximum size | The `assertion` JWT must be at most 16 KiB. |
180| Signing algorithm | Anthropic accepts only asymmetric algorithms (RSA and ECDSA families: ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512). It rejects HMAC (`HS256`, `HS384`, `HS512`) and `none`. |
181| Key ID | The JWT header must carry a `kid` that matches a key in the issuer’s JWKS. Anthropic rejects tokens without `kid`. |
182| Required claims | `sub` must be present. `iat` must be present and not in the future. `exp` must be present and in the future. |
183| Single use | An assertion that carries a `jti` claim can be exchanged only once for each issuer. Anthropic rejects a repeat exchange with the same `jti` as a replay. The issuer’s `check_jti` field (enabled by default) controls this check. Assertions without a `jti` claim are not subject to it. See the [Federation issuers API reference](https://platform.claude.com/docs/en/api/organization/federation/issuers). |
184| Maximum lifetime | The token’s lifetime (`exp` minus `iat`) must not exceed the issuer’s configured maximum (1 hour by default, configurable for each issuer in the Claude Console). |
185| Clock skew | Anthropic applies a 30-second leeway to `exp`, `nbf`, and `iat`. |
186186
187187## Rule matching semantics
188188
189A 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.
189A federation rule’s `match` block determines whether Anthropic accepts an incoming JWT. The JWT must satisfy every populated matcher. Set at least one of `subject_prefix`, `claims`, or `condition`. Anthropic rejects a `match` block that contains only `audience` or no matchers at all. This guards against rules that would accept every token from an issuer.
190190
191| Matcher | Type | Semantics |
192| ---------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
193| `subject_prefix` | string | Exact match against the JWT `sub` claim. A trailing `*` makes it a prefix match (the `sub` value must start with the characters before the `*`). Case-sensitive. |
194| `audience` | string | The JWT `aud` claim must contain this exact string. When `aud` is an array, any element matching exactly satisfies the check. |
195| `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 such as lists and maps, use `condition` with a CEL expression instead. |
196| `condition` | string (CEL) | A [CEL](https://cel.dev/) expression that must evaluate to `true`. |
191| Matcher | Type | Semantics |
192| ---------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
193| `subject_prefix` | string | Exact match against the JWT `sub` claim. A trailing `*` makes it a prefix match (the `sub` value must start with the characters before the `*`). Case-sensitive. |
194| `audience` | string | The JWT `aud` claim must contain this exact string. When `aud` is an array, any element matching exactly satisfies the check. |
195| `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 such as lists and maps, use `condition` with a Common Expression Language (CEL) expression instead. |
196| `condition` | string (CEL) | A [CEL](https://cel.dev/) expression that must evaluate to `true`. |
197197
198198### CEL evaluation environment
199199
from line 217
217217
218218### Token exchange errors
219219
220`POST /v1/oauth/token` returns errors in the standard [API error shape](https://platform.claude.com/docs/en/api/errors). The SDK reports an exchange failure as `WorkloadIdentityError` (ruby: `Anthropic::Credentials::WorkloadIdentityError`; go: `error`; java: `UnexpectedStatusCodeException`; csharp: `WorkloadIdentityException`; php: `OAuthException`), which gives the HTTP status and the error details from the response body. The request ID is on the error in Python, TypeScript, and Ruby, and in the exception’s response headers in Java. In Go it is only in the error’s message, because the error’s type is in an internal package; the C# and PHP errors do not carry it.
220`POST /v1/oauth/token` returns errors in the standard [API error shape](https://platform.claude.com/docs/en/api/errors). The SDK reports an exchange failure as `WorkloadIdentityError` (ruby: `Anthropic::Credentials::WorkloadIdentityError`; go: `error`; java: `UnexpectedStatusCodeException`; csharp: `WorkloadIdentityException`; php: `OAuthException`), which gives the HTTP status and the error details from the response body. Except in C# and PHP, the SDK also gives the request ID: on the error, in the exception’s response headers, or in the error’s message.
221221
222222| Status | Error | Cause | Resolution |
223223| ------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
224| 400 | `invalid_request_error` | `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. |
225| 400 | `invalid_request_error` | `workspace_id` is present but is not a well-formed `wrkspc_...` ID or the literal `default`. | Fix the `workspace_id` value; the response message names the expected format. |
224| 400 | `invalid_request_error` | `federation_rule_id` is malformed or a required request field is missing. | Check the `fdrl_` ID and that the request body includes all required fields. |
225| 400 | `invalid_request_error` | `workspace_id` is present but is not a well-formed `wrkspc_...` ID or the literal `default`. | Fix the `workspace_id` value. The response message names the expected format. |
226226| 401 | `authentication_error` | 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"`. |
227| 401 | `authentication_error` | 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; if the issuer recently rotated its signing key, see [Key rotation and caching](https://platform.claude.com/docs/en/manage-claude/wif-reference#key-rotation-and-caching). |
228| 401 | `authentication_error` | 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. |
227| 401 | `authentication_error` | 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. If the issuer recently rotated its signing key, see [Key rotation and caching](https://platform.claude.com/docs/en/manage-claude/wif-reference#key-rotation-and-caching). |
228| 401 | `authentication_error` | 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 rereading the token file. |
229229| 401 | `authentication_error` | 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. |
230| 401 | `authentication_error` | The `federation_rule_id` does not exist or 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. |
230| 401 | `authentication_error` | The `federation_rule_id` does not exist or is archived, or the JWT is not authorized for it (one error covers these cases to prevent enumeration). | Confirm the rule ID in the Claude Console and that the rule has not been archived. |
231231| 401 | `authentication_error` | The federation rule is enabled for more than one workspace and the request omits `workspace_id`. The authentication history entry shows reason `workspace_id_required`. | Set `ANTHROPIC_WORKSPACE_ID` (or the `workspace_id` body field on a raw request) to the `wrkspc_...` ID you want the token scoped to. With the `ant` CLI, set `workspace_id` in a federation profile and set `ANTHROPIC_PROFILE` to its name. See [Token exchange request](https://platform.claude.com/docs/en/manage-claude/wif-reference#token-exchange-request). |
232232
233Every assertion denial returns the same opaque `401` `authentication_error` with the fixed message `Authentication failed`, regardless of which check failed; a distinguishable error would let a caller probe rule configuration. The deny reason is recorded on the attempt’s entry in the [authentication history](https://platform.claude.com/settings/workload-identity-federation?tab=history) in the Claude Console, for example `match_subject_prefix` when the `sub` claim fails the rule’s `subject_prefix`, or `workspace_id_required` when the rule spans multiple workspaces and the request names none. Requests rejected before the rule’s organization is corroborated (the `400 invalid_request_error` family in the previous table) leave no history entry. Their response messages name the problem directly. A `401` with no matching history entry usually means the `federation_rule_id` itself was not recognized.
233Every assertion denial returns the same opaque `401` `authentication_error` with the fixed message `Authentication failed`, regardless of which check failed. A distinguishable error would let a caller probe rule configuration. The [authentication history](https://platform.claude.com/settings/workload-identity-federation?tab=history) in the Claude Console records the deny reason on the attempt’s entry. For example, it shows `match_subject_prefix` when the `sub` claim fails the rule’s `subject_prefix`, or `workspace_id_required` when the rule spans multiple workspaces and the request names none. Requests rejected before the rule’s organization is confirmed (the `400 invalid_request_error` family in the previous table) leave no history entry. Their response messages name the problem directly. A `401` with no matching history entry usually means that Anthropic did not recognize the `federation_rule_id`.
234234
235235### Common SDK-side failures
236236
237237| Symptom | Cause | Resolution |
238238| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
239| 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. |
239| SDK reports missing 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. |
240240| 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. |
241241| File-not-found error 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. |
242242| 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](https://platform.claude.com/docs/en/manage-claude/wif-reference#oauth-scopes). |
243| Authentication fails with empty credential | A credential environment variable is exported but set to an empty string. Empty values can still win their precedence slot. | Unset the variable with `unset VAR` rather than `VAR=""`. |
243| Authentication fails with empty credential | A credential environment variable is exported but set to an empty string. Empty values can win their precedence slot. | Unset the variable with `unset VAR` rather than `VAR=""`. |
244244
245245## Troubleshoot a failed exchange
246246
247A `401` `authentication_error` response is intentionally opaque and its message is always `Authentication failed`; the deny reason is recorded in the authentication history, not in the response.
247A `401` `authentication_error` response is intentionally opaque, and its message is always `Authentication failed`. The deny reason appears in the authentication history, not in the response.
248248
249249<Tip>
250 Start with the [authentication history page](https://platform.claude.com/settings/workload-identity-federation?tab=history) 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.
250 Start with the [authentication history page](https://platform.claude.com/settings/workload-identity-federation?tab=history) in the Claude Console. Recent exchange attempts show the issuer and rule that Anthropic evaluated, the JWT claims that it inspected, and which validation step failed. This information usually makes the following checks unnecessary.
251251</Tip>
252252
253One common opaque failure is a replayed assertion: an assertion that carries a `jti` claim can be [exchanged only once](https://platform.claude.com/docs/en/manage-claude/wif-reference#jwt-verification), so a workload that re-sends the same JWT (a retry loop, or a refresh that re-reads an unrotated token) is rejected on the second exchange. The authentication history page shows these attempts with the reason `jti_reused`; the fix is to mint a fresh assertion for each exchange.
253One common opaque failure is a replayed assertion. An assertion that carries a `jti` claim can be [exchanged only once](https://platform.claude.com/docs/en/manage-claude/wif-reference#jwt-verification). A workload that resends the same JWT (a retry loop, or a refresh that rereads an unrotated token) fails on the second exchange. The authentication history page shows these attempts with the reason `jti_reused`. To fix this, mint a fresh assertion for each exchange.
254254
255If you still need to debug from the JWT itself, work through these checks in order:
255If you need to debug from the JWT itself, work through these checks in order:
256256
257257<Steps>
258258 <Step title="Decode the JWT">
from line 272
272272 </Step>
273273
274274 <Step title="Check sub and each claims entry">
275 Compare `sub` against the rule’s `subject_prefix` (case-sensitive; a trailing `*` is a prefix match, and anything else is exact). Compare every key in the rule’s `claims` map against the same-named top-level claim.
275 Compare `sub` against the rule’s `subject_prefix`. The comparison is case-sensitive. A trailing `*` makes it a prefix match, and anything else is exact. Compare every key in the rule’s `claims` map against the same-named top-level claim.
276276 </Step>
277277
278278 <Step title="Check exp, nbf, and iat">
279 `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.
279 `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, Anthropic rejects an otherwise valid token.
280280 </Step>
281281
282282 <Step title="Check JWKS reachability">
from line 288
288288
289289## JWKS source modes
290290
291When you register a federation issuer, the `jwks` field controls how Anthropic obtains the public keys used to verify JWT signatures from that issuer. It is a discriminated union keyed on `type`:
291When you register a federation issuer, the `jwks` field controls how Anthropic obtains the public keys used to verify JWT signatures from that issuer. Its `type` field selects the source mode:
292292
293| `jwks.type` | `jwks` shape | Behavior | Use when |
294| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
295| `discovery` (default) | `{ "type": "discovery", "discovery_base": "https://..." }` (`discovery_base` is optional; set it when the discovery URL differs from `issuer_url`) | Anthropic fetches `<discovery_base or 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. |
296| `explicit_url` | `{ "type": "explicit_url", "url": "https://..." }` | Anthropic fetches the JWKS directly from `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. |
297| `inline` | `{ "type": "inline", "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. |
293| `jwks.type` | `jwks` shape | Behavior | Use when |
294| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
295| `discovery` (default) | `{ "type": "discovery", "discovery_base": "https://..." }` (set the optional `discovery_base` when the discovery URL differs from `issuer_url`) | Anthropic fetches `<discovery_base or 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. |
296| `explicit_url` | `{ "type": "explicit_url", "url": "https://..." }` | Anthropic fetches the JWKS directly from `url`. Anthropic uses the `issuer_url` only for string comparison against the JWT `iss` claim and never connects to it. | Your IdP does not serve a discovery document, or discovery is internal-only but the JWKS is publicly reachable. |
297| `inline` | `{ "type": "inline", "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. Anthropic uses the `issuer_url` 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. |
298298
299The discriminated union makes the companion fields mutually exclusive by construction. Both `discovery` and `explicit_url` also accept an optional `ca_cert_pem` string for issuers that serve TLS from a private CA.
299Each `type` takes its own companion fields, so those fields are mutually exclusive. Both `discovery` and `explicit_url` also accept an optional `ca_cert_pem` string for issuers that serve TLS from a private CA.
300300
301301### Key rotation and caching
302302
303In `discovery` and `explicit_url` modes, Anthropic caches the fetched JWKS. If your identity provider publishes a new signing key and immediately starts signing tokens with it, exchanges that present those tokens might fail with a signature error for up to 1 minute while the cache refreshes.
303In `discovery` and `explicit_url` modes, Anthropic caches the fetched JWKS. After your identity provider publishes a new signing key, exchanges of tokens signed with it might fail with a signature error for up to 1 minute while the cache refreshes.
304304
305To avoid this window, publish a new signing key in the JWKS at least 15 minutes before your identity provider starts signing tokens with it, and keep the superseded key in the JWKS until tokens it signed have expired. Managed identity providers typically follow this discipline on their own. If you operate your own issuer (a self-managed Kubernetes cluster, a SPIRE OIDC discovery provider, or an Okta custom authorization server with a configured rotation cadence), confirm that your rotation policy publishes new keys ahead of first use.
305To avoid this window, publish a new signing key in the JWKS at least 15 minutes before your identity provider starts signing tokens with it. Keep the replaced key in the JWKS until the tokens it signed have expired. Managed identity providers typically do this on their own. If you operate your own issuer, confirm that your rotation policy publishes new keys ahead of first use. Examples are a self-managed Kubernetes cluster, a SPIRE OIDC discovery provider, and an Okta custom authorization server with a configured rotation schedule.
306306
307307<Warning>
308 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.
308 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 fail signature verification.
309309</Warning>
310310