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]] = []