diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 0db190ed..03924c06 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -21,7 +21,7 @@ const { const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); const { AnthropicOidcTokenProvider } = require('../anthropic-oidc-token-provider'); const { ANTHROPIC_ENV } = require('../provider-env-constants'); -const { createProviderOidcAuth, createProviderOidcHeaderResolver } = require('./cloud-oidc-init'); +const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); const { bearerAuthHeaders, providerKeyHeaders } = require('./auth-headers'); let makeAnthropicTransform, loadCustomTransform, EXTENDED_CACHE_BETA; @@ -63,8 +63,8 @@ function createAnthropicAdapter(env, deps = {}) { const { oidcProvider, oidcConfigured, runtimeMethods: oidcRuntimeMethods, - resolveAuthHeaders, - } = createProviderOidcAuth(env, { + resolveHeaders: resolveOidcHeaders, + } = createProviderOidcHeaderStrategy(env, { staticAuthToken: apiKey, oidcProviderFactory: oidcRequested ? (env) => { const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; @@ -83,6 +83,9 @@ function createAnthropicAdapter(env, deps = {}) { oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'https://api.anthropic.com', }); } : null, + }, { + buildOidcHeaders: (token) => bearerAuthHeaders(token), + buildStaticHeaders: () => providerKeyHeaders(authHeaderName, apiKey), }); const oidcUnavailableError = oidcConfigured @@ -114,13 +117,6 @@ function createAnthropicAdapter(env, deps = {}) { // Build the composed transform once at construction time to avoid // re-allocating the wrapper function on every request. const composedBodyTransform = composeBodyTransforms(depsBodyTransform, optimisationsTransform); - const buildOidcBearerAuthHeaders = (token) => bearerAuthHeaders(token); - const buildStaticAuthHeaders = () => providerKeyHeaders(authHeaderName, apiKey); - const oidcHeaderResolver = createProviderOidcHeaderResolver({ - resolveAuthHeaders, - buildOidcHeaders: buildOidcBearerAuthHeaders, - buildStaticHeaders: buildStaticAuthHeaders, - }); const adapterMethods = createAdapterMethods({ apiKey, rawTarget, @@ -132,7 +128,7 @@ function createAnthropicAdapter(env, deps = {}) { validationMethod: 'POST', validationBody: '{}', validationHeaders: () => ({ - ...oidcHeaderResolver.resolveHeaders(), + ...resolveOidcHeaders(), 'anthropic-version': '2023-06-01', 'content-type': 'application/json', }), @@ -145,7 +141,7 @@ function createAnthropicAdapter(env, deps = {}) { skipModelsFetch: () => oidcConfigured && !oidcProvider?.isReady(), modelsPath: '/v1/models', modelsFetchHeaders: () => ({ - ...oidcHeaderResolver.resolveHeaders(), + ...resolveOidcHeaders(), 'anthropic-version': '2023-06-01', }), reflectionConfigured: !!apiKey || oidcRequested, @@ -168,7 +164,7 @@ function createAnthropicAdapter(env, deps = {}) { * @returns {Record} */ getAuthHeaders(req) { - const headers = oidcHeaderResolver.resolveHeaders(); + const headers = resolveOidcHeaders(); // OIDC configured but token not yet ready: fail closed so static creds are not leaked. if (Object.keys(headers).length === 0) { diff --git a/containers/api-proxy/providers/cloud-oidc-init.js b/containers/api-proxy/providers/cloud-oidc-init.js index 31b79b30..617dfc54 100644 --- a/containers/api-proxy/providers/cloud-oidc-init.js +++ b/containers/api-proxy/providers/cloud-oidc-init.js @@ -204,8 +204,50 @@ function createProviderOidcHeaderResolver({ }; } +/** + * Combined OIDC auth initialisation + header resolver for provider adapters. + * + * Eliminates the repeated two-step pattern of calling `createProviderOidcAuth` + * followed by `createProviderOidcHeaderResolver` in every provider adapter. + * Providers pass their provider-specific header builders; the common + * credential-resolution lifecycle is handled once in this function. + * + * @param {Record} env - Environment variables + * @param {object} [oidcAuthOptions] - Options forwarded to createProviderOidcAuth + * @param {string|undefined} [oidcAuthOptions.staticAuthToken] + * @param {boolean} [oidcAuthOptions.skipWhen=false] + * @param {((env: Record) => OidcAuthProvider|null|undefined)|null} [oidcAuthOptions.oidcProviderFactory] + * @param {object} headerOptions - Provider-specific header builders + * @param {(token: string) => Record} headerOptions.buildOidcHeaders + * @param {() => Record} headerOptions.buildStaticHeaders + * @returns {{ + * authProvider: string, + * oidcProvider: any, + * awsOidcProvider: any, + * oidcConfigured: boolean, + * runtimeMethods: { isEnabled: () => boolean, getOidcProvider: () => any, getAwsOidcProvider: () => any }, + * validationSkip: () => ({ skip: true, reason: string }|null), + * skipModelsFetch: () => boolean, + * resolveAuthHeaders: (buildOidcHeaders: (token: string) => Record, staticHeaders: Record) => Record, + * resolveHeaders: () => Record, + * }} + */ +function createProviderOidcHeaderStrategy(env, oidcAuthOptions = {}, { + buildOidcHeaders, + buildStaticHeaders, +}) { + const oidcAuth = createProviderOidcAuth(env, oidcAuthOptions); + const { resolveHeaders } = createProviderOidcHeaderResolver({ + resolveAuthHeaders: oidcAuth.resolveAuthHeaders, + buildOidcHeaders, + buildStaticHeaders, + }); + return { ...oidcAuth, resolveHeaders }; +} + module.exports = { resolveCloudOidcProviders, createProviderOidcAuth, createProviderOidcHeaderResolver, + createProviderOidcHeaderStrategy, }; diff --git a/containers/api-proxy/providers/cloud-oidc-init.test.js b/containers/api-proxy/providers/cloud-oidc-init.test.js index 62c4f122..ffc11ce8 100644 --- a/containers/api-proxy/providers/cloud-oidc-init.test.js +++ b/containers/api-proxy/providers/cloud-oidc-init.test.js @@ -4,6 +4,7 @@ const { resolveCloudOidcProviders, createProviderOidcAuth, createProviderOidcHeaderResolver, + createProviderOidcHeaderStrategy, } = require('./cloud-oidc-init'); describe('resolveCloudOidcProviders', () => { @@ -250,3 +251,83 @@ describe('createProviderOidcAuth', () => { auth.oidcProvider.shutdown(); }); }); + +describe('createProviderOidcHeaderStrategy', () => { + it('returns all oidcAuth fields plus a resolveHeaders function', () => { + const strategy = createProviderOidcHeaderStrategy({}, {}, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + }); + + expect(strategy.oidcConfigured).toBe(false); + expect(strategy.oidcProvider).toBeNull(); + expect(strategy.awsOidcProvider).toBeNull(); + expect(strategy.authProvider).toBe('azure'); + expect(typeof strategy.runtimeMethods).toBe('object'); + expect(typeof strategy.validationSkip).toBe('function'); + expect(typeof strategy.skipModelsFetch).toBe('function'); + expect(typeof strategy.resolveAuthHeaders).toBe('function'); + expect(typeof strategy.resolveHeaders).toBe('function'); + }); + + it('resolveHeaders returns static headers when no OIDC is configured', () => { + const strategy = createProviderOidcHeaderStrategy({}, {}, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + }); + + expect(strategy.resolveHeaders()).toEqual({ 'x-api-key': 'static-key' }); + }); + + it('resolveHeaders returns OIDC headers when a token is available', () => { + const mockProvider = { isReady: () => true, getToken: () => 'oidc-token' }; + const strategy = createProviderOidcHeaderStrategy({}, { + oidcProviderFactory: () => mockProvider, + }, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + }); + + expect(strategy.oidcConfigured).toBe(true); + expect(strategy.resolveHeaders()).toEqual({ Authorization: 'Bearer oidc-token' }); + }); + + it('resolveHeaders returns empty object when OIDC configured but token not ready', () => { + const mockProvider = { isReady: () => false, getToken: () => '' }; + const strategy = createProviderOidcHeaderStrategy({}, { + oidcProviderFactory: () => mockProvider, + }, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + }); + + expect(strategy.oidcConfigured).toBe(true); + expect(strategy.resolveHeaders()).toEqual({}); + }); + + it('forwards oidcAuthOptions to createProviderOidcAuth', () => { + const strategy = createProviderOidcHeaderStrategy({}, { + staticAuthToken: 'my-api-key', + }, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token }), + buildStaticHeaders: () => ({ 'x-api-key': 'my-api-key' }), + }); + + expect(strategy.runtimeMethods.isEnabled()).toBe(true); + }); + + it('resolveHeaders uses the provider-specific buildOidcHeaders decorator', () => { + const mockProvider = { isReady: () => true, getToken: () => 'oidc-token' }; + const strategy = createProviderOidcHeaderStrategy({}, { + oidcProviderFactory: () => mockProvider, + }, { + buildOidcHeaders: (token) => ({ Authorization: 'Bearer ' + token, 'x-custom': 'extra' }), + buildStaticHeaders: () => ({ 'x-api-key': 'static-key' }), + }); + + expect(strategy.resolveHeaders()).toEqual({ + Authorization: 'Bearer oidc-token', + 'x-custom': 'extra', + }); + }); +}); diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index 274e5e09..432f3f4f 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -35,7 +35,7 @@ const { deriveCopilotApiTarget, copilotTargetRequiresGitHubTokenPrefix, } = require('./copilot-auth'); -const { createProviderOidcAuth, createProviderOidcHeaderResolver } = require('./cloud-oidc-init'); +const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); const { bearerAuthHeaders, withCopilotIntegration } = require('./auth-headers'); const { URL } = require('url'); const { COPILOT_ENV } = require('../provider-env-constants'); @@ -66,11 +66,6 @@ function createCopilotAdapter(env, deps = {}) { // createOidcRuntimeAdapterMethods + oidcConfigured, and the real token is resolved // lazily inside getAuthHeaders. const authToken = staticAuthToken; - const { - authProvider, oidcProvider, awsOidcProvider, oidcConfigured, - runtimeMethods: oidcRuntimeMethods, - resolveAuthHeaders, - } = createProviderOidcAuth(env, { staticAuthToken: authToken, skipWhen: !!staticAuthToken }); // Extra headers to inject on all requests that use the BYOK API key. // Only populated when AWF_BYOK_EXTRA_HEADERS is set; ignored for standard // GitHub OAuth (COPILOT_GITHUB_TOKEN-only) requests. @@ -104,8 +99,11 @@ function createCopilotAdapter(env, deps = {}) { const bodyTransform = composeBodyTransforms(sanitizedBodyTransform, byokBodyFieldTransform); const requiresGitHubTokenPrefix = copilotTargetRequiresGitHubTokenPrefix(rawTarget, env); const authPrefix = (requiresGitHubTokenPrefix && !apiKey) ? 'token' : 'Bearer'; - const inferenceAuthHeaderResolver = createProviderOidcHeaderResolver({ - resolveAuthHeaders, + const { + authProvider, oidcProvider, awsOidcProvider, oidcConfigured, + runtimeMethods: oidcRuntimeMethods, + resolveHeaders: resolveInferenceHeaders, + } = createProviderOidcHeaderStrategy(env, { staticAuthToken: authToken, skipWhen: !!staticAuthToken }, { buildOidcHeaders: (token) => withCopilotIntegration(bearerAuthHeaders(token), integrationId), buildStaticHeaders: () => withCopilotIntegration({ ...(apiKey ? byokExtraHeaders : {}), @@ -236,7 +234,7 @@ function buildCopilotModelsRequest(extra = {}) { return withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId); } - return inferenceAuthHeaderResolver.resolveHeaders(); + return resolveInferenceHeaders(); }, bodyTransform, missingCredentialResponse: { diff --git a/containers/api-proxy/providers/openai.js b/containers/api-proxy/providers/openai.js index 94d6ee0e..5510df90 100644 --- a/containers/api-proxy/providers/openai.js +++ b/containers/api-proxy/providers/openai.js @@ -18,7 +18,7 @@ const { validateAuthHeaderEnv } = require('../oidc-adapter-utils'); const { bearerAuthHeaders, providerKeyHeaders } = require('./auth-headers'); const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); -const { createProviderOidcAuth, createProviderOidcHeaderResolver } = require('./cloud-oidc-init'); +const { createProviderOidcHeaderStrategy } = require('./cloud-oidc-init'); const { OPENAI_ENV, COPILOT_ENV } = require('../provider-env-constants'); /** @@ -64,13 +64,6 @@ function createOpenAIAdapter(env, deps = {}) { const basePath = explicitBasePath || (rawTarget === 'api.openai.com' ? '/v1' : ''); // OIDC auth strategy (Azure OpenAI, AWS Bedrock, GCP Vertex AI) - const { - authProvider, oidcConfigured, - runtimeMethods: oidcRuntimeMethods, - validationSkip, - skipModelsFetch, - resolveAuthHeaders, - } = createProviderOidcAuth(env, { staticAuthToken: apiKey }); function buildTokenAuthHeaders(key) { if (customAuthHeader) { return providerKeyHeaders(customAuthHeader, key); @@ -78,8 +71,13 @@ function createOpenAIAdapter(env, deps = {}) { return bearerAuthHeaders(key); } const buildStaticAuthHeaders = () => buildTokenAuthHeaders(apiKey); - const oidcHeaderResolver = createProviderOidcHeaderResolver({ - resolveAuthHeaders, + const { + authProvider, oidcConfigured, + runtimeMethods: oidcRuntimeMethods, + validationSkip, + skipModelsFetch, + resolveHeaders: resolveOidcHeaders, + } = createProviderOidcHeaderStrategy(env, { staticAuthToken: apiKey }, { buildOidcHeaders: buildTokenAuthHeaders, buildStaticHeaders: buildStaticAuthHeaders, }); @@ -110,7 +108,7 @@ function createOpenAIAdapter(env, deps = {}) { isManagementPort: true, adapterMethods, getAuthHeaders() { - return oidcHeaderResolver.resolveHeaders(); + return resolveOidcHeaders(); }, bodyTransform, missingCredentialResponse: {