Skip to content

fix(auth): align OAuth refresh policy with Codex - #185

Open
squarepots wants to merge 37 commits into
Lampese:mainfrom
squarepots:fix/oauth-refresh-policy
Open

squarepots wants to merge 37 commits into
Lampese:mainfrom
squarepots:fix/oauth-refresh-policy

Conversation

@squarepots

@squarepots squarepots commented Sep 21, 2026 •

Copy link
Copy Markdown

依赖 #182,请先审阅该 PR。

本 PR 让 OAuth token refresh 与当前 Codex 的判断规则一致,并让 desktop 与 codex-web 共享同一条跨进程 refresh 路径。

主动 refresh 算法:

  1. 如果 access token 的 exp 可以解析,只有在它已过期或将在 5 分钟内过期时 refresh。这个分支优先级高于 refresh age;新鲜 access token 不会因为旧的 last_refresh 或过期的 ID token 而 refresh。
  2. 如果 access token 的 exp 缺失或无法解析,回退到 last_refresh,且只有超过 8 天才 refresh。
  3. 当前 active profile 的 refresh age 来自 auth.json.last_refresh;inactive profile 使用 StoredAccount.last_refresh_at。该可选字段兼容旧数据,并且只在凭据实际创建、refresh 或从可信 auth.json reconciliation 时更新;不会使用 last_used_at,普通账号切换也不会伪造 refresh age。没有可信 refresh age 时不做主动 refresh。

凭据 generation 的 reconciliation: auth.json 仍决定 active runtime identity,但其 token payload 只有在 auth.json.last_refresh 不早于 StoredAccount.last_refresh_at 时才能写回 profile store。已知的新 generation 不会被旧的或没有 generation 的 runtime snapshot 覆盖。refresh 在同一 account-store mutation 中持久化 token 和 generation;因此 rotated refresh token 即使在 ID token 无效或 auth.json 写入失败后,也能在下一次 reconciliation 保持可恢复。较新的 runtime generation 仍会正常投影到 active profile。

收到 401 时如何处理? provider 401 可以触发一次受保护的 refresh 和一次 retry。操作会先获取 auth-operation.lock,然后重新读取当前凭据;如果另一个进程已经换入新的 access token,就直接复用它。

403 和其他状态如何处理? 403、429 及其他非 401 响应不会触发 refresh,也不会消耗 rotating refresh token。

refresh、401 retry、账号切换和 OAuth 完成都使用同一个跨进程 auth-operation.lock。usage、warm-up、账号 metadata、profile usage 和 reset credits 使用同一套 401/retry 规则。

refresh 响应缺少 id_token 时保留 durable token 并返回错误;缺少 refresh_token 时保留旧 refresh token。

验证

  • cargo test --locked rotated_refresh_token_survives_stale_runtime_reconciliation:通过
  • cargo test --locked newer_runtime_generation_projects_tokens_to_the_active_store:通过
  • 变更 Rust 文件通过 targeted rustfmt --edition 2021 --check
  • cargo check --manifest-path src-tauri/Cargo.toml --locked:通过
  • cargo test --manifest-path src-tauri/Cargo.toml --locked:65 个 Rust 测试通过
  • git diff --check:通过

Closes #136


Depends on #182. Please review that PR first.

This PR aligns OAuth token refresh with the current Codex decision rules and gives desktop and codex-web one shared cross-process refresh path.

Proactive refresh algorithm:

  1. When the access token has a parseable exp, refresh only when it is expired or within five minutes of expiry. This branch takes precedence over refresh age: a fresh access token is not refreshed because last_refresh is old or the ID token is expired.
  2. When the access-token exp is missing or unparseable, fall back to last_refresh and refresh only after eight days.
  3. The active profile takes refresh age from auth.json.last_refresh; inactive profiles use StoredAccount.last_refresh_at. This optional field remains backward-compatible and changes only when credentials are created, refreshed, or reconciled from trustworthy auth.json; it never uses last_used_at, and ordinary account switches do not fabricate refresh age. No trustworthy refresh age means no proactive refresh.

Credential-generation reconciliation: auth.json continues to determine the active runtime identity, but its token payload can write back to the profile store only when auth.json.last_refresh is not older than StoredAccount.last_refresh_at. A known newer generation cannot be replaced by an older or unversioned runtime snapshot. Refresh persists the tokens and their generation in the same account-store mutation, so a rotated refresh token remains recoverable on the next reconciliation even when the ID token is invalid or writing auth.json fails. A newer runtime generation still projects normally to the active profile.

What happens after a 401? A provider 401 may trigger one guarded refresh and one retry. The operation acquires auth-operation.lock and reloads current credentials first; if another process has already installed a new access token, it reuses it.

What happens after 403 and other statuses? A 403, 429, or other non-401 response never triggers refresh and never consumes a rotating refresh token.

Refresh, the 401 retry, account switching, and OAuth completion use the same cross-process auth-operation.lock. Usage, warm-up, account metadata, profile usage, and reset credits share the same 401/retry policy.

When a refresh response omits id_token, durable tokens are preserved and the operation returns an error. When it omits refresh_token, the existing refresh token is retained.

Validation

  • cargo test --locked rotated_refresh_token_survives_stale_runtime_reconciliation: passed
  • cargo test --locked newer_runtime_generation_projects_tokens_to_the_active_store: passed
  • Changed Rust files passed targeted rustfmt --edition 2021 --check
  • cargo check --manifest-path src-tauri/Cargo.toml --locked: passed
  • cargo test --manifest-path src-tauri/Cargo.toml --locked: 65 Rust tests passed
  • git diff --check: passed

Closes #136

Restores #172 after the source fork was accidentally deleted.


Maintenance triage — 2026-09-30

Prerequisite PRs: #182.

#136 also asks for human-readable authentication errors, which were implemented in the older #137. The refresh-policy work alone must not silently discard that UX acceptance concern. Preserve the error mapping or split a tracked follow-up before considering all of #136 complete.

@squarepots
squarepots force-pushed the fix/oauth-refresh-policy branch from 54fcfeb to 7aa083b Compare September 21, 2026 19:04
@Lampese Lampese added bug Something isn't working area:auth OAuth, token refresh, identity, activation and reauthentication. priority:P1 Address first: auth, data integrity, security, startup or release validation. status:needs-review Maintainer review required; this label is not merge approval. status:needs-ci No check rollup is visible for the current PR head; validate before landing. status:blocked Depends on another open PR; prerequisite numbers are in the maintenance note. labels Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:auth OAuth, token refresh, identity, activation and reauthentication. bug Something isn't working priority:P1 Address first: auth, data integrity, security, startup or release validation. status:blocked Depends on another open PR; prerequisite numbers are in the maintenance note. status:needs-ci No check rollup is visible for the current PR head; validate before landing. status:needs-review Maintainer review required; this label is not merge approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.2.12 regression: valid Codex accounts repeatedly marked unauthorized due to refresh_token_invalidated

2 participants