diff --git a/src/codex/auth-context.ts b/src/codex/auth-context.ts index 71a79b1b67..fa714bcbbf 100644 --- a/src/codex/auth-context.ts +++ b/src/codex/auth-context.ts @@ -576,11 +576,13 @@ export class CodexMainSubstitutionUnavailableError extends Error { * Silently forwarding would be the leak validateForwardAdmissionCredential exists to prevent. * - `main` with a dedicated-header caller keeps the existing intentional passthrough: the bearer * there is the user's own ChatGPT credential, not ours. + * - `main` on a non-Codex route strips a bearer used for OpenCodex admission, because routed + * adapters may otherwise interpret that forwarded header as their own upstream credential. */ export function materializeCodexUpstreamAuth( headers: Headers, ctx: CodexAuthContext, - options: { substituteMainCredential?: boolean } = {}, + options: { substituteMainCredential?: boolean; stripAuthorization?: boolean } = {}, ): Headers { const selected = new Headers(); for (const name of FORWARD_HEADERS) { @@ -602,6 +604,7 @@ export function materializeCodexUpstreamAuth( if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId); return selected; } + if (options.stripAuthorization === true) selected.delete("authorization"); return selected; } diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 3c7773e4e4..30c6d9a34e 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -1201,10 +1201,18 @@ async function resolveResponsesCodexAuth( response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"), }; } + // A bearer admission proves access with one of OpenCodex's own secrets. Routes that do not + // consume a stored Codex credential still need the caller's admission Authorization removed: + // some adapters intentionally use forwarded Authorization as their provider credential. + // Dedicated-header admission remains unchanged because its Authorization belongs upstream. + const stripAuthorization = options.admission?.source === "bearer" && !substituteMainCredential; return { ok: true, authCtx, - headers: materializeCodexUpstreamAuth(req.headers, authCtx, { substituteMainCredential }), + headers: materializeCodexUpstreamAuth(req.headers, authCtx, { + substituteMainCredential, + stripAuthorization, + }), }; } catch (err) { if (err instanceof CodexAccountCooldownError) { diff --git a/tests/codex-auth-context.test.ts b/tests/codex-auth-context.test.ts index b4e9f77d86..c2ea5f6180 100644 --- a/tests/codex-auth-context.test.ts +++ b/tests/codex-auth-context.test.ts @@ -787,6 +787,21 @@ describe("Codex auth context", () => { ); expect(headers.get("authorization")).toBe("Bearer user_chatgpt_token"); }); + + test("a routed-provider admission bearer is removed while other headers remain", () => { + const headers = materializeCodexUpstreamAuth( + new Headers({ + authorization: "Bearer ocx_data_localsecret", + "openai-beta": "responses=experimental", + }), + { kind: "main", accountId: null }, + { stripAuthorization: true }, + ); + + expect(headers.has("authorization")).toBe(false); + expect(headers.get("openai-beta")).toBe("responses=experimental"); + }); + test("selected pool headers replace inbound main auth", () => { const headers = headersForCodexAuthContext( new Headers({ authorization: "Bearer main_token", "chatgpt-account-id": "main_acc", "openai-beta": "responses=experimental" }),