Skip to content

feat(api-proxy): OIDC authentication for Azure OpenAI (Entra-only)#2599

Merged
lpcox merged 2 commits intomainfrom
copilot/oidc-auth-api-proxy
May 5, 2026
Merged

feat(api-proxy): OIDC authentication for Azure OpenAI (Entra-only)#2599
lpcox merged 2 commits intomainfrom
copilot/oidc-auth-api-proxy

Conversation

@lpcox
Copy link
Copy Markdown
Collaborator

@lpcox lpcox commented May 5, 2026

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:

  1. Mints a GitHub Actions OIDC token (via ACTIONS_ID_TOKEN_REQUEST_URL)
  2. Exchanges it for an Azure AD access token via workload identity federation
  3. Caches the token and proactively refreshes before expiry (~75% of lifetime)
  4. Serves tokens synchronously to the existing adapter interface (no async refactor needed)

Architecture

GitHub Actions Runner
  → OIDC Token (audience: api://AzureADTokenExchange)
    → Azure AD Token Exchange (client credentials + JWT assertion)
      → Azure AD Access Token (scope: cognitiveservices.azure.com)
        → Injected as Authorization header on upstream requests

New files

  • containers/api-proxy/oidc-token-provider.js — Core OIDC token acquisition and caching
  • containers/api-proxy/oidc-token-provider.test.js — Unit tests (8 tests)

Modified files

  • containers/api-proxy/providers/openai.js — Accepts AWF_AUTH_TYPE=github-oidc as alternative to static key
  • containers/api-proxy/server.js — Initializes OIDC providers on startup, cleans up on shutdown
  • containers/api-proxy/Dockerfile — Includes new file in image
  • src/services/api-proxy-service.ts — Forwards OIDC env vars to sidecar container

Configuration (gh-aw workflow .md)

permissions:
  id-token: write

engine:
  env:
    COPILOT_PROVIDER_BASE_URL: https://my-resource.openai.azure.com
    COPILOT_PROVIDER_TYPE: azure
    COPILOT_MODEL: gpt-4.1
  auth:
    type: github-oidc
    audience: https://cognitiveservices.azure.com

gh-aw maps engine.auth to AWF env vars:

Workflow field AWF env var
auth.type AWF_AUTH_TYPE=github-oidc
Azure tenant ID AWF_AUTH_AZURE_TENANT_ID
Azure client ID AWF_AUTH_AZURE_CLIENT_ID
OIDC audience AWF_AUTH_OIDC_AUDIENCE (default: api://AzureADTokenExchange)
Azure scope AWF_AUTH_AZURE_SCOPE (default: https://cognitiveservices.azure.com/.default)
Azure cloud 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 endpoint
  • login.microsoftonline.com — Azure AD token exchange
  • The Azure OpenAI endpoint (e.g., my-resource.openai.azure.com)

Testing

  • 8 new unit tests covering token acquisition, failure handling, sovereign clouds, adapter integration
  • All 589 existing api-proxy tests continue to pass
  • All 84 api-proxy-service tests pass
  • Lint passes with no new errors

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>
@lpcox lpcox requested a review from Mossaka as a code owner May 5, 2026 22:48
Copilot AI review requested due to automatic review settings May 5, 2026 22:48
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

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

Comment thread containers/api-proxy/oidc-token-provider.test.js Fixed
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Security Review

Finding 1: _httpGet bypasses Squid proxy — inconsistent with _httpPost

File: containers/api-proxy/oidc-token-provider.js:261-273

// _httpGet uses https.get() directly — no proxy routing
const mod = parsedUrl.protocol === 'https:' ? https : http;
const req = mod.get(url, { headers }, (res) => { ... });

Contrast with _httpPost (lines 294–326) which explicitly checks HTTP_PROXY/HTTPS_PROXY and implements proxy CONNECT for HTTPS. _httpGet — used to mint the GitHub OIDC JWT — makes direct outbound HTTPS calls from the api-proxy container, bypassing Squid entirely.

Consequence: token.actions.githubusercontent.com is reachable from the api-proxy sidecar without being in the domain allowlist, contradicting the PR description's stated requirement and the sidecar's design intent (all upstream traffic should go through Squid). It also creates an inconsistent egress path for HTTPS GET requests from the trusted sidecar.

Suggested fix: Apply the same proxy-CONNECT logic from _httpPost to _httpGet, or extract a shared _httpsRequest helper that routes through HTTP_PROXY/HTTPS_PROXY unconditionally.


Finding 2: Misleading fallback header when OIDC token unavailable

File: containers/api-proxy/providers/openai.js:90-98

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  ← incorrect
  return { 'Authorization': 'Bearer oidc-token-unavailable' };
}

isEnabled() returns !!apiKey || oidcEnabled (line 77). Since oidcEnabled is true whenever an OIDC provider is configured, isEnabled() never returns false during a token outage. Requests are forwarded to Azure with a literal oidc-token-unavailable Bearer token, producing 401s at the upstream instead of 503s at the proxy. The comment is wrong and the behavior masks token acquisition failures from callers.

Suggested fix: When getToken() returns null, explicitly return a 503 in the request handler (e.g., via a new isTokenAvailable() adapter method) so callers get a clear signal rather than an opaque upstream 401.

Generated by Security Guard for issue #2599 · ● 153.6K ·

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 OidcTokenProvider that 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-oidc and 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, but fetchStartupModels() only fetches models via getModelsFetchConfig(). As a result, OpenAI/Azure models will never be cached/populated in models.json for OIDC deployments, even after provider.initialize() completes. Consider returning a models fetch config in OIDC mode once oidcProvider.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

Comment on lines 77 to 99
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}` };
Comment on lines 88 to +98
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' };
}
Comment on lines +220 to +223
// Schedule proactive refresh
const refreshInSecs = Math.max(
expires_in * REFRESH_FACTOR,
expires_in - MIN_REFRESH_MARGIN_SECS
Comment on lines +261 to +272
_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')); });
});
Comment on lines +284 to +291
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) },
};
Comment on lines +113 to +119
// 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,
}),
@lpcox
Copy link
Copy Markdown
Collaborator Author

lpcox commented May 5, 2026

@copilot address review feedback

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

@copilot address review feedback

Implemented the requested review feedback in commit 8ce7a7d: fixed OIDC readiness gating and 503 behavior in OpenAI adapter, removed api-key injection for OIDC bearer tokens, corrected refresh scheduling math, updated OIDC GET/POST requests to honor proxy env vars, fixed protocol-aware default POST port, normalized AWF_AUTH_TYPE for forwarding Actions OIDC runtime env vars, removed the unused test variable, and added targeted tests for these paths.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

✅ Coverage Check Passed

Overall Coverage

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.46% ➡️ +0.01%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/services/api-proxy-service.ts 98.4% → 98.4% (+0.02%) 98.4% → 98.4% (+0.02%)
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

Copilot finished work on behalf of lpcox May 5, 2026 23:06
@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Smoke Test Results

✅ GitHub MCP Testing - Listed 2 merged PRs
✅ Playwright Testing - GitHub page title verified
✅ File Writing - Test file created
✅ Bash Tool Testing - File verified

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

api-proxy: inject X-Initiator: agent default on all Copilot-bound requests to prevent billing inflation
refactor(entrypoint): deduplicate signal-handler + token-cleanup into shared function
GitHub PR review: ✅
Safe Inputs GH CLI: ❌
Playwright title check: ✅
Tavily search: ❌
File/bash/build/discussion: ✅
Overall status: FAIL

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • registry.npmjs.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "registry.npmjs.org"

See Network Configuration for more information.

🔮 The oracle has spoken through Smoke Codex

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx passed ✅ PASS
Node.js execa passed ✅ PASS
Node.js p-limit passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Generated by Build Test Suite for issue #2599 · ● 412.7K ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧪 Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3
Node.js v24.14.1 v20.20.2
Go go1.22.12 go1.22.12

Result: ❌ Not all tests passed — Python and Node.js versions differ between host and chroot.

Tested by Smoke Chroot

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Smoke Test Results — FAIL

Check Result
Redis PING ❌ Timeout/no response
PostgreSQL pg_isready ❌ No response on port 5432
PostgreSQL SELECT 1 ❌ Timeout/no response

Overall: FAILhost.docker.internal services unreachable from this environment.

🔌 Service connectivity validated by Smoke Services

@github-actions github-actions Bot mentioned this pull request May 5, 2026
@lpcox lpcox merged commit e26e9de into main May 5, 2026
63 of 68 checks passed
@lpcox lpcox deleted the copilot/oidc-auth-api-proxy branch May 5, 2026 23:24
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🧪 Smoke Test Results

Test Result
GitHub MCP ✅ PR: "feat(api-proxy): OIDC authentication for Azure OpenAI (Entra-only)"
GitHub.com HTTP ✅ 200
File write/read smoke-test-copilot-25410852238.txt verified

Overall: PASS

Author: @lpcox

📰 BREAKING: Report filed by Smoke Copilot

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Smoke Test: Copilot BYOK (Offline) Mode

Test Result
GitHub MCP (list PRs)
GitHub.com connectivity ✅ HTTP 200
File write/read ⚠️ Template vars unresolved — skipped
BYOK inference (this response)

Running in BYOK offline mode (COPILOT_OFFLINE=true) via api-proxy → api.githubcopilot.com

Overall: PASS · Author: @lpcox · Reviewers: @Mossaka

🔑 BYOK report filed by Smoke Copilot BYOK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[awf] api-proxy: OIDC authentication for BYOK model provider (engine.auth)

4 participants