feat(api-proxy): GitHub OIDC → Azure AD workload identity federation auth#2562
feat(api-proxy): GitHub OIDC → Azure AD workload identity federation auth#2562
Conversation
There was a problem hiding this comment.
Pull request overview
Adds GitHub Actions OIDC → Azure AD workload identity support to the api-proxy so OpenAI-compatible traffic can authenticate with short-lived federated tokens instead of static API keys. This extends the existing provider-adapter architecture and compose generation for api-proxy-based credential isolation.
Changes:
- Adds a new
OidcTokenManagerto fetch GitHub OIDC tokens, exchange them for Azure AD access tokens, cache them, and refresh them proactively. - Updates the OpenAI adapter and provider server to support async auth header resolution and OIDC-backed request handling.
- Forwards OIDC/Azure-related environment variables into the api-proxy container and adds unit coverage for the new auth flow.
Show a summary per file
| File | Description |
|---|---|
src/docker-manager-compose.test.ts |
Adds compose-generation tests for forwarding OIDC/Azure env vars to api-proxy. |
src/compose-generator.ts |
Forwards OIDC/Azure/GitHub Actions token env vars into the api-proxy service. |
containers/api-proxy/server.test.js |
Adds adapter/server tests for async auth headers, OIDC behavior, and 503 auth failures. |
containers/api-proxy/server.js |
Wires in the OIDC token manager and makes request handling support async auth resolution. |
containers/api-proxy/providers/openai.js |
Adds OIDC-aware OpenAI adapter behavior and async auth header generation. |
containers/api-proxy/providers/index.js |
Passes the shared OIDC dependency into the OpenAI adapter. |
containers/api-proxy/oidc-auth.test.js |
Adds focused unit tests for token manager lifecycle, caching, and helper behavior. |
containers/api-proxy/oidc-auth.js |
Implements GitHub OIDC → Azure AD token acquisition, caching, refresh, and retry logic. |
containers/api-proxy/Dockerfile |
Includes the new oidc-auth.js file in the api-proxy image build. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (2)
src/compose-generator.ts:1357
- This only wires OIDC through raw host environment variables. There is still no corresponding config-file/CLI/schema support for the advertised
apiProxy.auth/engine.authblock insrc/config-file.ts,src/types/config.ts, orsrc/cli.ts, so the feature described in the PR is not actually configurable that way.
...(process.env.AWF_AUTH_TYPE && { AWF_AUTH_TYPE: process.env.AWF_AUTH_TYPE }),
...(process.env.AWF_AUTH_AUDIENCE && { AWF_AUTH_AUDIENCE: process.env.AWF_AUTH_AUDIENCE }),
...(process.env.AWF_AZURE_TENANT_ID && { AWF_AZURE_TENANT_ID: process.env.AWF_AZURE_TENANT_ID }),
...(process.env.AWF_AZURE_CLIENT_ID && { AWF_AZURE_CLIENT_ID: process.env.AWF_AZURE_CLIENT_ID }),
...(process.env.AWF_AZURE_SCOPE && { AWF_AZURE_SCOPE: process.env.AWF_AZURE_SCOPE }),
// Forward Actions OIDC endpoint vars so the api-proxy can mint tokens inside the container.
// These are safe to forward: they are ephemeral runner-scoped credentials, not long-lived secrets.
...(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && { ACTIONS_ID_TOKEN_REQUEST_URL: process.env.ACTIONS_ID_TOKEN_REQUEST_URL }),
...(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && { ACTIONS_ID_TOKEN_REQUEST_TOKEN: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }),
src/compose-generator.ts:1357
- Enabling OIDC here also introduces two new mandatory egress dependencies (
login.microsoftonline.comand the host fromACTIONS_ID_TOKEN_REQUEST_URL), but the existing preflight allowlist warning path is not updated for either one. A workflow that already has--enable-api-proxyconfigured will therefore look healthy and then fail with 503/auth errors as soon as the sidecar tries to mint a token through Squid.
...(process.env.AWF_AUTH_TYPE && { AWF_AUTH_TYPE: process.env.AWF_AUTH_TYPE }),
...(process.env.AWF_AUTH_AUDIENCE && { AWF_AUTH_AUDIENCE: process.env.AWF_AUTH_AUDIENCE }),
...(process.env.AWF_AZURE_TENANT_ID && { AWF_AZURE_TENANT_ID: process.env.AWF_AZURE_TENANT_ID }),
...(process.env.AWF_AZURE_CLIENT_ID && { AWF_AZURE_CLIENT_ID: process.env.AWF_AZURE_CLIENT_ID }),
...(process.env.AWF_AZURE_SCOPE && { AWF_AZURE_SCOPE: process.env.AWF_AZURE_SCOPE }),
// Forward Actions OIDC endpoint vars so the api-proxy can mint tokens inside the container.
// These are safe to forward: they are ephemeral runner-scoped credentials, not long-lived secrets.
...(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && { ACTIONS_ID_TOKEN_REQUEST_URL: process.env.ACTIONS_ID_TOKEN_REQUEST_URL }),
...(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && { ACTIONS_ID_TOKEN_REQUEST_TOKEN: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }),
- Files reviewed: 9/9 changed files
- Comments generated: 2
| ...(process.env.AWF_AUTH_TYPE && { AWF_AUTH_TYPE: process.env.AWF_AUTH_TYPE }), | ||
| ...(process.env.AWF_AUTH_AUDIENCE && { AWF_AUTH_AUDIENCE: process.env.AWF_AUTH_AUDIENCE }), | ||
| ...(process.env.AWF_AZURE_TENANT_ID && { AWF_AZURE_TENANT_ID: process.env.AWF_AZURE_TENANT_ID }), | ||
| ...(process.env.AWF_AZURE_CLIENT_ID && { AWF_AZURE_CLIENT_ID: process.env.AWF_AZURE_CLIENT_ID }), | ||
| ...(process.env.AWF_AZURE_SCOPE && { AWF_AZURE_SCOPE: process.env.AWF_AZURE_SCOPE }), | ||
| // Forward Actions OIDC endpoint vars so the api-proxy can mint tokens inside the container. | ||
| // These are safe to forward: they are ephemeral runner-scoped credentials, not long-lived secrets. | ||
| ...(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && { ACTIONS_ID_TOKEN_REQUEST_URL: process.env.ACTIONS_ID_TOKEN_REQUEST_URL }), | ||
| ...(process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && { ACTIONS_ID_TOKEN_REQUEST_TOKEN: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN }), |
| getModelsFetchConfig() { | ||
| if (oidcEnabled) return null; // token not yet available at startup | ||
| if (!apiKey) return null; |
|
@copilot merge main and address the review feedback |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
| * Unit tests for containers/api-proxy/oidc-auth.js | ||
| */ | ||
|
|
||
| const { createOidcTokenManager, OidcTokenManager, makeJsonRequest } = require('./oidc-auth'); |
| mgr._proxyAgent = undefined; | ||
|
|
||
| // Patch the module-level helper by monkey-patching the private method | ||
| const origFetch = mgr._fetchGitHubOidcToken.bind(mgr); |
| token_type: 'Bearer', | ||
| }; | ||
|
|
||
| const origExchange = mgr._exchangeForAzureToken.bind(mgr); |
…entication-support Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
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.
Security ReviewThe OIDC implementation is well-structured overall. One defence-in-depth gap worth addressing: Finding:
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Done in 93d636f. Merged main (picking up billing headers + X-Initiator injection from #2571) and addressed both review issues:
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
This comment has been minimized.
This comment has been minimized.
🔥 Smoke Test: Copilot BYOK (Offline) Mode
Running in BYOK offline mode ( Overall: PASS (core BYOK path confirmed working) PR by
|
|
Smoke Test Results ✅ GitHub MCP: Retrieved merged PRs
✅ Playwright: github.com page title verified Overall Status: PASS
|
🔬 Smoke Test Results
Overall: PARTIAL — MCP connectivity confirmed. Pre-computed test data ( PR by
|
Smoke Test: FAIL✅ PR: api-proxy: inject X-Initiator: agent default on all Copilot-bound requests to prevent billing inflation 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.
|
Chroot Smoke Test Results
Overall: ❌ Some versions do not match between host and chroot environment.
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
Smoke Test Results — FAIL
Overall: FAIL —
|
|
@copilot merge main |
…entication-support # Conflicts: # containers/api-proxy/Dockerfile # containers/api-proxy/server.js # src/docker-manager-compose.test.ts
Done in fdce93d. Merged main (server.js refactored into sub-modules, test files split) and also fixed the three CodeQL unused-variable alerts in Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
The api-proxy sidecar only supported static API keys, blocking Azure OpenAI deployments with Entra ID-only authentication (API keys disabled). This adds
engine.authblock support via GitHub Actions OIDC federation.New:
containers/api-proxy/oidc-auth.jsOidcTokenManagerhandles the full token lifecycle:ACTIONS_ID_TOKEN_REQUEST_URL?audience=<AWF_AUTH_AUDIENCE>client_credentials+client_assertion(federated identity grant) againstlogin.microsoftonline.com/{tenant}/oauth2/v2.0/tokenModified:
providers/openai.jsAccepts
deps.oidcAuth(OidcTokenManager).getAuthHeaders()is nowasync—returns the Azure AD bearer token when OIDC is enabled, falls back to staticOPENAI_API_KEYotherwise. Validation probe is skipped for OIDC adapters.getModelsFetchConfig()is nowasyncin OIDC mode—it acquires a fresh token so thatfetchStartupModels()populatescachedModels.openai, ensuring/reflect,models.json, andAWF_MODEL_ALIASESentries targetingopenai/*work correctly.Modified:
server.jscreateProviderServerrequest/upgrade handlers are nowasync, usingawait Promise.resolve(adapter.getAuthHeaders(req))— backward-compatible with existing sync adapters. Returns503with a clear error when token acquisition fails. OIDC managerstart()is awaited before binding listeners;stop()called on SIGTERM/SIGINT.fetchStartupModels()usesPromise.resolve(adapter.getModelsFetchConfig?.())to transparently support both sync and async config implementations.Modified:
src/compose-generator.tsForwards the following env vars from host to the api-proxy container when set:
When
AWF_AUTH_TYPE=github-oidcis set andenableApiProxyis true but no staticopenaiApiKeyis configured (OIDC-only mode), the agent container also receivesOPENAI_BASE_URLpointing to the api-proxy sidecar plus placeholder keys (sk-placeholder-for-oidc-proxy), ensuring OpenAI/Codex traffic routes through the sidecar instead of connecting directly to the upstream.Required allow-list additions
The api-proxy routes through Squid, so these domains must be in
--allow-domains:ACTIONS_ID_TOKEN_REQUEST_URL(e.g.pipelines.actions.githubusercontent.com)login.microsoftonline.comTests
60+ new tests across
oidc-auth.test.js(32),server.test.js(+21 including asyncgetAuthHeaders, asyncgetModelsFetchConfig, 503-on-auth-error, and asyncfetchStartupModelsadapter path), anddocker-manager-compose.test.ts(+7 including OIDC env-var forwarding and OIDC-only agent routing).