Reject shared OAuth auth file aliases
Cooper Gamble committed
May 29, 2026 at 11:28 UTC
d36228af8272535c10e4822e455308fbd7718a1b
2 files changed
+28
-5
plugins/_oauth/helpers/codex.py
+15
-5
@@ -1093,11 +1093,14 @@ def _write_auth_file_in_place(path: Path, data: dict[str, Any]) -> None:
1093
1094
def _validate_private_auth_path(path: Path) -> Path:
1095
resolved_path = path.expanduser().resolve(strict=False)
1096
- if _path_key(resolved_path) in {_path_key(candidate) for candidate in _known_codex_auth_paths()}:
1097
- raise RuntimeError(
1098
- "Agent Zero OAuth credentials must use an Agent Zero-owned auth file. "
1099
- "Choose a private auth_file_path or leave it empty for the default private store."
1100
- )
1096
+ for candidate in _known_codex_auth_paths():
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
+ )
1104
return resolved_path
1105
1106
@@ -1117,6 +1120,13 @@ def _path_key(path: Path) -> str:
1120
return os.path.normcase(str(path.expanduser().resolve(strict=False)))
1121
1122
1123
+def _same_existing_file(path: Path, candidate: Path) -> bool:
1124
+ try:
1125
+ return path.samefile(candidate)
1126
+ except OSError:
1127
+ return False
1128
+
1129
+
1130
def _auth_lock_path(path: Path) -> Path:
1131
digest = hashlib.sha256(_path_key(path).encode("utf-8")).hexdigest()
1132
return Path(files.get_abs_path("usr", "plugins", "_oauth", "codex", "locks", f"{digest}.lock"))
tests/test_oauth_codex.py
+13
@@ -267,6 +267,19 @@ def test_explicit_codex_cli_auth_path_is_rejected(tmp_path, monkeypatch):
267
codex.resolve_auth_write_path()
268
269
270
+def test_explicit_codex_cli_auth_hard_link_is_rejected(tmp_path, monkeypatch):
271
+ shared_auth = tmp_path / ".codex" / "auth.json"
272
+ shared_auth.parent.mkdir()
273
+ shared_auth.write_text(json.dumps({"tokens": {"refresh_token": "shared"}}), encoding="utf-8")
274
+ alias = tmp_path / "agent-zero-auth.json"
275
+ alias.hardlink_to(shared_auth)
276
+ monkeypatch.setenv("HOME", str(tmp_path))
277
+ monkeypatch.setattr(codex, "codex_config", lambda: {"auth_file_path": str(alias)})
278
+
279
+ with pytest.raises(RuntimeError, match="Agent Zero-owned auth file"):
280
+ codex.resolve_auth_write_path()
281
+
282
+
283
def test_write_auth_file_uses_atomic_replace_and_private_permissions(tmp_path, monkeypatch):
284
auth_path = tmp_path / "auth.json"
285
replacements: list[tuple[Path, Path]] = []