Skip to content

PalenaAI/codex-auth-helper

Repository files navigation

OpenAI

cxauth

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.

Platforms CI License: Apache 2.0 Go version For OpenAI Codex SSO providers Gateways


Why cxauth?

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.

Highlights

  • 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 modespassthrough (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 token on refresh_interval_ms; the helper returns cached tokens instantly and refreshes silently when near expiry.
  • First-class Codex wiringcxauth wire --write merges a correct [model_providers.<id>.auth] block into $CODEX_HOME/config.toml (wire_api = "responses", absolute command path, no forbidden env_key siblings).
  • 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.sh never touches ~/.codex or your OS keychain.

Deep dive: architecture, Codex auth contract, gateway matrix, and design decisions live in DESIGN.md.

Install

Homebrew (macOS / Linux):

brew install PalenaAI/tap/cxauth

From 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.

Quick start

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 SSO

Prefer 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 = 300000

Isolated end-to-end test (does not touch ~/.codex)

Same 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.sh

Everything 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" \
  codex

Cleanup: rm -rf "$SANDBOX".

Commands

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).

Gateway support

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

Configuration

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 wire

cxauth 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.

Enterprise / IT deployment

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)

Security

  • 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 read exp for 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.

Troubleshooting

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).

Development

make build          # ./cxauth
make test           # unit tests (race)
make vet
make license-check  # verify Apache headers
make package        # dist/ cross-compiled binaries + LICENSE/NOTICE + SHA256SUMS

Contributions welcome — see CONTRIBUTING.md (CLA + DCO sign-off required).

License

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.

About

Bring your own SSO to OpenAI Codex CLI — an OAuth2/SSO credential helper that logs Codex into any AI gateway (LiteLLM, Portkey, Bifrost, OIDC) using Microsoft Entra ID, Google Workspace, or any OIDC provider. No hand-managed API keys.

Topics

Resources

License

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors