review: harden OAuth state check and tighten error handling

Address self-review findings ahead of code review:
- auth_cli: require the OAuth `state` on the automated loopback callback
  (reject missing/mismatched — CSRF), keep manual paste lenient since it's
  user-initiated. Add tests for _finish state handling.
- codex: drop the redundant chatgpt-account-id from client default_headers;
  the per-request auth hook already stamps it (single source of truth).
- main: tighten the "sign-in expired" hint to match `error code: 401` /
  `http 401` rather than a bare "401" substring that could misfire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Singer
2026-07-22 16:14:09 -04:00
co-authored by Claude Fable 5
parent d35af02e47
commit 3d419d312f
4 changed files with 61 additions and 8 deletions
+34
View File
@@ -46,6 +46,40 @@ def test_login_rejects_unsupported_provider(monkeypatch: pytest.MonkeyPatch) ->
assert auth_cli.run_auth(["login", "gemini"]) == 2
def test_finish_requires_state_on_loopback(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
# Loopback (require_state=True): missing or mismatched state is rejected.
with pytest.raises(codex.CodexAuthError) as missing:
auth_cli._finish("code", None, "verifier", "expected", require_state=True)
assert missing.value.code == "state_mismatch"
with pytest.raises(codex.CodexAuthError) as mismatch:
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=True)
assert mismatch.value.code == "state_mismatch"
# Matching state proceeds to the exchange.
assert auth_cli._finish("code", "expected", "verifier", "expected", require_state=True) == {
"ok": True
}
def test_finish_manual_paste_allows_absent_state(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
# Manual paste (require_state=False): a bare code with no state is accepted,
# but a present-and-wrong state is still rejected.
assert auth_cli._finish("code", None, "verifier", "expected", require_state=False) == {
"ok": True
}
with pytest.raises(codex.CodexAuthError):
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=False)
def test_finish_rejects_missing_code() -> None:
with pytest.raises(codex.CodexAuthError) as exc:
auth_cli._finish(None, "expected", "verifier", "expected", require_state=True)
assert exc.value.code == "no_code"
@pytest.mark.parametrize("provider", ["chatgpt", "codex", "ChatGPT"])
def test_login_accepts_provider_aliases(provider: str, monkeypatch: pytest.MonkeyPatch) -> None:
reached = {"flow": False}