Codex.OAuth adds an SDK-managed ChatGPT OAuth path alongside the existing
API-key and CLI passthrough auth behavior.
Use it when you want:
- upstream-compatible persistent ChatGPT login written to
auth.json - a host-managed login UX built from
begin_login/1+await_login/2 - memory-only auth for embedded app-server clients
Auth resolution overview
For normal CLI-backed SDK execution, auth precedence remains:
CODEX_API_KEYauth.jsonOPENAI_API_KEY- ChatGPT OAuth tokens in
auth.json
Codex.OAuth only manages the ChatGPT OAuth branch. It does not replace direct
API-key auth for realtime or voice.
Persistent OAuth respects upstream auth_mode:
chatgptmeans managed ChatGPT OAuth persisted on diskchatgptAuthTokensremains external/ephemeral semantics- stale
OPENAI_API_KEYvalues inauth.jsondo not silently override a persisted ChatGPTauth_mode
Public API
{:ok, result} = Codex.OAuth.login(storage: :file, interactive?: true)
{:ok, status} = Codex.OAuth.status()
{:ok, status} = Codex.OAuth.refresh()
:ok = Codex.OAuth.logout()Host applications can control the browser UX directly:
{:ok, pending} = Codex.OAuth.begin_login(storage: :memory, interactive?: true)
:ok = Codex.OAuth.open_in_browser(pending)
{:ok, result} = Codex.OAuth.await_login(pending, timeout: 120_000)Storage modes
storage: :file or :auto
- writes upstream-compatible
auth.jsonunder the effectiveCODEX_HOME - normal exec/app-server/model-list flows can reuse those credentials naturally
Codex.OAuth.refresh/1rotates refreshed tokens back into that file
storage: :memory
- keeps tokens in memory only
- avoids writing the login to disk
- is the mode used for external app-server auth
Environment behavior
- Local desktop: browser auth code + PKCE + loopback callback
- WSL: browser first, then device code fallback if the callback never arrives
- SSH/headless/container: device code by default
- CI/non-interactive: no automatic login; existing credentials are used or the call fails clearly
The browser flow always uses:
- an external browser
- PKCE
S256 - a loopback listener bound to
127.0.0.1, with an upstream-compatible browser redirect URI onlocalhost - the upstream-compatible localhost callback port
1455by default, unless explicitly overridden
App-server integration
Persistent child auth:
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
process_env: %{"CODEX_HOME" => "/tmp/codex-home"},
oauth: [mode: :auto, storage: :file, interactive?: true]
)Memory-only external auth:
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
experimental_api: true,
process_env: %{"CODEX_HOME" => "/tmp/codex-home"},
oauth: [mode: :auto, storage: :memory, auto_refresh: true]
)Memory mode works like this:
- the SDK obtains ChatGPT OAuth tokens natively
- the app-server child initializes normally
- the SDK calls
account/login/startwithchatgptAuthTokens - a connection-owned refresh responder answers
account/chatgptAuthTokens/refresh
Set auto_refresh: false when you want to subscribe and respond to refresh
requests yourself.
Child environment semantics
When OAuth is used through Codex.AppServer.connect/2, auth resolution is based
on the effective child cwd and process_env, not the caller's current shell
state. That matters for isolated CODEX_HOME setups and repo-local config.
TLS / CA behavior
All OAuth HTTP traffic reuses Codex.Net.CA:
CODEX_CA_CERTIFICATESSL_CERT_FILE
The same trust root configuration is shared by CLI subprocesses, OAuth refresh, MCP HTTP/OAuth, remote model fetches, realtime websockets, and voice HTTP requests.
Example
Run the live OAuth example:
mix run examples/live_oauth_login.exs
mix run examples/live_oauth_login.exs --interactive
mix run examples/live_oauth_login.exs --interactive --browser --no-browser
mix run examples/live_oauth_login.exs --interactive --device
mix run examples/live_oauth_login.exs --interactive --app-server-memory
By default the example uses an isolated temporary CODEX_HOME, so it does not
change the login stored in your normal Codex home. It always prints the current
OAuth status first. In non-interactive mode it never starts a login on its
own: if no saved session is available, it prints SKIPPED and exits cleanly.
Useful switches:
--interactiveallows the example to start a real login when needed.--browserforces browser login.--deviceforces device-code login.--no-browserprints the authorization URL and leaves it to you to open it manually.--app-server-memorycontinues into the memory-mode app-server flow after login so you can see SDK-managed external auth in action.--keep-homekeeps the generated temporaryCODEX_HOMEinstead of deleting it when the example exits.CODEX_OAUTH_EXAMPLE_HOME=/pathreuses a specificCODEX_HOMEso you can return to the same saved session on later runs.--use-real-homemakes the example operate on your normal Codex home intentionally instead of an isolated temporary one.
For browser login, the example uses the upstream-compatible callback
http://localhost:1455/auth/callback by default. If that port is already in
use on your machine, pass --callback-port=<port> and open the printed URL in
your browser.