fix(auth): address PR #758 follow-up review
Six findings from the latest claude-bot review on PR #758: - JWKS cache had no kid-miss refresh path (Medium): on IdP key rotation every login failed for up to _OIDC_CACHE_TTL. Evict and refetch once before raising, per OIDC core §10.1.1. - _should_use_secure_cookies fell back to nextcloud_host scheme, but the cookie is issued by the MCP server. Switch to settings.nextcloud_mcp_server_url so split-scheme deployments get the right Secure flag. - _origin_matches_self compared raw netloc strings, which include the port. Browsers omit default ports per RFC 6454 §6.2; an mcp_server_url like :443 falsely 403'd every legitimate logout. Normalise (scheme, host, port) tuples with default ports stripped. - delete_oauth_session exists in storage.py — drop the stale "we don't have this method" comment and call it eagerly so replays can't be processed and the table doesn't accumulate completed-but-not-yet-expired browser-login rows. - extract_user_id_from_token's unused ctx param renamed to _ctx to signal "intentionally unused" at the signature level. - provisioning_decorator instantiated RefreshTokenStorage per call. Switch to get_shared_storage() for the lock-protected process-wide singleton. Plus pre-push self-review catch: lazy-logging on the unchanged except arm in session_backend.py. Adds 5 regression tests: - JWKS rotation: success on refetch - JWKS rotation: still-missing-kid surfaces original error - JWKS rotation: network error during refresh wrapped as IdTokenVerificationError - default-port CSRF: explicit :443 in config + portless Origin - default-port CSRF: portless config + explicit :443 in Origin - scheme-mismatch CSRF: same host, different scheme rejected 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
af25c281bf
commit
2d340a5a6b
@@ -192,6 +192,74 @@ async def test_logout_allows_same_origin_post(storage):
|
||||
assert await storage.get_browser_session_user("sid-Y") is None
|
||||
|
||||
|
||||
async def test_logout_allows_same_origin_post_with_explicit_default_port(storage):
|
||||
"""mcp_server_url has explicit :443; browser Origin omits the port.
|
||||
|
||||
RFC 6454 §6.2: browsers omit default ports in Origin headers. The
|
||||
netloc string ``mcp.example.com:443`` would never match ``mcp.example.com``
|
||||
without port normalisation, blocking every legitimate logout.
|
||||
"""
|
||||
await storage.create_browser_session(session_id="sid-PE", user_id="alice")
|
||||
|
||||
request = _build_request(
|
||||
cookie="sid-PE",
|
||||
oauth_context={
|
||||
"storage": storage,
|
||||
"config": {
|
||||
"mcp_server_url": "https://mcp.example.com:443",
|
||||
"discovery_url": None,
|
||||
},
|
||||
},
|
||||
headers={"origin": "https://mcp.example.com"},
|
||||
)
|
||||
|
||||
response = await oauth_logout(request)
|
||||
assert response.status_code == 302
|
||||
assert await storage.get_browser_session_user("sid-PE") is None
|
||||
|
||||
|
||||
async def test_logout_allows_same_origin_post_with_default_port_in_origin(storage):
|
||||
"""Symmetric case: config omits port, Origin includes :443."""
|
||||
await storage.create_browser_session(session_id="sid-PI", user_id="alice")
|
||||
|
||||
request = _build_request(
|
||||
cookie="sid-PI",
|
||||
oauth_context={
|
||||
"storage": storage,
|
||||
"config": {
|
||||
"mcp_server_url": "https://mcp.example.com",
|
||||
"discovery_url": None,
|
||||
},
|
||||
},
|
||||
headers={"origin": "https://mcp.example.com:443"},
|
||||
)
|
||||
|
||||
response = await oauth_logout(request)
|
||||
assert response.status_code == 302
|
||||
assert await storage.get_browser_session_user("sid-PI") is None
|
||||
|
||||
|
||||
async def test_logout_blocks_scheme_mismatch(storage):
|
||||
"""Same hostname but different scheme must be treated as cross-origin."""
|
||||
await storage.create_browser_session(session_id="sid-SC", user_id="alice")
|
||||
|
||||
request = _build_request(
|
||||
cookie="sid-SC",
|
||||
oauth_context={
|
||||
"storage": storage,
|
||||
"config": {
|
||||
"mcp_server_url": "https://mcp.example.com",
|
||||
"discovery_url": None,
|
||||
},
|
||||
},
|
||||
headers={"origin": "http://mcp.example.com"},
|
||||
)
|
||||
|
||||
response = await oauth_logout(request)
|
||||
assert response.status_code == 403
|
||||
assert await storage.get_browser_session_user("sid-SC") == "alice"
|
||||
|
||||
|
||||
async def test_logout_allows_referer_when_origin_missing(storage):
|
||||
"""Some browsers strip Origin on POST; Referer is the fallback signal."""
|
||||
await storage.create_browser_session(session_id="sid-Z", user_id="alice")
|
||||
|
||||
Reference in New Issue
Block a user