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)