feat(tui): native ChatGPT PKCE sign-in for openai-codex (#5778) - #5784
feat(tui): native ChatGPT PKCE sign-in for openai-codex (#5778)#5784Hmbown wants to merge 2 commits into
Conversation
Add a Codewhale-owned Sign in with ChatGPT adapter so the openai-codex route no longer requires Codex CLI. Browser PKCE uses the published auth.openai.com issuer, stores rotating refresh tokens in the existing credentials directory, and keeps external CLI import as an explicit alternative. Device-code is omitted because the issuer does not advertise device_authorization_endpoint. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewing PR #5784 — native ChatGPT PKCE sign-in for openai-codex
Todo list
|
|
Claude finished @Hmbown's task in 3m 42s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in for openai-codexTodo list
SummarySolid, well-scoped adapter that closely mirrors the existing xAI device-login pattern (shared Findings1. Blocking network I/O runs synchronously inside the async TUI event loop — 2. Doc claims OIDC-discovery-based revoke, but the endpoint is hardcoded — 3. Minor: unused localized message — Good choices worth calling out
|
There was a problem hiding this comment.
Codewhale review
Adds native Sign in with ChatGPT PKCE loopback flow for openai-codex, including config storage, CLI/TUI wiring, localization, tests, and docs. Overall solid but has several correctness concerns around loopback host resolution, hardcoded revocation endpoint versus the discovery claim, and opaque token expiry handling.
Findings
- [WARNING] Loopback callback uses localhost redirect while listener binds 127.0.0.1 (
crates/tui/src/chatgpt_oauth.rs)
bind_loopback_callback() binds only to 127.0.0.1, but redirect_uri_for_port() returns http://localhost:/auth/callback. On systems where localhost resolves to ::1 first (common on Linux with IPv6 enabled), the browser may connect to ::1 and be refused, breaking PKCE login. Bind to localhost (both v4/v6) or align the redirect URI to the bound address, and verify against OpenAI's registered redirect URIs. - [WARNING] Revocation endpoint hardcoded, not read from OIDC discovery (
crates/tui/src/chatgpt_oauth.rs)
PR claims to use the discovery revocation_endpoint, but revoke_endpoint() hardcodes /api/accounts/oauth/revoke and pkce_login() never fetches https://auth.openai.com/.well-known/openid-configuration. If OpenAI rotates endpoints, revoke will fail while the rest of the flow may still work. Fetch and use the advertised revocation_endpoint, or update docs to say it is hardcoded. - [INFO] select_entry prefers built-in client id even when custom client id is configured (
crates/tui/src/chatgpt_oauth.rs)
select_entry always prefers keys ending with ::app_EMo... (CHATGPT_OAUTH_CLIENT_ID). When CODEWHALE_CHATGPT_OAUTH_CLIENT_ID is set to a different client id, the current entry won't be preferred, and fallback may pick an older entry. Consider deriving the preferred suffix from the entry/config client id. - [WARNING] Opaque access tokens without expires_in are treated as expired and can fail closed incorrectly (
crates/tui/src/chatgpt_oauth.rs)
entry_access_token_is_fresh returns false when neither expires_at nor a JWT exp is present. For valid opaque tokens issued without expires_in (or with no JWT), get_owned_credentials_locked will attempt refresh and, if no refresh token is stored, error 'access token expired and no refresh_token is stored' even though the access token may be valid. Consider treating missing expiry as fresh until a 401 indicates otherwise, or require and document that the issuer always returns expires_in. - [WARNING] Local credential file removal errors are silently ignored during revoke (
crates/tui/src/chatgpt_oauth.rs)
In revoke_owned_login_locked, store.remove(&name) useslet _ = store.remove(&name);, so a failed deletion (permissions, I/O) leaves the token file on disk while the command reports success and the config pointer is unset. The user may believe credentials are revoked. Propagate or at least log the error. - [INFO] Logout clears only valid generation pointer, not legacy chatgpt pointer (
crates/cli/src/lib.rs)
run_logout_command_with_secrets_unlocked sets oauth_credential_generation to None only if is_valid_chatgpt_oauth_generation. If the config pointer somehow holds the legacy name chatgpt-oauth.json, logout will not clear it, then clear_all_chatgpt_oauth_credentials deletes that file, leaving a dangling config pointer. Use is_valid_chatgpt_oauth_generation or also check LEGACY_CHATGPT_OAUTH_FILE_NAME.
Suggestions
crates/tui/src/chatgpt_oauth.rs— Bind the callback listener tolocalhostas well as 127.0.0.1, or change redirect_uri_for_port to usehttp://127.0.0.1:<port>so the browser connects to the bound interface. Verify that the chosen URI is among the public client's registered redirect URIs.crates/tui/src/chatgpt_oauth.rs— Fetch the OIDC discovery document on first use and cache the advertised revocation_endpoint instead of hardcoding it, so remote revoke stays aligned with issuer configuration. If hardcoding is intentional, update docs and PR text accordingly.crates/tui/src/chatgpt_oauth.rs— Replacelet _ = store.remove(&name);with logging or propagate the error so users are not misled when local credential deletion fails.
Assessment
The PR is well-structured with good tests and documentation, but a few correctness issues should be addressed before merge: the localhost/IPv4 callback mismatch can break login on many systems, revocation endpoint is hardcoded despite the discovery claim, and opaque-token expiry handling may fail closed incorrectly. The code otherwise reuses patterns well and keeps token material redacted.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head c65d0ea1bacfa54d0dc419985d082dadc3a2c9b8). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| }) | ||
| .unwrap() | ||
| .unwrap(); | ||
| assert!(stored.contains("refresh-2"), "{stored}"); |
| .unwrap() | ||
| .unwrap(); | ||
| assert!(stored.contains("refresh-2"), "{stored}"); | ||
| assert!(!stored.contains("refresh-old"), "{stored}"); |

Summary
Native Sign in with ChatGPT for the first-class
openai-codexroute. Connecting a ChatGPT/Codex subscription no longer requires Codex CLI or~/.codex/auth.json. Browser PKCE with a localhost callback stores refreshable tokens in Codewhale-owned storage, matching the xAI device-login pattern.Closes #5778.
Terms research (ambiguous — implemented behind a documented boundary)
Public sources checked 2026-08-31:
localhost:1455) is the official Codex client flow. Device-code is documented as a Codex CLI beta (codex login --device-auth), not as a third-party API.https://auth.openai.com/.well-known/openid-configuration): advertisesauthorization_code+ PKCES256,refresh_token, scopesopenid profile email offline_access, andrevocation_endpoint. Does not advertisedevice_authorization_endpoint.app_EMoamEEZ73f0CkXaXp7hrann, Apache-2.0 Codex CLI).Conclusion: ambiguous. This PR implements the published authorization-code + PKCE S256 adapter and does not smuggle unpublished endpoints:
originator=codewhale(nevercodex_cli_rs)/oauth/authorizeand/oauth/tokenon the published issuerrevocation_endpointfor remote revoke/api/accounts/deviceauth/*(unpublished; issuer does not advertise device authorization)$CODEWHALE_HOME/credentials/chatgpt-auth-<hex>.json; Codex CLI files are never written or refreshedIf OpenAI later allocates a Codewhale-specific client id, swap
CODEWHALE_CHATGPT_OAUTH_CLIENT_ID.Behavior
/provider setup openai-codex(and missing-auth handoff) offers Sign in with ChatGPT first (subscription / ChatGPT billing) vs Import Codex CLI credentials (explicit read-only consent, Codex CLI remains owner). Copy distinguishes this from theopenaiAPI-key billing owner before any run.codewhale auth chatgpt//auth chatgptruns PKCE login.codewhale auth chatgpt-revoke//auth chatgpt-revokedeletes Codewhale-owned tokens (best-effort remote revoke) and does not touch Codex CLI consent.Testing
cargo fmt --all(clean)cargo clippy -p codewhale-config --tests -- -D warningscargo clippy -p codewhale-tui --tests -- -D warnings -A clippy::too_many_arguments(the allow is pre-existing onruntime_threads/underwater, not introduced here)cargo clippy -p codewhale-cli --tests -- -D warnings -A clippy::too_many_argumentscargo test -p codewhale-config --lib xai_credentialscargo test -p codewhale-tui --lib chatgpt_oauth(10 tests: PKCE S256, callback success/error/state, mock token exchange, persist/refresh/revoke, no live OpenAI)cargo test -p codewhale-tui --lib -- provider_picker::tests(126)cargo test -p codewhale-cli --lib -- parses_auth_subcommand_matrix auth_statuspython3 scripts/check-tui-locale-parity.pycargo test --workspace --all-features --lockednot run locally (TUI crate compile is large; CI will cover)Unit tests use a mock issuer. They do not hit live OpenAI.
Checklist
chatgpt_oauth.rs), not a product fork; it reuses the existing Codewhale-owned credentials directorydocs/PROVIDERS.md,docs/CONFIGURATION.md)