fix(auth): address PR #758 round-5 medium/low review

Three findings from the latest review on #758 (1 medium, 2 low):

Medium:
- browser_oauth_routes.oauth_logout: move delete_browser_session into a
  finally block so an error from delete_refresh_token can no longer leave
  an orphan browser_sessions row. The orphan was not exploitable
  (SessionAuthBackend rejects sessions without a live refresh token), but
  it lingered until the hourly cleanup cron — a correctness gap. New
  regression test pins the fix.

Low:
- oauth_callback_nextcloud: drop redundant ``or None`` from
  ``expected_nonce=nonce``. ``nonce`` is already ``str | None`` and
  ``secrets.token_urlsafe`` never produces an empty string, so the
  coercion was a no-op that could mislead future readers into thinking
  empty-string was a valid skip-the-check path.
- storage.RefreshTokenStorage.initialize: fail fast at startup when
  SQLite < 3.35, since ``DELETE ... RETURNING`` (used in
  ``delete_browser_session``) needs that minimum. Ubuntu 20.04 ships
  3.31 and would otherwise hit OperationalError on every logout.
  Prerequisite also documented in docs/installation.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-03 01:37:38 +02:00
co-authored by Claude Opus 4.7
parent b696541918
commit e2955e8246
5 changed files with 86 additions and 5 deletions
@@ -683,11 +683,22 @@ async def oauth_logout(request: Request) -> RedirectResponse | JSONResponse:
await _revoke_refresh_token_at_idp(oauth_ctx, refresh_token)
await storage.delete_refresh_token(user_id)
logger.info("Refresh token revoked + deleted for user %s", user_id)
await storage.delete_browser_session(session_id)
except Exception as e:
# Logout must always succeed locally; log and continue.
logger.warning("Logout cleanup failed (continuing): %s", e)
finally:
# Always drop the browser_sessions row, even when the
# refresh-token cleanup above failed — otherwise an orphan
# row lingers until the hourly cleanup cron (PR #758 round-5
# review medium 1). Not exploitable (SessionAuthBackend
# already rejects sessions without a live refresh token), but
# a correctness gap worth closing here.
try:
await storage.delete_browser_session(session_id)
except Exception as e:
logger.warning(
"Failed to delete browser session %s…: %s", session_id[:8], e
)
response = RedirectResponse(next_url, status_code=302)
response.delete_cookie("mcp_session")
+6 -3
View File
@@ -647,15 +647,18 @@ async def oauth_callback_nextcloud(request: Request):
# Verify ID token signature + claims (issue #626 finding 1).
# ``expected_nonce`` is the per-request nonce stored on the
# oauth_session row (PR #758 round-3 finding 1); falsy → skip nonce
# check for sessions written before the column existed.
# oauth_session row (PR #758 round-3 finding 1). ``nonce`` is already
# ``str | None`` and ``secrets.token_urlsafe`` never produces an empty
# string, so passing it directly is correct — pre-migration-006 rows
# surface as ``None`` from ``oauth_session.get("nonce")``, which
# ``verify_id_token`` already treats as "skip the check".
logger.info("oauth_callback_nextcloud: Verifying ID token")
try:
userinfo = await verify_id_token(
id_token,
discovery_url=discovery_url,
expected_audience=mcp_server_client_id,
expected_nonce=nonce or None,
expected_nonce=nonce,
)
except IdTokenVerificationError as e:
logger.error("ID token verification failed: %s", e)
+16
View File
@@ -29,6 +29,7 @@ import json
import logging
import os
import socket
import sqlite3
import time
from pathlib import Path
from typing import Any
@@ -139,10 +140,25 @@ class RefreshTokenStorage:
1. New database: Run migrations from scratch
2. Pre-Alembic database: Stamp with initial revision (no changes)
3. Alembic-managed database: Upgrade to latest version
Raises:
RuntimeError: when the underlying SQLite library is older than
3.35, which is required for ``DELETE ... RETURNING`` used by
``delete_browser_session`` (PR #758 round-5 review low 2).
Ubuntu 20.04 ships SQLite 3.31, so deployers on that
baseline must upgrade or use a newer Python image.
"""
if self._initialized:
return
if sqlite3.sqlite_version_info < (3, 35):
raise RuntimeError(
"SQLite >= 3.35 is required (DELETE ... RETURNING is used "
"by delete_browser_session); detected "
f"{sqlite3.sqlite_version}. Upgrade SQLite or use a Python "
"image with a newer bundled libsqlite3."
)
# Ensure directory exists
db_dir = Path(self.db_path).parent
db_dir.mkdir(parents=True, exist_ok=True)