Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions containers/api-proxy/providers/anthropic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -132,7 +128,7 @@ function createAnthropicAdapter(env, deps = {}) {
validationMethod: 'POST',
validationBody: '{}',
validationHeaders: () => ({
...oidcHeaderResolver.resolveHeaders(),
...resolveOidcHeaders(),
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
}),
Expand All @@ -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,
Expand All @@ -168,7 +164,7 @@ function createAnthropicAdapter(env, deps = {}) {
* @returns {Record<string, string>}
*/
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) {
Expand Down
42 changes: 42 additions & 0 deletions containers/api-proxy/providers/cloud-oidc-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string|undefined>} env - Environment variables
* @param {object} [oidcAuthOptions] - Options forwarded to createProviderOidcAuth
* @param {string|undefined} [oidcAuthOptions.staticAuthToken]
* @param {boolean} [oidcAuthOptions.skipWhen=false]
* @param {((env: Record<string, string|undefined>) => OidcAuthProvider|null|undefined)|null} [oidcAuthOptions.oidcProviderFactory]
* @param {object} headerOptions - Provider-specific header builders
* @param {(token: string) => Record<string, string>} headerOptions.buildOidcHeaders
* @param {() => Record<string, string>} 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<string, string>, staticHeaders: Record<string, string>) => Record<string, string>,
* resolveHeaders: () => Record<string, string>,
* }}
*/
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,
};
81 changes: 81 additions & 0 deletions containers/api-proxy/providers/cloud-oidc-init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
resolveCloudOidcProviders,
createProviderOidcAuth,
createProviderOidcHeaderResolver,
createProviderOidcHeaderStrategy,
} = require('./cloud-oidc-init');

describe('resolveCloudOidcProviders', () => {
Expand Down Expand Up @@ -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',
});
});
});
16 changes: 7 additions & 9 deletions containers/api-proxy/providers/copilot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 : {}),
Expand Down Expand Up @@ -236,7 +234,7 @@ function buildCopilotModelsRequest(extra = {}) {
return withCopilotIntegration({ 'Authorization': prefix + ' ' + githubToken }, integrationId);
}

return inferenceAuthHeaderResolver.resolveHeaders();
return resolveInferenceHeaders();
},
bodyTransform,
missingCredentialResponse: {
Expand Down
20 changes: 9 additions & 11 deletions containers/api-proxy/providers/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

/**
Expand Down Expand Up @@ -64,22 +64,20 @@ 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);
}
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,
});
Expand Down Expand Up @@ -110,7 +108,7 @@ function createOpenAIAdapter(env, deps = {}) {
isManagementPort: true,
adapterMethods,
getAuthHeaders() {
return oidcHeaderResolver.resolveHeaders();
return resolveOidcHeaders();
},
bodyTransform,
missingCredentialResponse: {
Expand Down
Loading