fix(auth): address PR #758 round-4 review
Seven findings from the latest review on #758 (3 medium, 4 low/nit): Medium: - storage.py: replace 5 ``assert self.cipher is not None`` sites with explicit ``RuntimeError`` so missing TOKEN_ENCRYPTION_KEY can't silently become an AttributeError under ``python -O`` - session_backend.py: document the silent-invalidation invariant — refresh-token TTL expiry without explicit logout deliberately makes the browser session unusable; future readers must not relax it - server/oauth_tools.py: drop user_id from the Flow 2 session_id identifier — use ``flow2_{secrets.token_hex(16)}`` so audit logs and DB rows don't carry user_id in the session_id field Low / nit: - token_utils.py: drop _fetch_locks dict entry in finally so a probed deployment can't grow the lock dict without bound; coalescing test now pins the invariant with len(_fetch_locks) == 0 - browser_oauth_routes.py: strip trailing slash from settings.nextcloud_host before constructing the well-known URL so a host configured as ``https://cloud.example.com/`` doesn't produce a double-slash - browser_oauth_routes.py: add comment explaining the three-layer CSRF policy on the mcp_session cookie set (SameSite=Lax + POST-only logout + Origin/Referer check) - oauth_routes.py: convert all 23 f-string log calls to lazy %-style per the CLAUDE.md / memory feedback_lazy_logging convention Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3a4fa8adc8
commit
b696541918
@@ -227,8 +227,14 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# Type narrowing: cipher is set after initialize()
|
||||
assert self.cipher is not None
|
||||
# ``assert`` is stripped under ``python -O``, which would silently
|
||||
# turn a missing TOKEN_ENCRYPTION_KEY into an ``AttributeError`` on
|
||||
# the next ``self.cipher.encrypt(...)``. Raise explicitly instead
|
||||
# (PR #758 round-4 review medium 1).
|
||||
if self.cipher is None:
|
||||
raise RuntimeError(
|
||||
"TOKEN_ENCRYPTION_KEY is not set — token storage operations unavailable"
|
||||
)
|
||||
encrypted_token = self.cipher.encrypt(refresh_token.encode())
|
||||
now = int(time.time())
|
||||
scopes_json = json.dumps(scopes) if scopes else None
|
||||
@@ -374,8 +380,14 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# Type narrowing: cipher is set after initialize()
|
||||
assert self.cipher is not None
|
||||
# ``assert`` is stripped under ``python -O``, which would silently
|
||||
# turn a missing TOKEN_ENCRYPTION_KEY into an ``AttributeError`` on
|
||||
# the next ``self.cipher.encrypt(...)``. Raise explicitly instead
|
||||
# (PR #758 round-4 review medium 1).
|
||||
if self.cipher is None:
|
||||
raise RuntimeError(
|
||||
"TOKEN_ENCRYPTION_KEY is not set — token storage operations unavailable"
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
@@ -461,8 +473,14 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# Type narrowing: cipher is set after initialize()
|
||||
assert self.cipher is not None
|
||||
# ``assert`` is stripped under ``python -O``, which would silently
|
||||
# turn a missing TOKEN_ENCRYPTION_KEY into an ``AttributeError`` on
|
||||
# the next ``self.cipher.encrypt(...)``. Raise explicitly instead
|
||||
# (PR #758 round-4 review medium 1).
|
||||
if self.cipher is None:
|
||||
raise RuntimeError(
|
||||
"TOKEN_ENCRYPTION_KEY is not set — token storage operations unavailable"
|
||||
)
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute(
|
||||
@@ -635,8 +653,14 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# Type narrowing: cipher is set after initialize()
|
||||
assert self.cipher is not None
|
||||
# ``assert`` is stripped under ``python -O``, which would silently
|
||||
# turn a missing TOKEN_ENCRYPTION_KEY into an ``AttributeError`` on
|
||||
# the next ``self.cipher.encrypt(...)``. Raise explicitly instead
|
||||
# (PR #758 round-4 review medium 1).
|
||||
if self.cipher is None:
|
||||
raise RuntimeError(
|
||||
"TOKEN_ENCRYPTION_KEY is not set — token storage operations unavailable"
|
||||
)
|
||||
|
||||
# Encrypt sensitive data
|
||||
encrypted_secret = self.cipher.encrypt(client_secret.encode())
|
||||
@@ -708,8 +732,14 @@ class RefreshTokenStorage:
|
||||
if not self._initialized:
|
||||
await self.initialize()
|
||||
|
||||
# Type narrowing: cipher is set after initialize()
|
||||
assert self.cipher is not None
|
||||
# ``assert`` is stripped under ``python -O``, which would silently
|
||||
# turn a missing TOKEN_ENCRYPTION_KEY into an ``AttributeError`` on
|
||||
# the next ``self.cipher.encrypt(...)``. Raise explicitly instead
|
||||
# (PR #758 round-4 review medium 1).
|
||||
if self.cipher is None:
|
||||
raise RuntimeError(
|
||||
"TOKEN_ENCRYPTION_KEY is not set — token storage operations unavailable"
|
||||
)
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
async with db.execute(
|
||||
|
||||
Reference in New Issue
Block a user