Harden OAuth auth path and proxy edge cases

Cooper Gamble committed May 29, 2026 at 11:54 UTC 96088de92362a1cc7a9e31edfa33e360eb30e3f2
3 files changed +61 -6
plugins/_oauth/helpers/codex.py
+14 -5
@@ -36,7 +36,7 @@ AUTH_FILENAME = "auth.json"
36 ACCESS_EXPIRY_MARGIN = timedelta(minutes=5)
37 REFRESH_INTERVAL = timedelta(minutes=55)
38 FALLBACK_CODEX_VERSION = "0.124.0"
39 -OAUTH_ERROR_KEYS = {"error", "error_description"}
39 +OAUTH_ERROR_KEYS = ("error_description", "error")
40 DEVICE_CODE_TIMEOUT_SECONDS = 15 * 60
41 WINDOWS_LOCK_RETRY_SECONDS = 0.05
42 USAGE_ENDPOINT_PATHS = (
@@ -1097,13 +1097,22 @@ def _validate_private_auth_path(path: Path) -> Path:
1097 if _path_key(resolved_path) == _path_key(candidate) or _same_existing_file(
1098 resolved_path, candidate
1099 ):
1100 - raise RuntimeError(
1101 - "Agent Zero OAuth credentials must use an Agent Zero-owned auth file. "
1102 - "Choose a private auth_file_path or leave it empty for the default private store."
1103 - )
1100 + raise _private_auth_path_error()
1101 + try:
1102 + if resolved_path.stat().st_nlink > 1:
1103 + raise _private_auth_path_error()
1104 + except FileNotFoundError:
1105 + pass
1106 return resolved_path
1107
1108
1109 +def _private_auth_path_error() -> RuntimeError:
1110 + return RuntimeError(
1111 + "Agent Zero OAuth credentials must use an Agent Zero-owned auth file. "
1112 + "Choose a private auth_file_path or leave it empty for the default private store."
1113 + )
1114 +
1115 +
1116 def _known_codex_auth_paths() -> list[Path]:
1117 candidates = [
1118 Path.home() / ".codex" / AUTH_FILENAME,
plugins/_oauth/helpers/routes.py
+6 -1
@@ -295,7 +295,12 @@ def _supplied_proxy_token() -> str:
295
296
297 def _host_is_local(host: str) -> bool:
298 - hostname = (host or "").split(":", 1)[0].strip("[]").lower()
298 + hostname = (host or "").strip().lower()
299 + if hostname.startswith("["):
300 + closing_bracket = hostname.find("]")
301 + hostname = hostname[1:closing_bracket] if closing_bracket >= 0 else hostname.strip("[]")
302 + elif hostname.count(":") == 1:
303 + hostname = hostname.split(":", 1)[0]
304 if hostname in {"localhost", "127.0.0.1", "::1"}:
305 return True
306 try:
tests/test_oauth_codex.py
+41
@@ -14,6 +14,7 @@ import yaml
14
15 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
16 from plugins._oauth.helpers import codex
17 +from plugins._oauth.helpers import routes
18 from plugins._oauth.extensions.python._functions.models.get_api_key.end._20_codex_account_dummy_key import (
19 CodexAccountDummyKey,
20 )
@@ -242,6 +243,21 @@ def test_normalize_usage_payload_accepts_zero_percent_headers():
243 assert usage["primary"]["label"] == "5h"
244
245
246 +def test_token_error_message_prefers_description():
247 + class FakeResponse:
248 + status_code = 400
249 + text = '{"error":"invalid_grant","error_description":"refresh token was already used"}'
250 +
251 + @staticmethod
252 + def json():
253 + return {
254 + "error": "invalid_grant",
255 + "error_description": "refresh token was already used",
256 + }
257 +
258 + assert codex._token_error_message(FakeResponse()) == "refresh token was already used"
259 +
260 +
261 def test_default_auth_file_ignores_codex_cli_credentials(tmp_path, monkeypatch):
262 shared_auth = tmp_path / ".codex" / "auth.json"
263 private_auth = tmp_path / "usr" / "plugins" / "_oauth" / "codex" / "auth.json"
@@ -280,6 +296,17 @@ def test_explicit_codex_cli_auth_hard_link_is_rejected(tmp_path, monkeypatch):
296 codex.resolve_auth_write_path()
297
298
299 +def test_explicit_private_auth_hard_link_is_rejected(tmp_path, monkeypatch):
300 + private_auth = tmp_path / "private-auth.json"
301 + private_auth.write_text(json.dumps({"tokens": {"refresh_token": "private"}}), encoding="utf-8")
302 + alias = tmp_path / "agent-zero-auth.json"
303 + alias.hardlink_to(private_auth)
304 + monkeypatch.setattr(codex, "codex_config", lambda: {"auth_file_path": str(alias)})
305 +
306 + with pytest.raises(RuntimeError, match="Agent Zero-owned auth file"):
307 + codex.resolve_auth_write_path()
308 +
309 +
310 def test_write_auth_file_uses_atomic_replace_and_private_permissions(tmp_path, monkeypatch):
311 auth_path = tmp_path / "auth.json"
312 replacements: list[tuple[Path, Path]] = []
@@ -392,6 +419,20 @@ def test_lock_file_retries_windows_contention(tmp_path, monkeypatch):
419 assert sleeps == [codex.WINDOWS_LOCK_RETRY_SECONDS, codex.WINDOWS_LOCK_RETRY_SECONDS]
420
421
422 +@pytest.mark.parametrize(
423 + ("host", "expected"),
424 + [
425 + ("localhost:5000", True),
426 + ("127.0.0.1:5000", True),
427 + ("[::1]:5000", True),
428 + ("::1", True),
429 + ("example.com:5000", False),
430 + ],
431 +)
432 +def test_proxy_local_host_detection_supports_loopback_ipv6(host, expected):
433 + assert routes._host_is_local(host) is expected
434 +
435 +
436 def test_load_auth_serializes_refresh_across_threads(tmp_path, monkeypatch):
437 auth_path = tmp_path / "auth.json"
438 _write_refreshable_auth(auth_path)