<CodexAuth> is a security-sensitive component: it brokers access to a user's
personal ChatGPT account. This document states the trust model and the hardening
the reference backend ships with.
OAuth tokens (access_token, refresh_token, expires_at) live only on the
backend, inside the session store / the codex CLI's ~/.codex/auth.json.
No token ever appears in any HTTP response to the browser, in any cookie
value, in any error message, or in any stream event. This invariant is enforced
by a guard test (tests/token-confinement.test.ts) and by keeping the backend in
a separate codex-auth/backend entry that the browser bundle never imports.
The reference router (createCodexRouter) sets a session cookie that is:
HttpOnly— unreadable from JavaScript (so the client always calls/sessionrather than trying to detect the cookie).Secure— never sent over plaintext. (Disable only for local HTTP dev viacookieOptions: { secure: false }.)SameSite=Strict— not attached to cross-site requests.- signed with
cookieSecret(must be ≥16 chars, high-entropy, from env). - rotated on successful login — session-fixation defense.
In-memory sessions are the default and TTL-expire; a process restart invalidates them. Use a shared store (Redis, etc.) in production.
The contract is cookie-authenticated, so every POST is CSRF-protected: the
router rejects requests whose Sec-Fetch-Site is cross-site (or whose Origin
host differs from Host), unless the origin is explicitly allowlisted. This is in
addition to SameSite=Strict. The PKCE state parameter protects the backend's
upstream call to auth.openai.com — it does not protect the browser↔backend
contract; that's what the CSRF check is for.
- Default
credentialsissame-origin. Do not default to'include'. - For a cross-origin backend, pass
allowedOriginstocreateCodexRouter. It emitsAccess-Control-Allow-Credentials: truewith the specific allowed origin — never*, never a reflected arbitrary origin. Unlisted origins get no CORS headers and are rejected.
The login URL returned by /login/start is validated before the popup navigates
to it: it must be https: and on an allowed host (default auth.openai.com).
javascript: and http: URLs are rejected. The holding screen written into the
popup is a static constant — no server data is interpolated (no XSS sink). The
popup-blocked fallback anchor uses rel="noopener noreferrer".
Assistant text from /run/stream is rendered as plain text by the default UI
(never dangerouslySetInnerHTML). The reference CLI runner sanitizes the CLI's
stderr (redacts JWT/key/path patterns) before forwarding any error.
- Don't expose
/login/startto unauthenticated or CSRF-able callers — an attacker who could start a device login and read theuserCodecould attempt their own approval flow. Keep the contract behind your normal app session. - Add rate limiting to
/login/startand/run/streamto prevent quota burn (the reference backend leaves this as an integration point). - The persisted browser session marker stores only
{ loggedIn, savedAt }by default — the account email is not persisted (opt in viapersistAccount), to avoid PII readable by an XSS.
Both runners reuse OpenAI's first-party Codex CLI OAuth client off-label, and
the directRunner additionally calls chatgpt.com/backend-api/codex/* directly
(the same backend the CLI talks to) with a CLI-looking User-Agent. Understand
the tradeoffs before shipping:
- Not a public/supported API. The client id and backend belong to OpenAI's Codex tooling. There is no official program letting third-party apps consume a user's ChatGPT subscription — you are using a first-party credential off-label.
- It can break or get accounts limited. These endpoints are undocumented and may change; driving usage this way may violate OpenAI's terms and could put users' accounts or the shared client id at risk.
- Each user must enable device-code authorization once (ChatGPT → Settings → Security & Login). Your app cannot toggle this for them.
- Fine for experiments, demos, and personal tools. Don't build a business on it. For production, use the official OpenAI API with your own key, or have each user bring their own key.
OpenAI's Terms of Use explicitly prohibit "reselling access or using ChatGPT to power third-party services," "sharing your account credentials or making your account available to anyone else," and "automatically or programmatically extracting data." Powering an app for other users with this software falls under those prohibitions. You are solely responsible for your use; the authors provide it as-is, with no warranty and no liability. See the Disclaimer in the README.
This is a product/ToS issue, not a code vulnerability — but ship with it in mind.
Found an issue? Open a private security advisory rather than a public issue.