diff --git a/containers/api-proxy/oidc-adapter-utils.js b/containers/api-proxy/oidc-adapter-utils.js index e26f7939a..c92f23acd 100644 --- a/containers/api-proxy/oidc-adapter-utils.js +++ b/containers/api-proxy/oidc-adapter-utils.js @@ -88,9 +88,34 @@ function resolveOidcAuthHeaders({ oidcProvider, awsOidcProvider, buildOidcHeader return null; } +/** + * Resolve auth headers with automatic static-key fallback. + * + * Combines OIDC resolution with a static-key fallback into a single call, + * encapsulating the repeated pattern across provider adapters: + * 1. If OIDC provider has a token → use buildOidcHeaders(token) + * 2. If OIDC is configured but no token yet → return empty {} (fail-safe) + * 3. If no OIDC → return staticHeaders + * + * @param {object} opts + * @param {{ getToken: () => (string|undefined|null) }|null|undefined} [opts.oidcProvider] + * @param {unknown} [opts.awsOidcProvider] + * @param {(token: string) => Record} opts.buildOidcHeaders + * @param {Record} opts.staticHeaders - Headers to use when no OIDC is configured + * @returns {Record} + */ +function resolveAuthHeadersWithFallback({ oidcProvider, awsOidcProvider, buildOidcHeaders, staticHeaders }) { + const oidcHeaders = resolveOidcAuthHeaders({ oidcProvider, awsOidcProvider, buildOidcHeaders }); + if (oidcHeaders !== null) { + return oidcHeaders; + } + return staticHeaders; +} + module.exports = { isValidHeaderName, validateAuthHeaderEnv, createOidcRuntimeAdapterMethods, resolveOidcAuthHeaders, + resolveAuthHeadersWithFallback, }; diff --git a/containers/api-proxy/providers/anthropic.js b/containers/api-proxy/providers/anthropic.js index 1d3ce4282..539b6f93c 100644 --- a/containers/api-proxy/providers/anthropic.js +++ b/containers/api-proxy/providers/anthropic.js @@ -20,6 +20,7 @@ const { const { validateAuthHeaderEnv, resolveOidcAuthHeaders, + resolveAuthHeadersWithFallback, } = require('../oidc-adapter-utils'); const { createBaseAdapterConfig, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); const { AnthropicOidcTokenProvider } = require('../anthropic-oidc-token-provider'); @@ -127,20 +128,16 @@ function createAnthropicAdapter(env, deps = {}) { validationPath: '/v1/messages', validationMethod: 'POST', validationBody: '{}', - validationHeaders: () => { - if (oidcProvider && oidcProvider.isReady()) { - return { - 'Authorization': `Bearer ${oidcProvider.getToken()}`, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }; - } - return { - [authHeaderName]: apiKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }; - }, + validationHeaders: () => ({ + ...resolveAuthHeadersWithFallback({ + oidcProvider, + awsOidcProvider: null, + buildOidcHeaders: (token) => ({ 'Authorization': `Bearer ${token}` }), + staticHeaders: { [authHeaderName]: apiKey }, + }), + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }), validationSkip: () => { if (!oidcConfigured) return null; // After OIDC init, validate using the acquired token @@ -149,12 +146,15 @@ function createAnthropicAdapter(env, deps = {}) { }, skipModelsFetch: () => oidcConfigured && !oidcProvider?.isReady(), modelsPath: '/v1/models', - modelsFetchHeaders: () => { - if (oidcProvider && oidcProvider.isReady()) { - return { 'Authorization': `Bearer ${oidcProvider.getToken()}`, 'anthropic-version': '2023-06-01' }; - } - return { [authHeaderName]: apiKey, 'anthropic-version': '2023-06-01' }; - }, + modelsFetchHeaders: () => ({ + ...resolveAuthHeadersWithFallback({ + oidcProvider, + awsOidcProvider: null, + buildOidcHeaders: (token) => ({ 'Authorization': `Bearer ${token}` }), + staticHeaders: { [authHeaderName]: apiKey }, + }), + 'anthropic-version': '2023-06-01', + }), reflectionConfigured: !!apiKey || oidcRequested, reflectionExtra: () => ({ auth_type: oidcRequested ? 'github-oidc/anthropic' : 'static-key', diff --git a/containers/api-proxy/proxy-utils.oidc.test.js b/containers/api-proxy/proxy-utils.oidc.test.js index b1a366b6b..705d58e1a 100644 --- a/containers/api-proxy/proxy-utils.oidc.test.js +++ b/containers/api-proxy/proxy-utils.oidc.test.js @@ -4,6 +4,7 @@ const { validateAuthHeaderEnv, createOidcRuntimeAdapterMethods, resolveOidcAuthHeaders, + resolveAuthHeadersWithFallback, } = require('./oidc-adapter-utils'); describe('isValidHeaderName', () => { @@ -106,3 +107,38 @@ describe('resolveOidcAuthHeaders', () => { expect(headers).toEqual({ Authorization: 'Bearer bearer-oidc-token' }); }); }); + +describe('resolveAuthHeadersWithFallback', () => { + it('returns OIDC headers when token is available', () => { + const headers = resolveAuthHeadersWithFallback({ + oidcProvider: { getToken: () => 'oidc-token' }, + awsOidcProvider: null, + buildOidcHeaders: (token) => ({ Authorization: ['oidc', token].join(':') }), + staticHeaders: { 'x-api-key': 'static-token' }, + }); + + expect(headers).toEqual({ Authorization: 'oidc:oidc-token' }); + }); + + it('returns an empty object when OIDC is configured but token is unavailable', () => { + const headers = resolveAuthHeadersWithFallback({ + oidcProvider: { getToken: () => '' }, + awsOidcProvider: null, + buildOidcHeaders: () => ({ Authorization: 'ignored-token' }), + staticHeaders: { 'x-api-key': 'static-token' }, + }); + + expect(headers).toEqual({}); + }); + + it('falls back to static headers when OIDC is not configured', () => { + const headers = resolveAuthHeadersWithFallback({ + oidcProvider: null, + awsOidcProvider: null, + buildOidcHeaders: () => ({ Authorization: 'ignored-token' }), + staticHeaders: { 'x-api-key': 'static-token' }, + }); + + expect(headers).toEqual({ 'x-api-key': 'static-token' }); + }); +});