From b3cdb9d7e769be46f3e13bc17dd37f0831b057d1 Mon Sep 17 00:00:00 2001 From: cerredz <422michaelcerreto@gmail.com> Date: Fri, 15 May 2026 19:17:53 -0400 Subject: [PATCH] Fix Anthropic WIF token exchange --- docs/pages/platform-deployment.md | 2 +- .../pages/platform-supported-llm-providers.md | 2 +- .../clients/anthropic-wif-credentials.test.ts | 67 +++++++++++++++++-- .../src/clients/anthropic-wif-credentials.ts | 13 ++-- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/docs/pages/platform-deployment.md b/docs/pages/platform-deployment.md index ba3cb5a220..3f94c70415 100644 --- a/docs/pages/platform-deployment.md +++ b/docs/pages/platform-deployment.md @@ -787,7 +787,7 @@ These environment variables set the default base URL for each LLM provider. Per- - **`ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID`** - Your Anthropic organization ID. -- **`ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID`** - (Optional) Service account ID (`svac_...`) for target verification. +- **`ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID`** - Service account ID (`svac_...`) for the target service account. - **`ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID`** - (Optional) Workspace ID to scope the minted token. diff --git a/docs/pages/platform-supported-llm-providers.md b/docs/pages/platform-supported-llm-providers.md index da65c8e098..26fa172a82 100644 --- a/docs/pages/platform-supported-llm-providers.md +++ b/docs/pages/platform-supported-llm-providers.md @@ -93,7 +93,7 @@ To enable WIF, set the following environment variables: | `ARCHESTRA_ANTHROPIC_WIF_ENABLED` | Yes | Set to `true` to enable WIF | | `ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID` | Yes | Federation rule ID (`fdrl_...`) from Claude Console | | `ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID` | Yes | Your Anthropic organization ID | -| `ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID` | No | Service account ID (`svac_...`) for target verification | +| `ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID` | Yes | Service account ID (`svac_...`) for the target service account | | `ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID` | No | Workspace ID to scope the token | | `ARCHESTRA_ANTHROPIC_WIF_IDENTITY_TOKEN_FILE` | No | Path to the file containing the OIDC identity token (JWT). Falls back to `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` env var | diff --git a/platform/backend/src/clients/anthropic-wif-credentials.test.ts b/platform/backend/src/clients/anthropic-wif-credentials.test.ts index b5b37d735b..a1f8de4dda 100644 --- a/platform/backend/src/clients/anthropic-wif-credentials.test.ts +++ b/platform/backend/src/clients/anthropic-wif-credentials.test.ts @@ -1,6 +1,5 @@ -import { describe, expect, test, vi, beforeEach, afterEach } from "@/test"; +import { afterEach, beforeEach, describe, expect, test, vi } from "@/test"; -// We test the module-level functions by mocking config and fetch describe("anthropic-wif-credentials", () => { const originalEnv = process.env; @@ -11,6 +10,7 @@ describe("anthropic-wif-credentials", () => { afterEach(() => { process.env = originalEnv; + vi.unstubAllGlobals(); }); describe("isAnthropicWifEnabled", () => { @@ -20,12 +20,69 @@ describe("anthropic-wif-credentials", () => { ); expect(isAnthropicWifEnabled()).toBe(false); }); + + test("requires service account configuration", async () => { + process.env.ARCHESTRA_ANTHROPIC_WIF_ENABLED = "true"; + process.env.ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID = "fdrl_test"; + process.env.ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID = + "00000000-0000-0000-0000-000000000000"; + + const { isAnthropicWifEnabled } = await import( + "@/clients/anthropic-wif-credentials" + ); + + expect(isAnthropicWifEnabled()).toBe(false); + }); }); describe("getAnthropicWifAccessToken", () => { - test("throws when no identity token source is configured", async () => { - // This tests the error path when WIF env vars are set but no token source - // We can't easily test the full flow without mocking fetch and fs + test("exchanges an identity token with Anthropic's JSON token request", async () => { + process.env.ARCHESTRA_ANTHROPIC_WIF_ENABLED = "true"; + process.env.ARCHESTRA_ANTHROPIC_WIF_FEDERATION_RULE_ID = "fdrl_test"; + process.env.ARCHESTRA_ANTHROPIC_WIF_ORGANIZATION_ID = + "00000000-0000-0000-0000-000000000000"; + process.env.ARCHESTRA_ANTHROPIC_WIF_SERVICE_ACCOUNT_ID = "svac_test"; + process.env.ARCHESTRA_ANTHROPIC_WIF_WORKSPACE_ID = "wrkspc_test"; + process.env.ANTHROPIC_IDENTITY_TOKEN = "identity-jwt"; + + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: vi.fn().mockResolvedValue({ + access_token: "sk-ant-oat01-token", + token_type: "Bearer", + expires_in: 3600, + scope: "workspace:developer", + }), + }); + vi.stubGlobal("fetch", fetchMock); + + const { getAnthropicWifAccessToken } = await import( + "@/clients/anthropic-wif-credentials" + ); + + await expect(getAnthropicWifAccessToken()).resolves.toBe( + "sk-ant-oat01-token", + ); + await expect(getAnthropicWifAccessToken()).resolves.toBe( + "sk-ant-oat01-token", + ); + + expect(fetchMock).toHaveBeenCalledOnce(); + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe("https://api.anthropic.com/v1/oauth/token"); + expect(init.method).toBe("POST"); + expect(init.headers).toMatchObject({ + "Content-Type": "application/json", + "anthropic-version": "2023-06-01", + }); + expect(JSON.parse(init.body as string)).toEqual({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: "identity-jwt", + federation_rule_id: "fdrl_test", + organization_id: "00000000-0000-0000-0000-000000000000", + service_account_id: "svac_test", + workspace_id: "wrkspc_test", + }); }); }); }); diff --git a/platform/backend/src/clients/anthropic-wif-credentials.ts b/platform/backend/src/clients/anthropic-wif-credentials.ts index b7cfc2858b..d1f93dab01 100644 --- a/platform/backend/src/clients/anthropic-wif-credentials.ts +++ b/platform/backend/src/clients/anthropic-wif-credentials.ts @@ -20,7 +20,10 @@ let cachedToken: { token: string; expiresAt: number } | null = null; export function isAnthropicWifEnabled(): boolean { const { wif } = config.llm.anthropic; return ( - wif.enabled && !!wif.federationRuleId && !!wif.organizationId + wif.enabled && + !!wif.federationRuleId && + !!wif.organizationId && + !!wif.serviceAccountId ); } @@ -81,9 +84,7 @@ export async function getAnthropicWifAccessToken(): Promise { federation_rule_id: federationRuleId, organization_id: organizationId, }; - if (serviceAccountId) { - body.service_account_id = serviceAccountId; - } + body.service_account_id = serviceAccountId; if (workspaceId) { body.workspace_id = workspaceId; } @@ -98,10 +99,10 @@ export async function getAnthropicWifAccessToken(): Promise { const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/x-www-form-urlencoded", + "Content-Type": "application/json", "anthropic-version": "2023-06-01", }, - body: new URLSearchParams(body).toString(), + body: JSON.stringify(body), }); if (!response.ok) {