fix(auth): address PR #758 round-6 medium/low review
Five findings from the latest review on #758 (2 medium, 3 nit): Medium: - browser_oauth_routes.oauth_login_callback + oauth_routes.oauth_callback_nextcloud: fail closed with 400 when the oauth_session row is unknown/expired. Previously both callbacks fell through with code_verifier="" and expected_nonce=None, silently bypassing the PKCE + nonce protections introduced in earlier rounds. Symmetric unit tests pin both contracts. - token_utils.verify_id_token: use secrets.compare_digest for the nonce check instead of short-circuit !=. Mirrors the sibling PKCE verifier comparison; closes the last secret-equality timing-side-channel surface in the auth path. Nit: - Tighten the comment at all 4 mcp_authorization_code/code_verifier store + retrieve sites so a future refactor sees the field reuse immediately (renaming the column requires a schema migration). - _should_use_secure_cookies: explicit string normalisation instead of bool(settings.cookie_secure). Dynaconf normally coerces but tests / direct settings.set calls can leave the raw string in place — bool("false") is True. New parametrized unit tests cover the coercion matrix + http/https fallback. - oauth_routes.py:591 f-string log converted to lazy %s formatting (folded into the Flow 2 callback rewrite). 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
e2955e8246
commit
ec9b9b2a75
@@ -479,7 +479,10 @@ async def oauth_authorize_nextcloud(
|
||||
state=state,
|
||||
code_challenge=code_challenge,
|
||||
code_challenge_method="S256",
|
||||
mcp_authorization_code=code_verifier, # Store code_verifier here temporarily
|
||||
# `mcp_authorization_code` field reused to store the PKCE
|
||||
# code_verifier (one-time-use). Renaming the column requires a
|
||||
# schema migration.
|
||||
mcp_authorization_code=code_verifier,
|
||||
nonce=nonce,
|
||||
flow_type="flow2",
|
||||
ttl_seconds=600, # 10 minutes
|
||||
@@ -580,22 +583,31 @@ async def oauth_callback_nextcloud(request: Request):
|
||||
oauth_config = oauth_ctx["config"]
|
||||
|
||||
# Retrieve code_verifier + nonce from session storage (PKCE + OIDC
|
||||
# nonce binding both required for Flow 2 — round-3 finding 1).
|
||||
code_verifier = ""
|
||||
nonce: str | None = None
|
||||
# nonce binding both required for Flow 2 — round-3 finding 1). Fail
|
||||
# closed when the row is missing/expired so PKCE + nonce verification
|
||||
# are not silently bypassed (round-6 review).
|
||||
oauth_session = await storage.get_oauth_session(state)
|
||||
if oauth_session:
|
||||
# code_verifier was stored in mcp_authorization_code field
|
||||
code_verifier = oauth_session.get("mcp_authorization_code", "")
|
||||
nonce = oauth_session.get("nonce")
|
||||
logger.info(
|
||||
f"Retrieved code_verifier for Flow 2 callback (state={state[:16]}...)"
|
||||
if not oauth_session:
|
||||
logger.warning("Flow 2 callback received unknown/expired state=%s", state[:16])
|
||||
return JSONResponse(
|
||||
{
|
||||
"error": "invalid_request",
|
||||
"error_description": (
|
||||
"Unknown or expired session — please retry the OAuth flow"
|
||||
),
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
# One-time-use session: delete eagerly so the stored code_verifier
|
||||
# can't be replayed for the remainder of the oauth_sessions TTL.
|
||||
# Mirrors browser_oauth_routes.oauth_login_callback (PR #758
|
||||
# follow-up review).
|
||||
await storage.delete_oauth_session(state)
|
||||
# `mcp_authorization_code` field reused to store the PKCE code_verifier
|
||||
# (one-time-use). Renaming the column requires a schema migration.
|
||||
code_verifier = oauth_session.get("mcp_authorization_code", "")
|
||||
nonce = oauth_session.get("nonce")
|
||||
logger.info("Retrieved code_verifier for Flow 2 callback (state=%s…)", state[:16])
|
||||
# One-time-use session: delete eagerly so the stored code_verifier
|
||||
# can't be replayed for the remainder of the oauth_sessions TTL.
|
||||
# Mirrors browser_oauth_routes.oauth_login_callback (PR #758
|
||||
# follow-up review).
|
||||
await storage.delete_oauth_session(state)
|
||||
|
||||
# Exchange code for tokens
|
||||
mcp_server_client_id = os.getenv(
|
||||
|
||||
Reference in New Issue
Block a user