mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
d1aaf789ad
## Why
`codex login` previously persisted newly issued OAuth credentials and
only then attempted to revoke the superseded refresh token. The old
credential must be revoked before a replacement browser or device-code
flow starts, and successful login must not perform any post-login
revocation attempt.
## What changed
- Revoke and clear existing stored auth before browser or device-code
CLI login begins.
- Remove superseded-token detection and revocation from the shared token
persistence path; successful login now only saves the new credentials.
- Read the raw configured auth store during CLI cleanup so
environment-provided auth cannot mask the stored refresh token.
- Preserve `auto` storage fallback semantics when keyring deletion fails
by clearing the fallback auth file.
- Add a process-level CLI regression test that requires the revoke
request to precede every device-login request and occur exactly once.
If replacement login is canceled or fails, the previous local
credentials have already been cleared. Remote revocation remains best
effort, matching explicit logout behavior.
## Validation
### Process-level before/after reproduction
I compiled the real `codex` CLI from the pre-fix parent (`14df0e8833`)
and from the PR implementation (`25c002f23b`; the login behavior is
unchanged at the current head), then ran the same device-code flow
against a local HTTP mock OAuth authority.
Each run:
1. Used a fresh temporary `CODEX_HOME` configured with
`cli_auth_credentials_store = "file"`.
2. Seeded that temporary home with managed ChatGPT auth containing
`old-access` and `old-refresh` tokens.
3. Pointed `CODEX_REVOKE_TOKEN_URL_OVERRIDE` at the mock `/oauth/revoke`
endpoint.
4. Ran the compiled CLI as:
```shell
CODEX_HOME=<temporary-home> \
CODEX_REVOKE_TOKEN_URL_OVERRIDE=<mock-issuer>/oauth/revoke \
<compiled-codex> login --device-auth --experimental_issuer <mock-issuer>
```
5. Recorded every request received by the mock authority. The mock
marked `new-access` valid when `/oauth/token` issued it and invalidated
it if `/oauth/revoke` arrived afterward, reproducing the observed
session-invalidating failure mode. After login exited, the harness also
verified the persisted refresh token and probed a protected endpoint
with `new-access`.
| Build | Observed request order | CLI/persistence result | `new-access`
probe |
| --- | --- | --- | --- |
| Pre-fix | `usercode → device token → OAuth token →
revoke(old-refresh)` | Exit `0`; `new-refresh` persisted | `401` |
| PR | `revoke(old-refresh) → usercode → device token → OAuth token` |
Exit `0`; `new-refresh` persisted | `200` |
The PR run therefore issued exactly one revocation request, before any
request that initiated the replacement login, and issued no revocation
after token exchange.
### Regression coverage
`codex-rs/cli/tests/login.rs::device_login_revokes_existing_auth_before_requesting_new_tokens`
runs the real first-party `codex` binary against a `wiremock` OAuth
server with an isolated temporary `CODEX_HOME`. It asserts:
- the exact request sequence is `/oauth/revoke`,
`/api/accounts/deviceauth/usercode`, `/api/accounts/deviceauth/token`,
then `/oauth/token`;
- there is exactly one revoke request and its body contains
`old-refresh` with the `refresh_token` hint;
- the completed login persists `new-refresh`.
Local validation:
- `just test -p codex-login` — 130 passed
- `just test -p codex-cli` — 280 passed, including the new process-level
regression test
- `just bazel-lock-check`
106 lines
3.2 KiB
TOML
106 lines
3.2 KiB
TOML
[package]
|
|
name = "codex-cli"
|
|
version.workspace = true
|
|
edition.workspace = true
|
|
license.workspace = true
|
|
build = "build.rs"
|
|
|
|
[[bin]]
|
|
name = "codex"
|
|
path = "src/main.rs"
|
|
|
|
[lib]
|
|
name = "codex_cli"
|
|
path = "src/lib.rs"
|
|
doctest = false
|
|
|
|
[lints]
|
|
workspace = true
|
|
|
|
[dependencies]
|
|
anyhow = { workspace = true }
|
|
clap = { workspace = true, features = ["derive"] }
|
|
clap_complete = { workspace = true }
|
|
codex-app-server = { workspace = true }
|
|
codex-app-server-daemon = { workspace = true }
|
|
codex-app-server-protocol = { workspace = true }
|
|
codex-app-server-test-client = { workspace = true }
|
|
codex-arg0 = { workspace = true }
|
|
codex-api = { workspace = true }
|
|
codex-chatgpt = { workspace = true }
|
|
codex-cloud-tasks = { path = "../cloud-tasks" }
|
|
codex-utils-cli = { workspace = true }
|
|
codex-config = { workspace = true }
|
|
codex-core = { workspace = true }
|
|
codex-core-plugins = { workspace = true }
|
|
codex-home = { workspace = true }
|
|
codex-exec = { workspace = true }
|
|
codex-exec-server = { workspace = true }
|
|
codex-execpolicy = { workspace = true }
|
|
codex-features = { workspace = true }
|
|
codex-git-utils = { workspace = true }
|
|
codex-install-context = { workspace = true }
|
|
codex-login = { workspace = true }
|
|
codex-memories-write = { workspace = true }
|
|
codex-mcp = { workspace = true }
|
|
codex-mcp-server = { workspace = true }
|
|
codex-model-provider = { workspace = true }
|
|
codex-models-manager = { workspace = true }
|
|
codex-plugin = { workspace = true }
|
|
codex-protocol = { workspace = true }
|
|
codex-responses-api-proxy = { workspace = true }
|
|
codex-rmcp-client = { workspace = true }
|
|
codex-rollout = { workspace = true }
|
|
codex-rollout-trace = { workspace = true }
|
|
codex-sandboxing = { workspace = true }
|
|
codex-state = { workspace = true }
|
|
codex-stdio-to-uds = { workspace = true }
|
|
codex-terminal-detection = { workspace = true }
|
|
codex-tui = { workspace = true }
|
|
codex-utils-absolute-path = { workspace = true }
|
|
codex-utils-path = { workspace = true }
|
|
crossterm = { workspace = true }
|
|
http = { workspace = true }
|
|
libc = { workspace = true }
|
|
os_info = { workspace = true }
|
|
owo-colors = { workspace = true }
|
|
regex-lite = { workspace = true }
|
|
serde = { workspace = true, features = ["derive"] }
|
|
serde_json = { workspace = true }
|
|
supports-color = { workspace = true }
|
|
sys-locale = { workspace = true }
|
|
tempfile = { workspace = true }
|
|
tokio = { workspace = true, features = [
|
|
"io-std",
|
|
"macros",
|
|
"net",
|
|
"process",
|
|
"rt-multi-thread",
|
|
"signal",
|
|
"time",
|
|
] }
|
|
toml = { workspace = true }
|
|
tracing = { workspace = true }
|
|
tracing-appender = { workspace = true }
|
|
tracing-subscriber = { workspace = true }
|
|
unicode-segmentation = { workspace = true }
|
|
url = { workspace = true }
|
|
which = { workspace = true }
|
|
|
|
[target.'cfg(target_os = "windows")'.dependencies]
|
|
codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
|
windows-sys = { version = "0.52", features = [
|
|
"Win32_Foundation",
|
|
"Win32_System_Console",
|
|
] }
|
|
|
|
[dev-dependencies]
|
|
assert_cmd = { workspace = true }
|
|
assert_matches = { workspace = true }
|
|
codex-utils-cargo-bin = { workspace = true }
|
|
insta = { workspace = true }
|
|
predicates = { workspace = true }
|
|
pretty_assertions = { workspace = true }
|
|
sqlx = { workspace = true }
|
|
wiremock = { workspace = true }
|