Skip to content

feat(api-proxy): GitHub OIDC → Azure AD workload identity federation auth#2562

Closed
Copilot wants to merge 6 commits intomainfrom
copilot/add-oidc-authentication-support
Closed

feat(api-proxy): GitHub OIDC → Azure AD workload identity federation auth#2562
Copilot wants to merge 6 commits intomainfrom
copilot/add-oidc-authentication-support

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

The api-proxy sidecar only supported static API keys, blocking Azure OpenAI deployments with Entra ID-only authentication (API keys disabled). This adds engine.auth block support via GitHub Actions OIDC federation.

New: containers/api-proxy/oidc-auth.js

OidcTokenManager handles the full token lifecycle:

  1. Fetch GitHub OIDC JWT from ACTIONS_ID_TOKEN_REQUEST_URL?audience=<AWF_AUTH_AUDIENCE>
  2. Exchange for Azure AD bearer token via client_credentials + client_assertion (federated identity grant) against login.microsoftonline.com/{tenant}/oauth2/v2.0/token
  3. Cache + proactively refresh 5 min before expiry; deduplicate concurrent refresh calls; back-off retry on failure

Modified: providers/openai.js

Accepts deps.oidcAuth (OidcTokenManager). getAuthHeaders() is now async—returns the Azure AD bearer token when OIDC is enabled, falls back to static OPENAI_API_KEY otherwise. Validation probe is skipped for OIDC adapters. getModelsFetchConfig() is now async in OIDC mode—it acquires a fresh token so that fetchStartupModels() populates cachedModels.openai, ensuring /reflect, models.json, and AWF_MODEL_ALIASES entries targeting openai/* work correctly.

Modified: server.js

createProviderServer request/upgrade handlers are now async, using await Promise.resolve(adapter.getAuthHeaders(req)) — backward-compatible with existing sync adapters. Returns 503 with a clear error when token acquisition fails. OIDC manager start() is awaited before binding listeners; stop() called on SIGTERM/SIGINT. fetchStartupModels() uses Promise.resolve(adapter.getModelsFetchConfig?.()) to transparently support both sync and async config implementations.

Modified: src/compose-generator.ts

Forwards the following env vars from host to the api-proxy container when set:

AWF_AUTH_TYPE=github-oidc
AWF_AUTH_AUDIENCE=api://AzureADTokenExchange   # default
AWF_AZURE_TENANT_ID=<tenant-id>
AWF_AZURE_CLIENT_ID=<client-id>
AWF_AZURE_SCOPE=https://cognitiveservices.azure.com/.default   # default
ACTIONS_ID_TOKEN_REQUEST_URL   # injected by Actions runner
ACTIONS_ID_TOKEN_REQUEST_TOKEN # injected by Actions runner

When AWF_AUTH_TYPE=github-oidc is set and enableApiProxy is true but no static openaiApiKey is configured (OIDC-only mode), the agent container also receives OPENAI_BASE_URL pointing 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:

  • Hostname from ACTIONS_ID_TOKEN_REQUEST_URL (e.g. pipelines.actions.githubusercontent.com)
  • login.microsoftonline.com
  • Your Azure OpenAI endpoint

Tests

60+ new tests across oidc-auth.test.js (32), server.test.js (+21 including async getAuthHeaders, async getModelsFetchConfig, 503-on-auth-error, and async fetchStartupModels adapter path), and docker-manager-compose.test.ts (+7 including OIDC env-var forwarding and OIDC-only agent routing).

Copilot AI changed the title [WIP] Add OIDC authentication for BYOK model provider in api-proxy feat(api-proxy): GitHub OIDC → Azure AD workload identity federation auth May 5, 2026
Copilot finished work on behalf of lpcox May 5, 2026 14:17
Copilot AI requested a review from lpcox May 5, 2026 14:17
@lpcox lpcox marked this pull request as ready for review May 5, 2026 18:28
@lpcox lpcox requested a review from Mossaka as a code owner May 5, 2026 18:28
Copilot AI review requested due to automatic review settings May 5, 2026 18:28
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 → 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 OidcTokenManager to 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.auth block in src/config-file.ts, src/types/config.ts, or src/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.com and the host from ACTIONS_ID_TOKEN_REQUEST_URL), but the existing preflight allowlist warning path is not updated for either one. A workflow that already has --enable-api-proxy configured 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

Comment thread src/compose-generator.ts
Comment on lines +1349 to +1357
...(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 }),
Comment on lines 117 to 119
getModelsFetchConfig() {
if (oidcEnabled) return null; // token not yet available at startup
if (!apiKey) return null;
@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented May 5, 2026

@copilot merge main and address the review feedback

@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.45% 86.55% 📈 +0.10%
Statements 86.39% 86.48% 📈 +0.09%
Functions 90.61% 90.61% ➡️ +0.00%
Branches 79.23% 79.41% 📈 +0.18%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/compose-generator.ts 90.4% → 90.5% (+0.11%) 90.1% → 90.2% (+0.12%)
src/container-lifecycle.ts 86.1% → 87.2% (+1.09%) 86.3% → 87.4% (+1.08%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

Comment thread containers/api-proxy/oidc-auth.test.js Outdated
* Unit tests for containers/api-proxy/oidc-auth.js
*/

const { createOidcTokenManager, OidcTokenManager, makeJsonRequest } = require('./oidc-auth');
Comment thread containers/api-proxy/oidc-auth.test.js Outdated
mgr._proxyAgent = undefined;

// Patch the module-level helper by monkey-patching the private method
const origFetch = mgr._fetchGitHubOidcToken.bind(mgr);
Comment thread containers/api-proxy/oidc-auth.test.js Outdated
token_type: 'Bearer',
};

const origExchange = mgr._exchangeForAzureToken.bind(mgr);
…entication-support

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@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

The OIDC implementation is well-structured overall. One defence-in-depth gap worth addressing:

Finding: makeJsonRequest does not enforce HTTPS before sending credentials

File: containers/api-proxy/oidc-auth.js, lines 59–111 and 282–303

// oidc-auth.js line 69-70
const isHttps = parsed.protocol === 'https:';
const mod = isHttps ? https : http;          // HTTP falls through silently

_fetchGitHubOidcToken() calls makeJsonRequest with ACTIONS_ID_TOKEN_REQUEST_URL and sends ACTIONS_ID_TOKEN_REQUEST_TOKEN as Authorization: Bearer <token>. If that env var were ever an `(redacted) URL — misconfig, self-hosted runner misconfiguration, or env-var injection on a compromised runner — the bearer token would travel in plaintext.

Suggested fix — add an explicit guard in _fetchGitHubOidcToken():

async _fetchGitHubOidcToken() {
  const url = new URL(this._oidcUrl);
  if (url.protocol !== 'https:') {
    throw new Error(`ACTIONS_ID_TOKEN_REQUEST_URL must use HTTPS, got: \$\{url.protocol}`);
  }
  url.searchParams.set('audience', this._audience);
  // ...
}

Severity: Low — GitHub's runner always provides an HTTPS endpoint and the Squid domain allowlist provides a second layer of control (an arbitrary HTTP endpoint would need to be explicitly allow-listed). However, an explicit check is cheap and eliminates a class of misconfiguration risk.


Everything else looks sound: container hardening (cap_drop: ALL, no-new-privileges) is preserved; auth errors return a generic 503 without leaking token content; the Azure AD URL uses encodeURIComponent for the tenant ID; concurrent refresh calls are deduplicated; and the Squid proxy ensures all outbound OIDC and Azure AD requests route through the domain allowlist.

Generated by Security Guard for issue #2562 · ● 181.5K ·

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 5, 2026

@copilot merge main and address the review feedback

Done in 93d636f. Merged main (picking up billing headers + X-Initiator injection from #2571) and addressed both review issues:

  1. Agent routing in OIDC-only mode: When AWF_AUTH_TYPE=github-oidc is set with no static openaiApiKey, the agent now receives OPENAI_BASE_URL pointing to the api-proxy sidecar plus placeholder keys (sk-placeholder-for-oidc-proxy), ensuring OpenAI traffic routes through the sidecar.

  2. Model list population in OIDC mode: getModelsFetchConfig() is now async — it acquires a fresh Azure AD token so fetchStartupModels() can populate cachedModels.openai. fetchStartupModels now uses Promise.resolve() to transparently support both sync and async implementations, keeping existing adapters unchanged. /reflect, models.json, and AWF_MODEL_ALIASES entries targeting openai/* now work correctly under OIDC.

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:

  • api.example.com
    • Triggering command: /opt/hostedtoolcache/node/20.20.2/x64/bin/node /opt/hostedtoolcache/node/20.20.2/x64/bin/node /home/REDACTED/work/gh-aw-firewall/gh-aw-firewall/containers/api-proxy/node_modules/jest-worker/build/processChild.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

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

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔥 Smoke Test: Copilot BYOK (Offline) Mode

Test Result
GitHub MCP (list PRs)
GitHub.com connectivity ⚠️ pre-step vars not expanded
File write/read ⚠️ pre-step vars not expanded
BYOK inference (agent → api-proxy → api.githubcopilot.com)

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

Overall: PASS (core BYOK path confirmed working)

PR by @Copilot · Assignees: @lpcox, @Copilot

🔑 BYOK report filed by Smoke Copilot BYOK

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Smoke Test Results

✅ GitHub MCP: Retrieved merged PRs

  • api-proxy: inject X-Initiator: agent default on all Copilot-bound requests to prevent billing inflation
  • refactor: extract shared parameterised test factory for log sub-commands

✅ Playwright: github.com page title verified
✅ File Writing: Test file created successfully
✅ Bash Tool: File verification passed

Overall Status: PASS

💥 [THE END] — Illustrated by Smoke Claude

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🔬 Smoke Test Results

Test Status
GitHub MCP connectivity
GitHub.com HTTP connectivity ⚠️ Pre-step data unavailable (template vars not expanded)
File write/read ⚠️ Pre-step data unavailable (template vars not expanded)

Overall: PARTIAL — MCP connectivity confirmed. Pre-computed test data (${{ steps.smoke-data.outputs.* }}) was not expanded at runtime.

PR by @Copilot · Assignees: @lpcox, @Copilot

📰 BREAKING: Report filed by Smoke Copilot

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Smoke Test: FAIL

✅ PR: api-proxy: inject X-Initiator: agent default on all Copilot-bound requests to prevent billing inflation
✅ PR: refactor: extract shared parameterised test factory for log sub-commands
❌ safeinputs-gh/github-discussion-query unavailable
✅ Playwright title contains GitHub
❌ Tavily search unavailable
✅ file + bash readback
✅ npm ci && npm run build
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

Chroot Smoke Test 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

Overall: ❌ Some versions do not match between host and chroot environment.

Tested by Smoke Chroot

@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 All passed ✅ PASS
Node.js execa All passed ✅ PASS
Node.js p-limit All passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Note (Java): Maven's default local repository at ~/.m2/repository was not writable due to directory ownership (root). Tests succeeded using -Dmaven.repo.local=/tmp/gh-aw/agent/m2-repo as a workaround.

Generated by Build Test Suite for issue #2562 · ● 459.7K ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Smoke Test Results — FAIL

Check Result
Redis PING ❌ No response (timeout)
PostgreSQL pg_isready ❌ No response
PostgreSQL SELECT 1 ❌ No response (timeout)

Overall: FAILhost.docker.internal is not reachable from this runner. Service containers may not be running or the hostname is not resolvable in this environment.

🔌 Service connectivity validated by Smoke Services

@github-actions github-actions Bot mentioned this pull request May 5, 2026
@lpcox
Copy link
Copy Markdown
Collaborator

lpcox commented May 5, 2026

@copilot merge main

…entication-support

# Conflicts:
#	containers/api-proxy/Dockerfile
#	containers/api-proxy/server.js
#	src/docker-manager-compose.test.ts
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 5, 2026

@copilot merge main

Done in fdce93d. Merged main (server.js refactored into sub-modules, test files split) and also fixed the three CodeQL unused-variable alerts in oidc-auth.test.js (OidcTokenManager, origFetch, origExchange). The OIDC tests were migrated from the deleted docker-manager-compose.test.ts to src/services/api-proxy-service.test.ts.

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:

  • api.example.com
    • Triggering command: /opt/hostedtoolcache/node/20.20.2/x64/bin/node /opt/hostedtoolcache/node/20.20.2/x64/bin/node /home/REDACTED/work/gh-aw-firewall/gh-aw-firewall/containers/api-proxy/node_modules/jest-worker/build/processChild.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of lpcox May 5, 2026 21:34
@lpcox lpcox closed this May 5, 2026
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