Repository navigation
[Bug]: restore Codex env_key bearer admission and direct-mode OpenAI credential substitution #1686
Description
Summary
/v1/responses rejects requests authenticated with Authorization: Bearer <token> even though that is exactly what the Codex CLI sends when a provider is injected with env_key (the runtime contract since codex-cli 0.146). resolveResponsesApiAuth only reads x-opencodex-api-key ("Dedicated header ONLY"), so any user who injects the proxy with env_key — the modern runtime's preferred form — gets 401 authentication_error: opencodex API key required on every model, including native OpenAI passthrough.
This is inconsistent with the rest of the proxy: resolveApiAuth (the non-responses path) already accepts authorization as a fallback, and the grok injector already emits env_key.
This issue also owns the directly coupled failure previously tracked in #1687: after an env_key admission bearer is accepted, codexAccountMode: "direct" must replace the proxy admission credential with the stored main ChatGPT/OpenAI credential before forwarding upstream. The two failures are one end-to-end auth contract and must be fixed and tested together.
Reproduction
# 1. Inject the proxy with env_key (codex-cli 0.146+ contract)
# [model_providers.opencodex]
# base_url = "http://127.0.0.1:10100/v1"
# env_key = "OPENCODEX_API_AUTH_TOKEN"
# 2. Start the proxy with OPENCODEX_API_AUTH_TOKEN exported, then request:
curl -X POST http://127.0.0.1:10100/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENCODEX_API_AUTH_TOKEN" \
-d '{"model":"gpt-5.6-luna","input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}],"stream":true,"store":false}'Logs and screenshots
Admission failure:
{"error":{"message":"opencodex API key required","type":"authentication_error","code":"invalid_api_key"}}The same request with x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN succeeds (200), proving the token is valid — only the header is rejected.
After bearer admission is accepted, direct-mode forwarding can instead fail with the second half of this contract:
{"error":{"message":"OpenCodex admission credentials cannot be forwarded upstream","type":"invalid_request_error","code":"invalid_request_error"}}Area
Proxy runtime / Codex integration / direct OpenAI forwarding
Version
2.14.2 (also present on the affected main/dev lineage when reported)
OS
Ubuntu 24.04 (VPS, root)
Config shape
{
"providers": {
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"codexAccountMode": "direct",
"authMode": "forward"
}
}
}Root cause
There are two sequential failures in the same env_key flow.
1. Responses admission rejects the bearer form
src/server/auth-cors.ts resolveResponsesApiAuth only accepts the dedicated OpenCodex header:
const actual = req.headers.get("x-opencodex-api-key")?.trim();
if (!actual) return null;Meanwhile resolveApiAuth already accepts Authorization: Bearer as a fallback, and modern Codex env_key injection populates that standard bearer header.
2. Direct-mode forwarding must substitute the admission bearer
Once the caller is admitted, the same bearer is a proxy admission credential and must never be forwarded to ChatGPT/OpenAI. In codexAccountMode: "direct", when a stored main Codex account exists, the forwarding path must replace it with the real account access token and chatgpt-account-id.
The regression tracked in former #1687 was that validateForwardAdmissionCredential rejected the admission bearer unconditionally while the ctx.kind === "main" substitution path had also been removed. That makes a correct env_key request fail even after admission is fixed.
Required fix
Treat this as one end-to-end contract:
resolveResponsesApiAuthacceptsAuthorization: Bearer <OPENCODEX_API_AUTH_TOKEN>as an admission fallback while preserving the dedicated header path.- Codex injection uses the supported
env_keycontract, with compatibility handling for older runtimes where required. - For direct-mode OpenAI forwarding, an admitted proxy bearer is never sent upstream.
- If a stored main ChatGPT/OpenAI account is available, replace the admission bearer with the stored access token and the matching
chatgpt-account-id. - If substitution is impossible, remain fail-closed and reject the request rather than leaking the admission credential.
- Pool mode and non-bearer dedicated-header flows remain unchanged.
Security invariant
Accepting the bearer for proxy admission does not mean it may be forwarded upstream. Admission validation and upstream authentication are separate credential domains.
The proxy must therefore preserve both properties:
- accept the modern Codex
env_keybearer when its value matches the configured OpenCodex admission token; - never forward that admission token to ChatGPT/OpenAI, substituting a real stored account credential only when one is available.
Acceptance matrix
x-opencodex-api-keywith the valid admission secret -> admitted, unchanged.Authorization: Bearer <valid admission secret>-> admitted.- no valid admission key where auth is required -> 401.
- direct mode + admission bearer + main account present -> upstream receives the stored ChatGPT/OpenAI credential, not the admission token.
- direct mode + admission bearer + no usable main account -> fail closed with no credential leak.
- pool mode -> existing pool credential override remains unchanged.
- dedicated-header admission -> existing forwarding behavior remains unchanged.
- regression coverage proves the OpenCodex admission secret cannot appear in upstream request headers.
Alternatives considered
- Keep
env_http_headersand do nothing: breaks modern Codexenv_keyusers. - Fix only admission: still leaves direct-mode requests failing at the forward-credential guard.
- Fix only forwarding: requests never reach that stage when
/v1/responsesrejects the bearer. - Forward the admission bearer as-is: invalid and unsafe because the OpenCodex admission secret is not an upstream ChatGPT/OpenAI credential.
Related
- [Bug]: ForwardAdmissionCredentialError blocks direct-mode OpenAI forwarding when caller authenticates with the proxy admission token #1687 — merged into this issue and closed as a duplicate because it is the second stage of the same
env_keybearer flow.
Activity
coderabbitai commentedon Aug 14, 2026
🔗 Related PRs
#496 - fix(auth): support standard OpenAI Authorization and x-api-key headers in remote mode [open]
#749 - fix(responses): reject expired forward continuations [merged]
#1020 - fix(anthropic): surface upstream error bodies through sidecar bridges [merged]
#1471 - fix(auth): pin forwarded Codex credentials to ChatGPT [merged]
#1755 - fix(claude): authenticate gateway model cache refresh [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
[-][Bug]: /v1/responses rejects Authorization: Bearer when provider is injected with env_key (codex-cli >= 0.146)[/-][+][Bug]: restore Codex env_key bearer admission and direct-mode OpenAI credential substitution[/+]Wibias commentedon Aug 16, 2026
Merged #1687 into this issue because the two reports are sequential stages of the same Codex
env_keyauthentication contract. Admission must first accept the proxy token fromAuthorization: Bearer; direct-mode forwarding must then replace that admission credential with the stored ChatGPT/OpenAI account credential rather than rejecting or forwarding the proxy secret. The title, scope, security invariant, and acceptance matrix now cover both stages end-to-end.lidge-jun commentedon Aug 16, 2026
Groundwork landed on
devby #1853 (merged as the commit above). Keeping this open for the remaining half.What landed. The blocker was structural:
DataPlaneAdmissionnamed which credential matched but not how it was presented, so "a proxy secret arriving as a bearer on the Responses transport" could not be expressed — the transport rejected every bearer, which is why a client configured withenv_keycould not reach Direct at all.DataPlaneAdmissionnow carriessource: loopback | dedicated | bearer | x-api-key.x-api-keyis still refused there, and a bearer that is not our secret stays unadmitted — so Codex Direct passthrough is untouched and the two bearer domains still never mix.materializeCodexUpstreamAuthis now the single place the two credential domains meet. Pool and main-pool overwrite as before;mainwith an admission bearer substitutes the stored main credential and throwsCodexMainSubstitutionUnavailableErrorbefore any upstream I/O when none is usable;mainwith a dedicated header keeps the existing intentional passthrough.One correction to the original report worth recording. I could not reproduce an admission secret actually reaching an upstream at this SHA: Direct rejects a recognized proxy bearer before upstream auth resolution, pool/main-pool overwrite it, and non-forward adapters install their own credential. Only canonical
authMode: "forward"forwards a caller bearer, which is intentional. The real defect is the inverse — the proxy refused the flow you wanted instead of admitting it and substituting. That is why the fix widens admission and adds guaranteed substitution together; widening alone would have created the leak the forward guard prevents.What is still open:
handleResponses.env_keyfrom injection — today it writes onlyenv_http_headers.Both are documented at
devlog/_plan/260816_wave34_closeout/100_1686_admission.md. They are deliberately separate commits so the boundary change could be reviewed on its own.lidge-jun commentedon Aug 16, 2026
fix(auth): serve env_key bearer admission on Direct by substituting s…
feat(auth): admit an admission bearer and substitute the stored main …
mkarolczyk commentedon Aug 17, 2026
Still reproducible on released 2.24.0 with the default loopback bind
I can confirm that the Direct-mode path described here is still broken in the released
2.24.0.Environment:
2.24.0127.0.0.1:10100api: openai-responsesAuthorization: Bearer <local OpenCodex admission value>authMode: "forward"andcodexAccountMode: "direct"during the testThe request repeatedly fails before upstream execution. A representative
usage.jsonlentry contains:{ "provider": "openai", "requestedModel": "gpt-5.6-luna", "admissionKind": "loopback", "inboundProtocol": "responses", "status": 401, "errorCode": "invalid_api_key", "upstreamError": "OpenCodex admission credentials cannot be forwarded upstream" }This matches the unresolved P1 review finding in #1861: on the default loopback bind,
resolveResponsesApiAuth()returns{ kind: "loopback", source: "loopback" }before inspecting the presented bearer. Consequently,substituteMainCredentialremains false and Direct reachesvalidateForwardAdmissionCredential().The distributed 2.24.0 source still contains this ordering in
src/server/auth-cors.ts, whilesrc/server/responses/core.tssubstitutes only whenadmission?.source === "bearer".Could this issue be reopened, or should I create a focused follow-up for the loopback case? A regression test should cover default
127.0.0.1+ Direct + anAuthorizationadmission bearer, not only a bind where API authentication is required.Related review finding: #1861 (comment)
fix(codex): emit modern env_key admission line on non-loopback inject (…
fix(responses): stop requiring a ChatGPT credential for routed providers
fix(windows): scope fixture ownership and finish the shard-only test …
fix(windows): scope fixture ownership and finish the shard-only test …
fix(windows): await the native-main gate before asserting admission
fix(windows): unblock the isolated fabric producer and unpin CI-scale…
fix(windows): unblock the isolated fabric producer and unpin CI-scale…
feat(auth): admit an admission bearer and substitute the stored main …
fix(auth): serve env_key bearer admission on Direct by substituting s…
fix(codex): emit modern env_key admission line on non-loopback inject (…
fix(responses): stop requiring a ChatGPT credential for routed providers
fix(windows): unblock the isolated fabric producer and unpin CI-scale…