Bring your own SSO to OpenAI Codex CLI.
An OAuth2 / SSO credential helper that logs Codex into any AI gateway
using your corporate identity —
Microsoft Entra ID, Google Workspace, or any OIDC provider. No hand-managed API keys.
When a company puts an AI gateway in front of Codex, it wants the credential to be the developer's SSO identity — not a shared, long-lived API key copied into a dotfile. But Codex was built around API keys and ChatGPT sign-in, while an SSO-fronted gateway wants a short-lived OAuth bearer JWT.
Codex has no Anthropic-style apiKeyHelper. It does support command-backed auth on custom model providers: it shells out to a helper that prints a token to stdout, then sends it as Authorization: Bearer.
cxauth is that helper. The developer signs in once with their company account; from then on Codex gets fresh, short-lived tokens on its refresh interval. Access follows the IdP lifecycle (offboarding, Conditional Access, MFA), the gateway attributes usage to a real person, and no one generates or pastes an API key.
you ── cxauth login ─▶ browser SSO ─▶ refresh token stored in your OS keychain
│
Codex ── auth.command ─▶ cxauth token ───────┤─▶ fresh short-lived token
│ · silent refresh
│ · optional browser re-auth (opt-in)
▼
Authorization: Bearer <jwt>
│
▼
AI gateway (validates the SSO JWT via JWKS) ─▶ model
Standalone sibling of ccauth for Claude Code — same gateways, providers, profiles, and enterprise layering; separate binary, config dir, and keychain service.
- Any gateway — LiteLLM, Portkey, Bifrost, or any OIDC-aware OpenAI-compatible gateway, via one tool.
- Any SSO — Entra ID, Google Workspace, or generic OIDC (Okta/Auth0/Keycloak).
- Two credential modes — passthrough (emit the raw SSO JWT for the gateway to validate) or exchange (trade it for a gateway-native key via RFC 8693 or a broker).
- Invisible refresh — Codex re-invokes
cxauth tokenonrefresh_interval_ms; the helper returns cached tokens instantly and refreshes silently when near expiry. - First-class Codex wiring —
cxauth wire --writemerges a correct[model_providers.<id>.auth]block into$CODEX_HOME/config.toml(wire_api = "responses", absolute command path, no forbiddenenv_keysiblings). - Zero-config for users — IT provisions everything centrally (MDM / remote config / embedded); developers run only
cxauth login. See ENTERPRISE.md. - Secure by construction — public-client + PKCE, refresh token in the OS keychain, tokens never logged.
- Safe sandbox testing —
./hack/sandbox-test.shnever touches~/.codexor your OS keychain.
Deep dive: architecture, Codex auth contract, gateway matrix, and design decisions live in DESIGN.md.
Homebrew (macOS / Linux):
brew install PalenaAI/tap/cxauthFrom source (Go 1.25+):
git clone https://github.com/PalenaAI/codex-auth-helper.git
cd codex-auth-helper
make install # builds ./cxauth into $GOBIN (or `make build` for ./cxauth)Or download a prebuilt, checksummed binary from the releases page.
cxauth setup --write # pick provider + gateway; writes config + Codex wiring
cxauth login # sign in (browser). Use --device for headless/SSH.
cxauth status # confirm you're logged in and see token expiry
# start Codex — it now authenticates through your SSOPrefer to hand-edit? cxauth init writes a fully commented config; cxauth wire prints the exact config.toml fragment Codex needs.
What gets wired (conceptually):
model_provider = "corp-litellm"
[model_providers.corp-litellm]
name = "cxauth (entra → litellm)"
base_url = "https://llm-gateway.example.com/v1"
wire_api = "responses"
[model_providers.corp-litellm.auth]
command = "/usr/local/bin/cxauth"
args = ["token", "--profile", "work"]
timeout_ms = 10000
refresh_interval_ms = 300000Same shape as ccauth's sandbox — pass Entra + gateway env vars; the script writes config, opens a browser login, curls the gateway with the SSO token, and wires an isolated Codex:
make build
TENANT_ID="…" \
CLIENT_ID="…" \
API_SCOPE="api://<gateway-app-id>/.default" \
GATEWAY_URL="https://gateway.example.com/v1" \
MODEL="gpt-5-codex" \
./hack/sandbox-test.shEverything lands in a throwaway $SANDBOX (config, file-store session, CODEX_HOME). Your real ~/.codex and OS keychain are never used (store = "file" in the sandbox profile).
Then dogfood Codex against that sandbox only:
CODEX_HOME="$SANDBOX/codex" \
CXAUTH_CONFIG_DIR="$SANDBOX/config" \
CXAUTH_STATE_DIR="$SANDBOX/state" \
codexCleanup: rm -rf "$SANDBOX".
| Command | What it does |
|---|---|
cxauth setup [--write] |
Interactive wizard: choose provider + gateway, write the profile (and optionally the Codex wiring) |
cxauth init [--force] |
Write a commented example config.toml to edit by hand |
cxauth login [--device] |
Interactive SSO sign-in; stores the refresh token in your OS keychain (or file store) |
cxauth token |
Print the current credential to stdout — this is your Codex auth.command. Silent refresh; optional browser re-auth only when enabled |
cxauth status [--json] |
Show the active profile, config source, login state, and token expiry |
cxauth wire [--write] |
Print (or merge) the Codex model_providers + auth block for a profile |
cxauth unwire |
Remove the managed provider entry from $CODEX_HOME/config.toml |
cxauth config path|show|sync |
Inspect the config layers (user/embedded/managed/remote) or pull remote config |
cxauth doctor [--probe] |
Diagnose config + session and print the gateway-side checklist |
cxauth logout |
Delete the stored session for a profile |
cxauth gateways |
List supported gateways and how each authenticates |
All commands accept -p/--profile <name> (default: the config's default_profile).
Run cxauth gateways for the live table. Codex requires the OpenAI Responses surface (wire_api = "responses").
| Gateway | Native inbound JWT? | Mode | Notes |
|---|---|---|---|
| LiteLLM (Enterprise) | ✅ enable_jwt_auth (+ OAuth2 introspection) |
passthrough | Base URL …/v1. Must set JWT_AUDIENCE + JWT_ISSUER — unset = checks silently off. example |
| Portkey (Enterprise) | ✅ JWKS, RS256, org mapping | passthrough | Base URL https://api.portkey.ai/v1. Route via x-portkey-provider. example |
| Bifrost | ❌ virtual-key only | exchange | Broker → sk-bf-*, or the edge-auth plugin. Base …/openai/v1 |
| Generic OIDC gateway | ✅ (you configure JWKS) | passthrough | Envoy / APISIX / Kong / custom OpenAI-compatible proxy |
Config lives at ~/.config/cxauth/config.toml (override dir with CXAUTH_CONFIG_DIR), one [profiles.<name>] per gateway+SSO combination — the same profile model as ccauth. Minimal Entra → LiteLLM:
default_profile = "work"
[profiles.work]
provider = "entra"
gateway = "litellm"
mode = "passthrough"
# helper_interactive = false # Codex default: silent-only (true needs timeout_ms ≥ 180000)
[profiles.work.oauth]
tenant_id = "<tenant-guid>"
client_id = "<app-registration-client-id>"
scopes = ["api://<gateway-app-id>/.default"]
flow = "auth_code" # or "device_code"
[profiles.work.gateway_opts]
base_url = "http://localhost:4000/v1" # OpenAI-compatible root
ttl_ms = 300000 # Codex refresh_interval_ms when unset
timeout_ms = 10000 # Codex auth.timeout_ms
# provider_id = "corp-litellm" # model_providers table key
# model = "gpt-5-codex" # optional top-level model on wirecxauth init documents every provider/gateway combination. Env overrides for CI/headless: CXAUTH_TENANT_ID, CXAUTH_CLIENT_ID, CXAUTH_CLIENT_SECRET, CXAUTH_ISSUER, CXAUTH_BASE_URL, CXAUTH_REFRESH_INTERVAL_MS.
Setting up your identity provider? Step-by-step guides (provider app + the matching gateway validation values): Microsoft Entra ID · Google Workspace · Keycloak / generic OIDC.
Users shouldn't type tenant/client IDs or run a wizard. IT provisions everything centrally and the developer runs only cxauth login. Config is layered (user < embedded < managed < remote), distributable via MDM managed file, remote config URL, or a compile-time embedded branded binary, with an optional allow_user_profiles = false lockdown.
Codex wiring can be pushed the same way: cxauth wire --write merges into $CODEX_HOME/config.toml (default ~/.codex/config.toml). Project-local .codex/config.toml cannot set model_provider / model_providers — that is a Codex security boundary; wire only at user or IT-managed level.
Full guide: ENTERPRISE.md.
cxauth config path # which layers are active on this machine
cxauth config show # merged config + per-profile source (secrets redacted)- Public client + PKCE + loopback redirect (no client secret on disk for Entra/OIDC public clients).
- Refresh token in the OS keychain, with a
0600-file fallback for headless environments. - Only short-lived tokens are emitted; cxauth never logs tokens.
- The gateway verifies signature +
iss+aud+exp+ scopes; cxauth parses tokens unverified only to readexpfor refresh timing. - Codex hard-kills the auth command at
timeout_ms— default helper is silent-only so a browser re-auth never races a 5–10s kill. - Report vulnerabilities per SECURITY.md — not via public issues.
| Symptom | Likely cause / fix |
|---|---|
no valid session … run cxauth login |
Not logged in for this profile (Codex default is no auto browser re-auth). Run cxauth login -p <name>. |
Gateway returns 401 |
Wrong mode, or the gateway isn't validating your IdP. Run cxauth doctor and check the gateway-side checklist (JWT_AUDIENCE/JWT_ISSUER on LiteLLM, JWKS on Portkey). Match token aud/iss from cxauth token claims. |
Gateway 500 / max_output_tokens after curl smoke test |
Auth worked — raise max_tokens (or use MAX_TOKENS=512 ./hack/sandbox-test.sh). Not a JWT failure. |
| Codex auth fails silently | Ensure CXAUTH_CONFIG_DIR / CXAUTH_STATE_DIR are set if the session lives outside defaults; run cxauth token manually to see stderr. |
OIDC discovery … failed |
Bad issuer/tenant. Entra needs a tenant GUID or verified domain, not common. |
| Google re-prompts weekly | Publish the OAuth consent screen — "Testing" apps get 7-day refresh tokens. |
| Device flow blocked (Entra) | Conditional Access may block device-code. Use flow = "auth_code". |
| Wire refused / timeout warnings | helper_interactive=true needs timeout_ms ≥ 180000; silent default uses timeout_ms = 10000. |
Run cxauth doctor --probe to exercise the whole path end-to-end (credential redacted).
make build # ./cxauth
make test # unit tests (race)
make vet
make license-check # verify Apache headers
make package # dist/ cross-compiled binaries + LICENSE/NOTICE + SHA256SUMSContributions welcome — see CONTRIBUTING.md (CLA + DCO sign-off required).
Licensed under the Apache License, Version 2.0 — Copyright 2026 bitkaio LLC (https://bitkaio.com). See LICENSE and NOTICE.
Codex and OpenAI marks are trademarks of OpenAI, used nominatively to indicate compatibility. Not affiliated with OpenAI, LiteLLM, Portkey, or Bifrost.