Skip to content

[Bug]: ForwardAdmissionCredentialError blocks direct-mode OpenAI forwarding when caller authenticates with the proxy admission token #1687

Closed as duplicate

Description

@rrmlima

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

  1. 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();
+  }
 }
  1. Restore the ctx.kind === "main" block in headersForCodexAuthContext (src/codex/auth-context.ts) that replaces the admission bearer with Bearer ${main.accessToken} + chatgpt-account-id when bearer === process.env.OPENCODEX_API_AUTH_TOKEN.

Alternatives considered

  • Keep rejecting unconditionally and require clients to send the real ChatGPT token: breaks the env_key injection contract and the whole point of codexAccountMode: "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:

  • direct mode + admission bearer + main account present → 200 (token substituted upstream)
  • direct mode + admission bearer + no main account → 401 (fail-closed, unchanged)
  • pool mode → unchanged (pool accessToken override)
  • x-opencodex-api-key auth (non-bearer) → unchanged (no admission-bearer path)

Activity

  1. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    streamingSSE, WebSocket, terminal stream frames
    on Aug 14, 2026
  2. coderabbitai commented on Aug 14, 2026

    @coderabbitai
    Contributor
    🔗 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 plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. added
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Aug 15, 2026
  4. Wibias commented on Aug 16, 2026

    @Wibias
    Contributor

    Closing this as a duplicate of #1686.

    The two reports are not independent fixes. #1687 is the second stage of the same env_key bearer flow tracked by #1686: once /v1/responses accepts the OpenCodex admission token from Authorization: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management authstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions