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
34 changes: 33 additions & 1 deletion containers/api-proxy/copilot-adapter-enterprise.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,30 @@ describe('createCopilotAdapter — GHE enterprise auth format', () => {
expect(headers['Authorization']).toBe('token ghu_enterprise_token_123');
});

it('uses "Bearer" when AWF_PLATFORM_TYPE=github.com overrides GHES-looking GITHUB_SERVER_URL', () => {
it('uses "token" when auto-derived target is api.enterprise even with AWF_PLATFORM_TYPE=github.com', () => {
// With GITHUB_SERVER_URL pointing to a non-ghe.com host, deriveCopilotApiTarget
// auto-derives to api.enterprise.githubcopilot.com — a catalog endpoint that
// always requires the 'token' prefix. AWF_PLATFORM_TYPE cannot override catalog
// endpoints (it only overrides the GHES heuristic for custom/unknown targets).
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_token_123',
AWF_PLATFORM_TYPE: 'github.com',
GITHUB_SERVER_URL: 'https://ghes.mycompany.com',
});
const headers = adapter.getAuthHeaders(fakeReq);
expect(headers['Authorization']).toBe('token ghu_token_123');
});

it('uses "Bearer" when AWF_PLATFORM_TYPE=github.com overrides GHES heuristic for a custom proxy', () => {
// AWF_PLATFORM_TYPE=github.com does suppress the 'token' prefix for
// custom/unknown targets (not catalog endpoints), e.g. a custom proxy host.
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_token_123',
COPILOT_API_TARGET: 'custom-proxy.internal',
AWF_PLATFORM_TYPE: 'github.com',
GITHUB_SERVER_URL: 'https://ghes.mycompany.com',
});
const headers = adapter.getAuthHeaders(fakeReq);
expect(headers['Authorization']).toBe(bearerGithubComOverrideToken);
});
});
Expand Down Expand Up @@ -182,6 +199,21 @@ describe('createCopilotAdapter — Copilot Business auth format', () => {
expect(headers['Authorization']).toBe('token ghu_business_token_123');
expect(headers['Authorization']).not.toContain('token token');
});

it('uses "token" prefix for Business target when AWF_PLATFORM_TYPE=ghec is set (regression #5871)', () => {
// Regression: gh-aw automatically sets AWF_PLATFORM_TYPE=ghec on *.ghe.com
// runners. Previously this caused the 'token' prefix to be suppressed for
// COPILOT_API_TARGET=api.business.githubcopilot.com, yielding
// "400 bad request: Authorization header is badly formatted".
const adapter = createCopilotAdapter({
COPILOT_GITHUB_TOKEN: 'ghu_business_token_123',
COPILOT_API_TARGET: 'api.business.githubcopilot.com',
AWF_PLATFORM_TYPE: 'ghec',
GITHUB_SERVER_URL: 'https://myorg.ghe.com',
});
const headers = adapter.getAuthHeaders(fakeReq);
expect(headers['Authorization']).toBe('token ghu_business_token_123');
});
});

describe('createCopilotAdapter — Azure OIDC (Entra) getAuthHeaders', () => {
Expand Down
28 changes: 25 additions & 3 deletions containers/api-proxy/copilot-auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,33 @@ describe('copilotTargetRequiresGitHubTokenPrefix', () => {
})).toBe(true);
});

it('returns false when an explicit non-GHES AWF_PLATFORM_TYPE overrides the Business host', () => {
// The explicit platform override is the documented escape hatch and wins
// even for the Business host, matching isGhesInstance semantics.
it('returns true for the Business host even when AWF_PLATFORM_TYPE=ghec is set', () => {
// Regression: gh-aw automatically sets AWF_PLATFORM_TYPE=ghec on *.ghe.com
// (GHEC) runners. The catalog endpoint check must take priority over the
// platform-type guard so that Copilot Business customers who set
// COPILOT_API_TARGET=api.business.githubcopilot.com still receive the
// required 'token' prefix and not 'Bearer'. See github/gh-aw-firewall#5871.
expect(copilotTargetRequiresGitHubTokenPrefix('api.business.githubcopilot.com', {
AWF_PLATFORM_TYPE: 'ghec',
})).toBe(true);
});

it('returns true for Business host with AWF_PLATFORM_TYPE=ghec and *.ghe.com server (full production scenario)', () => {
// Full production scenario from issue #5871: Copilot Business on GHEC.
// COPILOT_API_TARGET=api.business.githubcopilot.com, GITHUB_SERVER_URL=*.ghe.com,
// AWF_PLATFORM_TYPE=ghec (auto-injected by gh-aw on GHEC runners).
expect(copilotTargetRequiresGitHubTokenPrefix('api.business.githubcopilot.com', {
AWF_PLATFORM_TYPE: 'ghec',
GITHUB_SERVER_URL: 'https://myorg.ghe.com',
})).toBe(true);
});

it('returns false when AWF_PLATFORM_TYPE=github.com for a non-catalog custom target', () => {
// The platform override should still suppress the GHES heuristic for
// custom/unknown proxy targets (only catalog endpoints are unaffected).
expect(copilotTargetRequiresGitHubTokenPrefix('custom-proxy.internal', {
AWF_PLATFORM_TYPE: 'github.com',
GITHUB_SERVER_URL: 'https://ghes.mycompany.com',
})).toBe(false);
});

Expand Down
34 changes: 21 additions & 13 deletions containers/api-proxy/providers/copilot-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,17 @@ const GITHUB_TOKEN_PREFIX_COPILOT_TARGETS = new Set([
* `token <value>` Authorization prefix instead of `Bearer <value>`.
*
* This is true when either:
* 1. The environment is a GHES instance (see {@link isGhesInstance}), or
* 2. The resolved target is a GitHub-hosted Copilot endpoint that accepts the
* GitHub token directly — i.e. the Enterprise or Business host. This case
* covers Copilot Business customers who set
* COPILOT_API_TARGET=api.business.githubcopilot.com while running against a
* *.ghe.com (GHEC) server, which the GHES heuristics would otherwise miss.
* 1. The resolved target is a known GitHub-hosted Copilot endpoint that
* authenticates the GitHub token directly — i.e. the Enterprise or Business
* host. This check takes highest priority and is NOT overridable by
* AWF_PLATFORM_TYPE. Without this ordering, gh-aw's automatic
* AWF_PLATFORM_TYPE=ghec injection on *.ghe.com runners would suppress the
* `token` prefix for Copilot Business customers who set
* COPILOT_API_TARGET=api.business.githubcopilot.com.
* 2. The environment is a GHES instance (see {@link isGhesInstance}).
*
* An explicit non-GHES AWF_PLATFORM_TYPE remains an override that forces
* 'Bearer', consistent with {@link isGhesInstance}.
* An explicit non-GHES AWF_PLATFORM_TYPE overrides the GHES heuristics (case 2)
* but never overrides known catalog endpoints (case 1).
*
* BYOK API keys always use `Bearer`; this predicate only governs the GitHub
* token case (callers gate on the absence of a real BYOK key).
Expand All @@ -270,14 +272,20 @@ const GITHUB_TOKEN_PREFIX_COPILOT_TARGETS = new Set([
* @returns {boolean}
*/
function copilotTargetRequiresGitHubTokenPrefix(resolvedTarget, env = process.env) {
// An explicit non-GHES platform type is an intentional override that forces
// the standard 'Bearer' prefix, even when the resolved host looks like an
// Enterprise/Business endpoint. (isGhesInstance honors the same override.)
// Known GitHub-hosted Copilot endpoints always require the 'token' prefix for
// GitHub OAuth/PAT credentials, regardless of platform type. This check must
// come before the AWF_PLATFORM_TYPE guard so that an explicit platform type
// (e.g. AWF_PLATFORM_TYPE=ghec set by gh-aw on *.ghe.com runners) does not
// suppress the required 'token' prefix for Business/Enterprise endpoints.
const target = normalizeApiTarget(resolvedTarget);
if (target && GITHUB_TOKEN_PREFIX_COPILOT_TARGETS.has(target)) return true;

// An explicit non-GHES platform type overrides the GHES heuristics below
// for custom/unknown targets but never overrides catalog endpoints (above).
if (env.AWF_PLATFORM_TYPE && env.AWF_PLATFORM_TYPE !== 'ghes') return false;

if (isGhesInstance(resolvedTarget, env)) return true;
const target = normalizeApiTarget(resolvedTarget);
return target ? GITHUB_TOKEN_PREFIX_COPILOT_TARGETS.has(target) : false;
return false;
}

module.exports = {
Expand Down
Loading