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.
OAuth files, API keys, and normal CODEX_HOME state are native standalone
mechanics until a caller explicitly promotes them through governed authority.
Governed Codex execution must use control-plane authority refs, credential
handles or leases, target grants, and materialized auth. A saved auth.json,
env var, local login, or shell-provided remote auth token cannot satisfy
governed authority on its own.
When governed_authority: refs are present, OAuth and account helpers derive
their child context from supplied process_env / env values instead of the
caller shell. Missing governed CODEX_HOME is rejected for persistent
child-auth work, and unmanaged ambient Codex auth/base-url/model env is
rejected before it can influence the effect.
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
ChatGPT plan strings are canonicalized before the SDK exposes or forwards them:
hcand case variants ofenterprisebecome"enterprise"educationandedubecome"edu"- existing canonical lowercase plans such as
"free","plus","pro","team", and"business"stay unchanged
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.
For the current browser-login presentation contract, pass the app-server options directly to the account login helper:
Codex.AppServer.Account.login_start(conn, :chatgpt,
app_brand: :chatgpt,
codex_streamlined_login: true,
use_hosted_login_success_page: true
)app_brand accepts :codex or :chatgpt. The boolean fields are emitted only
when enabled, matching codex-cli 0.144.0's account/login/start request.
Experimental Amazon Bedrock login
The upstream app-server schema includes an unstable Bedrock request variant:
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
experimental_api: true,
process_env: %{"CODEX_HOME" => "/tmp/codex-home"}
)
Codex.AppServer.Account.login_start(
conn,
{:amazon_bedrock, bedrock_api_key, "us-east-1"}
)The exact request is type: "amazonBedrock" with required apiKey and
region fields. credentialSource belongs to returned account state and is
not a login input. bedrockApiKey auth records persist their key and region in
the nested bedrock_api_key object, with all SDK Inspect paths redacted.
This surface is parser/request ready, not a claim that login currently works:
the current upstream account processor still returns “Amazon Bedrock login is
not implemented.” Do not log or inspect the supplied credential, and keep the
connection opted into experimental_api: true while the API remains unstable.
Remote websocket app-server connections use the same external-auth shape:
{:ok, conn} =
Codex.AppServer.connect_remote(
"wss://app-server.example/ws",
auth_token_env: "CODEX_REMOTE_AUTH_TOKEN",
experimental_api: true,
oauth: [mode: :auto, storage: :memory, auto_refresh: true]
)Remote mode only supports storage: :memory. storage: :file and :auto are
rejected because there is no managed child process or child CODEX_HOME to
prepare ahead of time.
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.
For connect_remote/2, cwd and process_env still inform auth-context
resolution, but they do not create or prepare a child process. Remote bearer
auth headers are only attached for wss:// or loopback ws:// websocket URLs.
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.