Skip to content

api-proxy: add Anthropic Workload Identity Federation provider (engine.auth) #3843

Description

@benissimo

Problem

The api-proxy sidecar supports OIDC authentication (AWF_AUTH_TYPE=github-oidc) for AWS Bedrock, GCP Vertex AI, and Azure OpenAI, but not for the Anthropic API itself. Workloads that want to authenticate to Anthropic via short-lived Workload Identity Federation tokens (sk-ant-oat01-..., bound to a Console service account) instead of long-lived static API keys (sk-ant-api03-...) currently have no path through gh-aw.

This blocks the natural cost-attribution + key-governance posture for organisations that want one Anthropic service account per product/workflow rather than a single shared key.

Context

A minimal working POC that bypasses the api-proxy entirely (plain GitHub Actions YAML, Anthropic Python SDK doing the exchange directly) is at:

https://gist.github.com/benissimo/340e7cfb5fdd102c5cd8d39cf91bcc32

The exchange completes in ~3 seconds end-to-end and produces a successful audit event in the Claude Console — proving the federation rule + service-account + exchange chain works against the existing Anthropic API tier (no Enterprise contract required). The remaining gap is purely on the gh-aw / api-proxy side.

Proposed solution

Mirror the AWS / GCP pattern. The change is small and self-contained:

1. New file: containers/api-proxy/anthropic-oidc-token-provider.js

Extends BaseOidcTokenProvider. _doRefresh() does:

  1. mintGitHubOidcToken({audience: 'https://api.anthropic.com'}) (using the existing github-oidc.js helper)
  2. POST https://api.anthropic.com/v1/oauth/token with:
    {
      "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
      "assertion": "<github-oidc-jwt>",
      "federation_rule_id": "fdrl_...",
      "organization_id": "<uuid>",
      "service_account_id": "svac_...",
      "workspace_id": "wrkspc_..."
    }
  3. Cache the returned access_token (sk-ant-oat01-...) and expires_in.

Refresh, retry, shutdown — all inherited from BaseOidcTokenProvider.

2. Edit containers/api-proxy/providers/anthropic.js

Add the OIDC config-parsing block, matching the structure already in providers/openai.js (lines ~70-130). Read from env:

Env var Purpose
AWF_AUTH_TYPE github-oidc to enable
AWF_AUTH_PROVIDER anthropic (selects this branch)
AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID required
AWF_AUTH_ANTHROPIC_ORGANIZATION_ID required
AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID required
AWF_AUTH_ANTHROPIC_WORKSPACE_ID optional (only needed if rule covers multiple workspaces)
AWF_AUTH_OIDC_AUDIENCE optional, defaults to https://api.anthropic.com

Modify getAuthHeaders(req) so that when the OIDC provider is configured, it returns Authorization: Bearer <oat-token-from-provider> (instead of the current x-api-key: <static-key>). Expose getOidcProvider() so server.js's startup latch initializes it.

The existing static-key path stays untouched — selection is by AWF_AUTH_TYPE presence.

3. Tests

Modeled on aws-oidc-token-provider.test.js — JWT mint, exchange POST, cache, refresh schedule, error paths.

4. (Out of scope for this repo, but related)

The githubnext/gh-aw frontmatter schema would need to expose this — likely as engine.auth.type: anthropic-wif plus the rule/org/SA/workspace fields — and compile them down to the AWF_AUTH_* env vars the api-proxy reads. Happy to file a parallel issue there once the api-proxy contract is settled here.

Why this is the right shape

  • No server.js changes. The adapter abstraction handles this entirely via the existing getOidcProvider() hook.
  • Refresh problem already solved by BaseOidcTokenProvider (REFRESH_FACTOR=0.75, auto-schedule, retry-on-failure). Inherited free.
  • Env-var forwarding already in place. awf --env-all forwards ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN to the api-proxy container; the AWS/GCP providers in providers/openai.js already consume them this way.
  • Credential-isolation property preserved. The real sk-ant-oat01-... never enters the agent container — same property the static-key path provides today.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions