Claude Access — a Service's Own Anthropic Identity

A service that calls Claude does not need an Anthropic API key. Switch Claude access on for its OAuth client and the builder gives that service its own identity at Anthropic. The service then proves who it is with a short-lived token from Bio-ID, and Anthropic hands back a short-lived access token for the Messages API. Nothing long-lived is stored anywhere.

The Short Version

  1. A platform admin switches Claude access on for the service's OAuth client (Console → OAuth → your client).
  2. tawa deploy --prod. The pod now has four ANTHROPIC_* ids.
  3. At runtime the service asks Bio-ID for an assertion, exchanges it at Anthropic, and calls Claude with the token it gets back.

Step 3 is done for you by @insureco/bio/claude (@insureco/bio 0.15.0+) — see Using it from a service.

What the switch does

claudeAccessEnabled is a per-OAuth-client flag in Bio-ID. A client with it on may ask Bio-ID for an assertion: a 5-minute JWT whose audience is https://api.anthropic.com and whose sub is the client id. A client with it off is refused.

Anthropic trusts Bio-ID as an identity provider (Workload Identity Federation). When the flag goes on, the builder creates two things at Anthropic for that client, and only that client:

Service accountThe identity the service acts as. Organization role developer.
Federation ruleAccepts an assertion only when sub is this client's id and the token came from the client_credentials grant. Grants workspace:inference in the platform workspace.

workspace:inference covers the Messages API (including streaming and token counting), the Models API, and the OpenAI-compatible chat endpoint. It does not cover Files, Skills, Managed Agents or anything administrative.

Usage is billed to the platform's Anthropic workspace. That is why switching it on is restricted.

Who can switch it

ActionWho
Switch onPlatform admin only
Switch offPlatform admin, or the org that owns the client

The flag is per client, and a service has one client per environment, so prod and sandbox are switched separately.

Switching on can fail, and when it does the switch goes back to off

The console never shows Claude access as on for a service that cannot actually use it. If the builder cannot create the Anthropic identity, it sets the flag back to off and returns the reason:

ResponseMeans
502 CLAUDE_FEDERATION_FAILEDAnthropic refused or could not be reached. The message says which call failed.
503 CLAUDE_FEDERATION_NOT_CONFIGUREDThis builder has no Anthropic federation configured. A platform operator matter.

error.details.flagReverted is true when the flag was set back. If it is false, Bio-ID refused the revert and the flag is still on: switch it off by hand.

Switching off

Bio-ID stops issuing assertions for the client at once. That is the control that matters. The builder then archives the federation rule and the service account at Anthropic. If that cleanup fails, the flag still stays off and the response is a 200 carrying a warning; switching it off again retries the cleanup.

An Anthropic access token the service already holds keeps working until it expires, which is a matter of minutes.

What a deploy injects

On every deploy, after OAuth provisioning, the builder checks the client's flag. When it is on, the builder makes sure the service account and rule still exist (re-creating either if it is missing) and injects:

VariableExampleWhat it is
ANTHROPIC_FEDERATION_RULE_IDfdrl_…This service's federation rule
ANTHROPIC_ORGANIZATION_IDa UUIDThe platform's Anthropic organization
ANTHROPIC_SERVICE_ACCOUNT_IDsvac_…This service's service account
ANTHROPIC_WORKSPACE_IDwrkspc_…The workspace usage is billed to
  • They are identifiers, not credentials, and are injected as plain env vars. What authenticates the service is its own Bio-ID assertion.
  • They are reserved: tawa config set refuses all four (RESERVED_ENV_VAR).
  • The names are the ones the Anthropic SDKs read for workload identity federation.
  • A deploy is what puts them in the pod. Switching Claude access on does not change a running pod, and switching it off does not remove them until the next deploy.
  • If anything goes wrong the deploy continues and the build log gets a [warn] line; the variables are simply not injected that time.

The service must have an OAuth client, which means spec.auth in catalog-info.yaml (mode: sso or mode: service-only). Without one there is no BIO_CLIENT_ID to ask Bio-ID with. See Authentication.

Using it from a service

Use the helper. It makes both calls, caches the Claude token until just before it expires and refreshes it itself, so a client you build once keeps working. Server-side only.

npm install @insureco/bio@^0.15.0
import Anthropic from '@anthropic-ai/sdk'
import { claudeAuthFromEnv } from '@insureco/bio/claude'

const auth = claudeAuthFromEnv(process.env, { logger })
// auth.mode: 'federation' | 'api-key' | 'unconfigured'

// The SDK insists on an apiKey; this placeholder never leaves the process.
const client = new Anthropic({ apiKey: 'federated', fetch: auth.fetch })

Not using the SDK? auth.fetch is a drop-in fetch, and await auth.headers() gives the auth header for axios or anything else. CommonJS works too: const { claudeAuthFromEnv } = require('@insureco/bio/claude').

auth.modeWhenWhat auth.fetch sends
federationthe three injected ids and BIO_ID_URL / BIO_CLIENT_ID / BIO_CLIENT_SECRET are all setAuthorization: Bearer <short-lived token>; removes x-api-key and anthropic-workspace-id
api-keyfederation is not configured and ANTHROPIC_API_KEY is setx-api-key, as before
unconfiguredneithernothing: a Claude request throws, naming the missing settings
  • Federation wins when both are configured, so a leftover key cannot shadow it.
  • auth.problems lists the missing federation settings by name. Log auth.mode once at boot; after a deploy that line tells you which credential the pod is really using.
  • Moving an existing service off a key? Pass { fallbackToApiKey: true } for the rollout. If an exchange fails and the key is still set, that request uses the key and one warning is logged. Remove the flag, then the key, once the boot line says federation in production.
  • The helper only touches requests to https://api.anthropic.com. A custom ANTHROPIC_BASE_URL bypasses it.
  • The service needs spec.auth (sso or service-only) so it has a Bio-ID client at all.
  • No .npmrc yet? Add @insureco:registry=https://git.insureco.io/api/packages/insureco/npm/.

What the helper does (and how to do it without it)

Two calls.

1. Ask Bio-ID for an assertion.

POST {BIO_ID_URL}/api/oauth/token
Content-Type: application/json

{ "grant_type": "client_credentials",
  "client_id": "<BIO_CLIENT_ID>",
  "client_secret": "<BIO_CLIENT_SECRET>",
  "audience": "https://api.anthropic.com" }

The access_token in the reply is the assertion. It lives five minutes.

2. Exchange it at Anthropic.

POST https://api.anthropic.com/v1/oauth/token
Content-Type: application/json
anthropic-version: 2023-06-01

{ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
  "assertion": "<the assertion from step 1>",
  "federation_rule_id": "<ANTHROPIC_FEDERATION_RULE_ID>",
  "organization_id": "<ANTHROPIC_ORGANIZATION_ID>",
  "service_account_id": "<ANTHROPIC_SERVICE_ACCOUNT_ID>",
  "workspace_id": "<ANTHROPIC_WORKSPACE_ID>" }

The reply is { access_token, token_type, expires_in, scope }. Send access_token to the Claude API as Authorization: Bearer <token>, or pass it to the SDK as its auth token (authToken in TypeScript, auth_token in Python).

Every exchange needs a fresh assertion

An assertion carries a jti and Anthropic accepts it once. A second exchange with the same assertion is rejected with a bare 401 Authentication failed, whatever is left of its five minutes.

So:

  • Cache the Anthropic access token, until shortly before expires_in runs out.
  • Never cache or reuse the assertion. Ask Bio-ID for a new one each time you exchange.
  • A retry of a failed exchange needs a new assertion too.

Expect expires_in to be about ten minutes, not an hour. The rule allows an hour, but Anthropic caps an access token at twice the remaining life of the assertion that bought it.

Why not the Anthropic SDK's own federation support?

The SDKs can perform the exchange themselves from the injected variables, but they need an identity token — the assertion — supplied through ANTHROPIC_IDENTITY_TOKEN_FILE or ANTHROPIC_IDENTITY_TOKEN, and a fresh one for every exchange. Nothing writes an assertion into the pod, and a static value cannot work: it expires in five minutes and is accepted once. The helper is what supplies a fresh one, and it works with every SDK version and with raw HTTP.

Two things that bite if you try the SDK's automatic path anyway:

  • ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN in the environment takes precedence over federation in the SDK. (Through the helper, federation wins.)
  • Setting ANTHROPIC_IDENTITY_TOKEN to an assertion you fetched at startup works exactly once.

What the builder creates, precisely

For operators and for anyone reading the Anthropic Console.

Both resources carry the same name, derived from the client id:

Client idName at Anthropic
riskatlas-prodriskatlas-prod
client_f7b6eca7fe3cdefca6709838c34f1ececlient-f7b6eca7fe3cdefca6709838c34f1ece-x3c30418180e1

A client id that is already lowercase letters, digits and hyphens is used as it is. Anything else is lower-cased, has every other character replaced with -, and gets -x plus the first 12 hex characters of sha256(client id) appended, so two client ids can never share a name.

The rule:

{ "name": "<name>",
  "issuer_id": "<the Bio-ID issuer>",
  "match": { "condition": "claims.sub == \"<client id>\" && claims.token_type == \"client_credentials\"" },
  "target": { "type": "service_account", "service_account_id": "<the service account>" },
  "workspace_id": "<the platform workspace>",
  "oauth_scope": "workspace:inference",
  "token_lifetime_seconds": 3600 }

Nothing about this is stored in the builder's database. The name is how the builder finds what it created, which is also why a deploy can repair it.

If a rule with that name already exists and is not exactly this — a different condition, target, scope, issuer or workspace — the builder reports it and stops. It never adopts a rule it cannot vouch for, and it never archives one it cannot prove belongs to that client. Fix or archive the rule in the Anthropic Console, then switch Claude access on again.

Platform operators

The builder acts at Anthropic as its own admin identity: it mints a Bio-ID assertion from its own BIO_CLIENT_ID / BIO_CLIENT_SECRET and exchanges it through one Console-created rule that grants org:admin. That token is held in memory only, for a few minutes, and never appears in a log line, a response or an error message. An org:admin token obtained this way can only manage rules scoped workspace:inference or workspace:developer, so the builder cannot grant anything broader than what a service gets here.

Builder env varValue
ANTHROPIC_ORG_IDAnthropic organization UUID
ANTHROPIC_FEDERATION_ISSUER_IDfdis_… — Bio-ID registered as a federation issuer
ANTHROPIC_ADMIN_RULE_IDfdrl_… — the Console-created rule that grants the builder org:admin
ANTHROPIC_ADMIN_SERVICE_ACCOUNT_IDsvac_… — that rule's admin service account
ANTHROPIC_FEDERATION_WORKSPACE_IDwrkspc_… — the workspace service rules are bound to
ANTHROPIC_INFERENCE_RULE_IDfdrl_… — a separate, inference-only rule for the builder's own Claude calls (see below). Never the admin rule

The first five are optional and belong together. If any is missing or malformed the feature is off: switching Claude access on is refused with 503, and deploys inject nothing. The sixth is independent of them: it switches how the builder authenticates its own Claude calls, nothing else.

  • ANTHROPIC_FEDERATION_WORKSPACE_ID is its own setting. It is not the builder's ANTHROPIC_WORKSPACE_ID, which belongs to the AI proxy's API key and may name a different workspace.
  • It must be the organization's default workspace. A new service account is a member of the default workspace only, and the builder does not add workspace memberships.
  • The builder's own client (builder-prod) is never given a service identity, and its flag is never auto-reverted. That flag is what lets the builder obtain its admin assertion.
  • Every switch writes a Septor event, oauth.claude_access_changed, with the client id, the outcome and the Anthropic ids. No tokens.

The builder's own Claude calls

The builder calls Claude itself: the /ai/chat proxy the tawa CLI falls back to when a developer has no key of their own, the AI advice on tawa troubleshoot, and the changelog summaries written on every deploy. Since iec-builder#274 those calls authenticate the same way a service's do — @insureco/bio/claude, with the builder's own builder-prod client — rather than with a static ANTHROPIC_API_KEY.

The difference from the provisioning identity above is the rule. Provisioning goes through the Console-created org:admin rule (ANTHROPIC_ADMIN_RULE_ID). Inference goes through a separate, inference-only rule, ANTHROPIC_INFERENCE_RULE_ID, scoped workspace:inference and targeting the same service account. The admin rule is never used for inference: the builder refuses to run with ANTHROPIC_INFERENCE_RULE_ID set to the admin rule's id, and reports it.

authModeWhenWhat the proxy sends to Anthropic
federationANTHROPIC_INFERENCE_RULE_ID is set and well formed, together with ANTHROPIC_ORG_ID, ANTHROPIC_ADMIN_SERVICE_ACCOUNT_ID and the builder's BIO_ID_URL / BIO_CLIENT_ID / BIO_CLIENT_SECRETAuthorization: Bearer <short-lived token>; no x-api-key, no anthropic-workspace-id
api-keyfederation is not configured and ANTHROPIC_API_KEY is setx-api-key, plus anthropic-workspace-id when ANTHROPIC_WORKSPACE_ID is set — exactly as before
unconfiguredneithernothing: GET /ai/status answers available: false and POST /ai/chat answers 503 AI_UNAVAILABLE
  • Federation wins when both are configured. A malformed id counts as missing.
  • fallbackToApiKey is on for the rollout: if an exchange fails while the key is still set, that request uses the key and the builder log carries one warning per outage. Remove the key once the builder's startup line says Claude API auth: federation.
  • The startup line names any missing setting (ANTHROPIC_INFERENCE_RULE_ID is not set, …) and never a value. GET /ai/status carries authMode beside available and model; the CLI keeps reading the latter two.
  • Nothing changes for a developer: tawa still uses its own key when it has one and the proxy when it does not.

What NOT to do

# WRONG: paste an Anthropic API key into a service that has Claude access
tawa config set --secret ANTHROPIC_API_KEY=sk-ant-...
# The key shadows federation in the SDK, and it is the long-lived secret this replaces.

# WRONG: set the injected ids by hand
tawa config set ANTHROPIC_FEDERATION_RULE_ID=fdrl_...  # refused: RESERVED_ENV_VAR
// WRONG: one assertion for the life of the process
const assertion = await getAssertion()          // at startup
setInterval(() => exchange(assertion), 9 * 60_000)   // 401 from the second call on

// RIGHT: a new assertion for every exchange; cache only what Anthropic returns
async function claudeToken() {
  if (cached && cached.expiresAt - 60_000 > Date.now()) return cached.token
  const assertion = await getAssertion()        // fresh, every time
  const { access_token, expires_in } = await exchange(assertion)
  cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 }
  return access_token
}
  • Do not call Bio-ID or the exchange from a browser. The client secret and the assertion are server-side only.
  • Do not expect the switch to change a running pod. Deploy.
  • Do not log the assertion or the access token.

Common mistakes

  • Expecting ANTHROPIC_* in the pod right after the switch. They arrive on the next deploy.
  • No spec.auth. No OAuth client, so nothing to switch and nothing to authenticate with.
  • Reusing an assertion. The second exchange fails with an opaque 401. The reason is only visible in the Anthropic Console's authentication history (jti_reused).
  • Leaving ANTHROPIC_API_KEY set. The SDK keeps using it and federation never runs.
  • Calling something outside workspace:inference. Files, Skills and Managed Agents answer 403.
  • Switching it on in prod and testing in sandbox. The flag is per client, and each environment has its own.

See also

Last updated: October 6, 2026