fix(api-proxy): add required Anthropic WIF exchange parameters#3979
Conversation
Include federation_rule_id, organization_id, and service_account_id (and optional workspace_id) in the /v1/oauth/token exchange request body. Read these from AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID, AWF_AUTH_ANTHROPIC_ORGANIZATION_ID, AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID, and AWF_AUTH_ANTHROPIC_WORKSPACE_ID environment variables. Remove the unused scope field. Update docs and tests accordingly.
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
This PR fixes Anthropic GitHub OIDC → Anthropic WIF token exchange by sending the required Anthropic parameters (federation_rule_id, organization_id, service_account_id, optional workspace_id) in the /v1/oauth/token request body, and updates documentation/tests accordingly.
Changes:
- Extend Anthropic OIDC token exchange payload to include required WIF fields (and optionally
workspace_id). - Wire new Anthropic-specific WIF environment variables into the Anthropic provider adapter.
- Update docs and tests to reflect the new required configuration.
Show a summary per file
| File | Description |
|---|---|
| docs/awf-config-spec.md | Documents Anthropic WIF required/conditional configuration variables. |
| docs/api-proxy-sidecar.md | Updates Anthropic WIF env var docs and GitHub Actions example. |
| containers/api-proxy/providers/anthropic.js | Reads Anthropic WIF env vars and passes them into the OIDC token provider. |
| containers/api-proxy/anthropic-oidc-token-provider.test.js | Updates tests for new required config and adds WIF field-related assertions. |
| containers/api-proxy/anthropic-oidc-token-provider.js | Adds required WIF parameters to the Anthropic OAuth token exchange request body and logs. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
containers/api-proxy/providers/anthropic.js:77
- OIDC configuration gating/error messaging only checks for ACTIONS_ID_TOKEN_; with the new required Anthropic WIF fields, the adapter should also require non-empty federationRuleId/organizationId/serviceAccountId (and treat empty/whitespace values as missing). Otherwise we instantiate the provider with undefined fields and surface a confusing 503/400. Consider updating the
if (requestUrl && requestToken)guard andoidcUnavailableErrorto mention the required AWF_AUTH_ANTHROPIC_ variables.
if (oidcRequested) {
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (requestUrl && requestToken) {
const federationRuleId = env.AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID;
const organizationId = env.AWF_AUTH_ANTHROPIC_ORGANIZATION_ID;
const serviceAccountId = env.AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID;
const workspaceId = env.AWF_AUTH_ANTHROPIC_WORKSPACE_ID;
oidcProvider = new AnthropicOidcTokenProvider({
requestUrl,
requestToken,
federationRuleId,
organizationId,
serviceAccountId,
...(workspaceId !== undefined ? { workspaceId } : {}),
oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'https://api.anthropic.com',
});
}
}
const oidcConfigured = !!oidcProvider;
const oidcUnavailableError = oidcConfigured
? 'Anthropic OIDC token unavailable; retry shortly'
: 'Anthropic OIDC requires ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN (permissions: id-token: write).';
- Files reviewed: 5/5 changed files
- Comments generated: 7
| const federationRuleId = env.AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID; | ||
| const organizationId = env.AWF_AUTH_ANTHROPIC_ORGANIZATION_ID; | ||
| const serviceAccountId = env.AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID; | ||
| const workspaceId = env.AWF_AUTH_ANTHROPIC_WORKSPACE_ID; | ||
| oidcProvider = new AnthropicOidcTokenProvider({ | ||
| requestUrl, | ||
| requestToken, | ||
| federationRuleId, | ||
| organizationId, | ||
| serviceAccountId, | ||
| ...(workspaceId !== undefined ? { workspaceId } : {}), | ||
| oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'https://api.anthropic.com', | ||
| }); |
| constructor(config) { | ||
| super('anthropic_oidc', config); | ||
| this._requestUrl = config.requestUrl; | ||
| this._requestToken = config.requestToken; | ||
| this._federationRuleId = config.federationRuleId; | ||
| this._organizationId = config.organizationId; | ||
| this._serviceAccountId = config.serviceAccountId; | ||
| this._workspaceId = config.workspaceId; | ||
| this._oidcAudience = config.oidcAudience || 'https://api.anthropic.com'; | ||
| this._scope = config.scope; | ||
|
|
| const body = { | ||
| grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
| assertion: oidcJwt, | ||
| federation_rule_id: this._federationRuleId, | ||
| organization_id: this._organizationId, | ||
| service_account_id: this._serviceAccountId, | ||
| }; | ||
| if (this._workspaceId !== undefined) { | ||
| body.workspace_id = this._workspaceId; | ||
| } |
| # AWF_AUTH_ANTHROPIC_WORKSPACE_ID: wrkspc_... # required for multi-workspace rules | ||
| run: | | ||
| sudo --preserve-env=AWF_AUTH_TYPE,AWF_AUTH_PROVIDER \ | ||
| sudo --preserve-env=AWF_AUTH_TYPE,AWF_AUTH_PROVIDER,AWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID,AWF_AUTH_ANTHROPIC_ORGANIZATION_ID,AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID \ |
| it('should include required WIF fields in the exchange request body', async () => { | ||
| const capturedBodies = []; | ||
| const wifServer = createMockServer({ | ||
| oauthToken: (body) => { | ||
| capturedBodies.push(JSON.parse(body)); | ||
| return { | ||
| statusCode: 200, | ||
| body: JSON.stringify({ access_token: 'sk-ant-oat01-wif-token', expires_in: 3600 }), | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| await new Promise(resolve => wifServer.listen(0, '127.0.0.1', resolve)); | ||
| const wifPort = wifServer.address().port; | ||
|
|
||
| const provider = new AnthropicOidcTokenProvider({ | ||
| requestUrl: `http://127.0.0.1:${wifPort}/token`, | ||
| requestToken: 'mock-token', | ||
| federationRuleId: 'fdrl_myrule', | ||
| organizationId: 'org-00000000-0000-0000-0000-000000000001', | ||
| serviceAccountId: 'svac_myaccount', | ||
| workspaceId: 'wrkspc_myworkspace', | ||
| }); | ||
|
|
||
| // Override _exchangeForAnthropicToken to point at the local mock | ||
| const origExchange = provider._exchangeForAnthropicToken.bind(provider); | ||
| provider._exchangeForAnthropicToken = async (jwt) => { | ||
| const response = await httpPost( | ||
| `http://127.0.0.1:${wifPort}/v1/oauth/token`, | ||
| JSON.stringify({ | ||
| grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
| assertion: jwt, | ||
| federation_rule_id: provider._federationRuleId, | ||
| organization_id: provider._organizationId, | ||
| service_account_id: provider._serviceAccountId, | ||
| workspace_id: provider._workspaceId, | ||
| }), | ||
| { 'Content-Type': 'application/json', 'Accept': 'application/json' } | ||
| ); | ||
| const data = JSON.parse(response.body); | ||
| return { access_token: data.access_token, expires_in: data.expires_in || 3600 }; | ||
| }; | ||
|
|
| // Override _exchangeForAnthropicToken to point at the local mock | ||
| const origExchange = provider._exchangeForAnthropicToken.bind(provider); | ||
| provider._exchangeForAnthropicToken = async (jwt) => { |
| const provider = new AnthropicOidcTokenProvider({ | ||
| requestUrl: 'http://localhost:0/token', | ||
| requestToken: 'test', | ||
| federationRuleId: 'fdrl_norule', | ||
| organizationId: 'org-uuid', | ||
| serviceAccountId: 'svac_nows', | ||
| }); | ||
|
|
||
| // Access the private fields to verify workspace_id is undefined | ||
| expect(provider._workspaceId).toBeUndefined(); | ||
|
|
||
| provider.shutdown(); |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
All review feedback addressed in the latest commit. Here's a summary of what changed:
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Fixed in the latest commit. |
Smoke Test: Claude Engine
Result: PASS
|
🔥 Smoke Test: Copilot BYOK (Offline Mode)
Running in BYOK offline mode ( PR author: Overall: PARTIAL (BYOK path ✅, pre-step smoke data unexpanded)
|
🔥 Smoke Test Results
PR: fix(api-proxy): add required Anthropic WIF exchange parameters Overall: PASS
|
Smoke Test: API Proxy OpenTelemetry Tracing
All scenarios pass. ✅
|
Gemini Engine Smoke Test Results\n\n- PR: refactor: move test helper imports to .test-utils.ts files (#3972)\n- PR: (Not found - mcpscripts missing)\n- GitHub MCP Testing: ❌\n- GitHub.com Connectivity: ❌ (SSL 35 / HTTP 400)\n- File Writing Testing: ✅\n- Bash Tool Testing: ✅\n\nOverall status: FAILWarning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "localhost"See Network Configuration for more information.
|
Chroot Runtime Version Comparison
Result: Not all versions match —
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
Smoke Test Results — FAIL
Overall: ❌ FAIL
|
|
feat: support custom API auth headers for internal AI gateways ✅ Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
network:
allowed:
- defaults
- "registry.npmjs.org"See Network Configuration for more information.
|
The Anthropic OIDC provider was sending only
grant_typeandassertionto/v1/oauth/token, omitting thefederation_rule_id,organization_id, andservice_account_idfields that Anthropic requires — causing all exchanges to fail with400 invalid_grant.Changes
anthropic-oidc-token-provider.js: ExtendsAnthropicOidcTokenProviderConfigwith requiredfederationRuleId,organizationId,serviceAccountIdand optionalworkspaceId; includes them in the exchange request body; removes the unusedscopefield; surfaces the new fields in init success/failure log context. Constructor now validates required fields and throws a clear error if any are missing. Empty/whitespaceworkspaceIdis normalized toundefinedsoworkspace_id: ""is never sent.httpPostis stored asthis._httpPostto enable test spying without module-level mocking.providers/anthropic.js: ReadsAWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID,AWF_AUTH_ANTHROPIC_ORGANIZATION_ID,AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID,AWF_AUTH_ANTHROPIC_WORKSPACE_IDfrom env and passes them to the provider constructor.api-proxy-service.ts: AddsAWF_AUTH_ANTHROPIC_FEDERATION_RULE_ID,AWF_AUTH_ANTHROPIC_ORGANIZATION_ID,AWF_AUTH_ANTHROPIC_SERVICE_ACCOUNT_ID,AWF_AUTH_ANTHROPIC_WORKSPACE_IDto thepickEnvVarsOIDC allowlist so they are forwarded into the api-proxy sidecar container.anthropic-oidc-token-provider.test.js: Updates existing tests with required fields; rewrites WIF-fields and workspace_id-omission tests to spy onthis._httpPostand assert on the body produced by the real_exchangeForAnthropicTokenimplementation; adds tests for required-field validation and empty-stringworkspaceIdnormalization.api-proxy-service-env-forwarding.test.ts: Adds a test block covering forwarding of all fourAWF_AUTH_ANTHROPIC_*WIF env vars.Docs (
awf-config-spec.md§9.5.4,api-proxy-sidecar.md): Replaces the incorrect "no provider-specific variables required" with a config table and updated GitHub Actions example; addsAWF_AUTH_ANTHROPIC_WORKSPACE_IDto thesudo --preserve-env=...allowlist.The exchange body now looks like:
{ "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_..." }workspace_idis included only whenAWF_AUTH_ANTHROPIC_WORKSPACE_IDis set and non-empty — required when the federation rule covers multiple workspaces.