feat(api-proxy): OIDC authentication for Azure OpenAI (Entra-only)#2599
feat(api-proxy): OIDC authentication for Azure OpenAI (Entra-only)#2599
Conversation
Adds GitHub Actions OIDC → Azure AD workload identity federation support to the api-proxy sidecar. This enables BYOK mode with Azure OpenAI deployments that have API keys disabled (Entra-only authentication). Components: - oidc-token-provider.js: Mints GitHub OIDC token, exchanges for Azure AD access token, caches with proactive background refresh - OpenAI adapter: Now supports AWF_AUTH_TYPE=github-oidc as alternative to static OPENAI_API_KEY - api-proxy-service.ts: Forwards OIDC env vars to sidecar container - server.js: Initializes OIDC providers on startup, cleans up on shutdown New env vars (set by gh-aw when engine.auth is configured): - AWF_AUTH_TYPE=github-oidc - AWF_AUTH_AZURE_TENANT_ID - AWF_AUTH_AZURE_CLIENT_ID - AWF_AUTH_OIDC_AUDIENCE (default: api://AzureADTokenExchange) - AWF_AUTH_AZURE_SCOPE (default: https://cognitiveservices.azure.com/.default) - AWF_AUTH_AZURE_CLOUD (public|usgovernment|china) - ACTIONS_ID_TOKEN_REQUEST_URL (from Actions runtime) - ACTIONS_ID_TOKEN_REQUEST_TOKEN (from Actions runtime) Closes #2544 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
| Metric | Base | PR | Delta |
|---|---|---|---|
| Lines | 86.76% | 86.84% | 📈 +0.08% |
| Statements | 86.70% | 86.77% | 📈 +0.07% |
| Functions | 81.22% | 81.22% | ➡️ +0.00% |
| Branches | 79.45% | 79.22% | 📉 -0.23% |
📁 Per-file Coverage Changes (1 files)
| File | Lines (Before → After) | Statements (Before → After) |
|---|---|---|
src/container-lifecycle.ts |
87.1% → 88.2% (+1.14%) | 87.5% → 88.6% (+1.11%) |
Coverage comparison generated by scripts/ci/compare-coverage.ts
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.
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 ReviewFinding 1:
|
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Adds GitHub Actions OIDC-based authentication to the containers/api-proxy sidecar so Azure OpenAI deployments can run in Entra-only mode (no static API keys), and wires the required env propagation from the CLI service layer.
Changes:
- Introduces an
OidcTokenProviderthat mints a GitHub Actions OIDC token and exchanges it for an Azure AD access token with caching/refresh. - Updates the OpenAI adapter to support
AWF_AUTH_TYPE=github-oidcand server startup/shutdown to initialize/cleanup OIDC providers. - Forwards OIDC-related env vars (including Actions runtime token vars) into the api-proxy container.
Show a summary per file
| File | Description |
|---|---|
src/services/api-proxy-service.ts |
Forwards OIDC configuration and Actions runtime OIDC env vars into the api-proxy container. |
containers/api-proxy/server.js |
Initializes OIDC providers during startup and shuts them down on SIGTERM/SIGINT. |
containers/api-proxy/providers/openai.js |
Adds OIDC auth path and reflection metadata for OpenAI/Azure OpenAI via bearer tokens. |
containers/api-proxy/oidc-token-provider.js |
New token provider implementing GitHub OIDC mint + Azure token exchange + refresh scheduling. |
containers/api-proxy/oidc-token-provider.test.js |
New unit tests covering provider behavior and OpenAI adapter integration. |
containers/api-proxy/Dockerfile |
Ships the new OIDC provider file in the api-proxy image. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comments suppressed due to low confidence (1)
containers/api-proxy/providers/openai.js:140
getModelsFetchConfig()returns null whenever OIDC is configured, butfetchStartupModels()only fetches models viagetModelsFetchConfig(). As a result, OpenAI/Azure models will never be cached/populated in models.json for OIDC deployments, even afterprovider.initialize()completes. Consider returning a models fetch config in OIDC mode onceoidcProvider.isReady()is true (using the bearer token), or triggering a second model fetch after OIDC init.
getModelsFetchConfig() {
if (oidcEnabled) return null; // Models fetched after OIDC init
if (!apiKey) return null;
const modelsPath = basePath ? `${basePath}/models` : '/v1/models';
return {
url: `https://${rawTarget}${modelsPath}`,
opts: { method: 'GET', headers: { 'Authorization': `Bearer ${apiKey}` } },
cacheKey: 'openai',
};
- Files reviewed: 6/6 changed files
- Comments generated: 6
| isEnabled() { return !!apiKey || oidcEnabled; }, | ||
| getTargetHost() { return rawTarget; }, | ||
| getBasePath() { return basePath; }, | ||
|
|
||
| /** | ||
| * Get the OIDC token provider (if configured). | ||
| * Used by server.js to initialize OIDC on startup. | ||
| * @returns {OidcTokenProvider|null} | ||
| */ | ||
| getOidcProvider() { return oidcProvider; }, | ||
|
|
||
| getAuthHeaders() { | ||
| // OIDC takes precedence when configured | ||
| if (oidcProvider) { | ||
| const token = oidcProvider.getToken(); | ||
| if (token) { | ||
| return { 'Authorization': `Bearer ${token}`, 'api-key': token }; | ||
| } | ||
| // Token not yet available (pre-init or refresh failure) | ||
| // Return empty — server will return 503 via isEnabled() short-circuit | ||
| return { 'Authorization': 'Bearer oidc-token-unavailable' }; | ||
| } | ||
| return { 'Authorization': `Bearer ${apiKey}` }; |
| getAuthHeaders() { | ||
| // OIDC takes precedence when configured | ||
| if (oidcProvider) { | ||
| const token = oidcProvider.getToken(); | ||
| if (token) { | ||
| return { 'Authorization': `Bearer ${token}`, 'api-key': token }; | ||
| } | ||
| // Token not yet available (pre-init or refresh failure) | ||
| // Return empty — server will return 503 via isEnabled() short-circuit | ||
| return { 'Authorization': 'Bearer oidc-token-unavailable' }; | ||
| } |
| // Schedule proactive refresh | ||
| const refreshInSecs = Math.max( | ||
| expires_in * REFRESH_FACTOR, | ||
| expires_in - MIN_REFRESH_MARGIN_SECS |
| _httpGet(url, headers) { | ||
| return new Promise((resolve, reject) => { | ||
| const parsedUrl = new URL(url); | ||
| const mod = parsedUrl.protocol === 'https:' ? https : http; | ||
| const req = mod.get(url, { headers }, (res) => { | ||
| let body = ''; | ||
| res.on('data', (chunk) => { body += chunk; }); | ||
| res.on('end', () => resolve({ statusCode: res.statusCode, body })); | ||
| }); | ||
| req.on('error', reject); | ||
| req.setTimeout(10_000, () => { req.destroy(new Error('OIDC request timeout')); }); | ||
| }); |
| const parsedUrl = new URL(url); | ||
| const options = { | ||
| method: 'POST', | ||
| hostname: parsedUrl.hostname, | ||
| port: parsedUrl.port || 443, | ||
| path: parsedUrl.pathname + parsedUrl.search, | ||
| headers: { ...headers, 'Content-Length': Buffer.byteLength(body) }, | ||
| }; |
| // GitHub Actions OIDC runtime tokens (needed by OIDC token provider in api-proxy) | ||
| ...(process.env.AWF_AUTH_TYPE === 'github-oidc' && process.env.ACTIONS_ID_TOKEN_REQUEST_URL && { | ||
| ACTIONS_ID_TOKEN_REQUEST_URL: process.env.ACTIONS_ID_TOKEN_REQUEST_URL, | ||
| }), | ||
| ...(process.env.AWF_AUTH_TYPE === 'github-oidc' && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN && { | ||
| ACTIONS_ID_TOKEN_REQUEST_TOKEN: process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN, | ||
| }), |
|
@copilot address review feedback |
Implemented the requested review feedback in commit |
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
This comment has been minimized.
This comment has been minimized.
Smoke Test Results✅ GitHub MCP Testing - Listed 2 merged PRs Overall: PASS
|
This comment has been minimized.
This comment has been minimized.
|
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.
|
🏗️ Build Test Suite Results
Overall: 8/8 ecosystems passed — ✅ PASS
|
🧪 Chroot Version Comparison Results
Result: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot.
|
Smoke Test Results — FAIL
Overall: FAIL —
|
🧪 Smoke Test Results
Overall: PASS Author:
|
|
Smoke Test: Copilot BYOK (Offline) Mode
Running in BYOK offline mode ( Overall: PASS · Author:
|
Summary
Adds OIDC authentication support to the api-proxy sidecar, enabling BYOK mode with Azure OpenAI deployments that have API keys disabled (Entra-only authentication via workload identity federation).
Closes #2544
Upstream issue: github/gh-aw#30260
Problem
Azure OpenAI deployments following security best practices disable static API keys and require Entra ID (Azure AD) authentication only. The api-proxy sidecar previously only supported static API keys, making these deployments incompatible with BYOK mode.
Solution
Implements a reusable OIDC token provider that:
ACTIONS_ID_TOKEN_REQUEST_URL)Architecture
New files
containers/api-proxy/oidc-token-provider.js— Core OIDC token acquisition and cachingcontainers/api-proxy/oidc-token-provider.test.js— Unit tests (8 tests)Modified files
containers/api-proxy/providers/openai.js— AcceptsAWF_AUTH_TYPE=github-oidcas alternative to static keycontainers/api-proxy/server.js— Initializes OIDC providers on startup, cleans up on shutdowncontainers/api-proxy/Dockerfile— Includes new file in imagesrc/services/api-proxy-service.ts— Forwards OIDC env vars to sidecar containerConfiguration (gh-aw workflow .md)
gh-aw maps
engine.authto AWF env vars:auth.typeAWF_AUTH_TYPE=github-oidcAWF_AUTH_AZURE_TENANT_IDAWF_AUTH_AZURE_CLIENT_IDAWF_AUTH_OIDC_AUDIENCE(default:api://AzureADTokenExchange)AWF_AUTH_AZURE_SCOPE(default:https://cognitiveservices.azure.com/.default)AWF_AUTH_AZURE_CLOUD(public|usgovernment|china)Required firewall domains
When using OIDC auth, the workflow allowlist must include:
token.actions.githubusercontent.com— GitHub OIDC token endpointlogin.microsoftonline.com— Azure AD token exchangemy-resource.openai.azure.com)Testing