Skip to content

[Bug]: restore Codex env_key bearer admission and direct-mode OpenAI credential substitution #1686

Description

@rrmlima

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:

  1. resolveResponsesApiAuth accepts Authorization: Bearer <OPENCODEX_API_AUTH_TOKEN> as an admission fallback while preserving the dedicated header path.
  2. Codex injection uses the supported env_key contract, with compatibility handling for older runtimes where required.
  3. For direct-mode OpenAI forwarding, an admitted proxy bearer is never sent upstream.
  4. If a stored main ChatGPT/OpenAI account is available, replace the admission bearer with the stored access token and the matching chatgpt-account-id.
  5. If substitution is impossible, remain fail-closed and reject the request rather than leaking the admission credential.
  6. 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_key bearer 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-key with 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_headers and do nothing: breaks modern Codex env_key users.
  • Fix only admission: still leaves direct-mode requests failing at the forward-credential guard.
  • Fix only forwarding: requests never reach that stage when /v1/responses rejects the bearer.
  • Forward the admission bearer as-is: invalid and unsafe because the OpenCodex admission secret is not an upstream ChatGPT/OpenAI credential.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingcliCLI, config inject, packaging flagsproxyHTTP 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