Repository navigation
[Bug]: ForwardAdmissionCredentialError blocks direct-mode OpenAI forwarding when caller authenticates with the proxy admission token #1687
Description
Summary
In direct mode (codexAccountMode: "direct"), forwarding to the ChatGPT/OpenAI upstream fails with ForwardAdmissionCredentialError whenever the caller authenticates with the proxy's own admission token via Authorization: Bearer (the env_key contract). The guard validateForwardAdmissionCredential rejects the admission bearer unconditionally on current main, even though the proxy has a main Codex account token available to substitute.
This regressed in the 2.14.x WebSocket refactor: the 2.12-era guard if (getMainAccountToken()) return; (which allowed the forward when a substitution is possible) was removed together with the ctx.kind === "main" substitution block in headersForCodexAuthContext.
Reproduction
# config.toml:
# [model_providers.opencodex]
# base_url = "http://127.0.0.1:10100/v1"
# env_key = "OPENCODEX_API_AUTH_TOKEN"
# config.json:
# "providers": { "openai": { "codexAccountMode": "direct", "authMode": "forward" } }
# ~/.codex/auth.json contains a main ChatGPT account (getMainAccountToken() non-null)
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
{"error":{"message":"OpenCodex admission credentials cannot be forwarded upstream","type":"invalid_request_error","code":"invalid_request_error"}}Area
Proxy runtime
Version
2.14.2 (also on current main)
OS
Ubuntu 24.04 (VPS)
Config shape
{
"providers": {
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"authMode": "forward",
"codexAccountMode": "direct"
}
}
}Root cause
src/server/auth-cors.ts validateForwardAdmissionCredential (current main):
export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
}In direct mode, resolveCodexAuthContext returns { kind: "main" }, and headersForCodexAuthContext no longer substitutes the admission bearer with the real ChatGPT token (getMainAccountToken() from ~/.codex/auth.json) — the ctx.kind === "main" block was removed in the WebSocket refactor. The 2.12 version of this guard was:
if (!bearer || !isProxyAdmissionSecret(bearer, config)) return;
if (getMainAccountToken()) return; // substitution happens in headersForCodexAuthContext
throw new ForwardAdmissionCredentialError();Proposed fix
- Restore the guard in
validateForwardAdmissionCredential:
export function validateForwardAdmissionCredential(headers: Headers, config: OcxConfig): void {
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
- if (bearer && isProxyAdmissionSecret(bearer, config)) throw new ForwardAdmissionCredentialError();
+ if (bearer && isProxyAdmissionSecret(bearer, config)) {
+ // Safe to forward-substitute only when a main account token exists
+ // (headersForCodexAuthContext performs the substitution).
+ if (getMainAccountToken()) return;
+ throw new ForwardAdmissionCredentialError();
+ }
}- Restore the
ctx.kind === "main"block inheadersForCodexAuthContext(src/codex/auth-context.ts) that replaces the admission bearer withBearer ${main.accessToken}+chatgpt-account-idwhenbearer === process.env.OPENCODEX_API_AUTH_TOKEN.
Alternatives considered
- Keep rejecting unconditionally and require clients to send the real ChatGPT token: breaks the
env_keyinjection contract and the whole point ofcodexAccountMode: "direct"with a proxy-managed main account. Not viable. - Only document the limitation: the regression is a behavior change from 2.12; should be fixed, not documented.
Additional context
This mirrors how other gateways solve the same problem: the OmniRoute ChatGPT executor uses the stored session token plus a chatgpt-account-id header to route to the real account (open-sse/executors/chatgpt-web.ts:3055 — if (tokenEntry.accountId) headers["chatgpt-account-id"] = tokenEntry.accountId;). Substituting the caller's admission credential with the pool/main account credential is the established pattern; the proxy just needs to keep doing it in the WS path.
Security note: without a main account token the guard still throws (fail-closed), so the change only relaxes the check when a substitution is actually possible.
Test matrix:
directmode + admission bearer + main account present → 200 (token substituted upstream)directmode + admission bearer + no main account → 401 (fail-closed, unchanged)poolmode → unchanged (pool accessToken override)x-opencodex-api-keyauth (non-bearer) → unchanged (no admission-bearer path)
Activity
coderabbitai commentedon Aug 14, 2026
🔗 Related PRs
#496 - fix(auth): support standard OpenAI Authorization and x-api-key headers in remote mode [open]
#671 - feat(codex): add exact account routing [merged]
#749 - fix(responses): reject expired forward continuations [merged]
#1358 - fix(claude): contain main credentials to canonical forward routes [merged]
#1471 - fix(auth): pin forwarded Codex credentials to ChatGPT [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!
Wibias commentedon Aug 16, 2026
Closing this as a duplicate of #1686.
The two reports are not independent fixes. #1687 is the second stage of the same
env_keybearer flow tracked by #1686: once/v1/responsesaccepts the OpenCodex admission token fromAuthorization: Bearer, direct-mode OpenAI forwarding must replace that admission credential with the stored ChatGPT/OpenAI account credential. Fixing only #1686's admission check would still fail here, while fixing only this forwarding path would not help because the request is rejected earlier.I merged this issue's root cause, security constraint, direct-mode substitution requirements, and regression matrix into #1686 so there is now one canonical end-to-end issue. No scope is being dropped.