fix(auth): address PR #758 round-7 important review
- Coerce refresh_expires_in to int before arithmetic in both callback paths so IdPs that serialize the field as a JSON string (e.g. AWS Cognito) don't trigger an unhandled TypeError 500. - Drop the orphaned oauth_session row written by _check_logged_in. The canonical Flow 2 row is created by generate_oauth_url_for_flow2 keyed by `state`, which is what the unified callback looks up; the flow2_<hex> session_id was never matched and just churned the table for 10 minutes per call. - Match delete_cookie attributes (httponly, secure, samesite) to the set_cookie call on logout so browsers reliably evict the cookie even on implementations that consider security flags during deletion. 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
ec9b9b2a75
commit
27fcf05d3a
@@ -554,7 +554,9 @@ async def oauth_login_callback(request: Request) -> RedirectResponse | HTMLRespo
|
|||||||
refresh_expires_in = token_data.get("refresh_expires_in")
|
refresh_expires_in = token_data.get("refresh_expires_in")
|
||||||
refresh_expires_at = None
|
refresh_expires_at = None
|
||||||
if refresh_expires_in:
|
if refresh_expires_in:
|
||||||
refresh_expires_at = int(time.time()) + refresh_expires_in
|
# Some IdPs (e.g. AWS Cognito) return refresh_expires_in as a JSON
|
||||||
|
# string rather than an int; coerce to be safe.
|
||||||
|
refresh_expires_at = int(time.time()) + int(refresh_expires_in)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Refresh token expires in %ss (at timestamp %s)",
|
"Refresh token expires in %ss (at timestamp %s)",
|
||||||
refresh_expires_in,
|
refresh_expires_in,
|
||||||
@@ -714,7 +716,15 @@ async def oauth_logout(request: Request) -> RedirectResponse | JSONResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = RedirectResponse(next_url, status_code=302)
|
response = RedirectResponse(next_url, status_code=302)
|
||||||
response.delete_cookie("mcp_session")
|
# Match the attributes from set_cookie so browsers reliably evict the
|
||||||
|
# cookie even on edge-case implementations that consider security flags
|
||||||
|
# when matching for deletion.
|
||||||
|
response.delete_cookie(
|
||||||
|
"mcp_session",
|
||||||
|
httponly=True,
|
||||||
|
secure=_should_use_secure_cookies(),
|
||||||
|
samesite="lax",
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("User logged out, session cookie cleared")
|
logger.info("User logged out, session cookie cleared")
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -699,7 +699,9 @@ async def oauth_callback_nextcloud(request: Request):
|
|||||||
refresh_expires_in = token_data.get("refresh_expires_in")
|
refresh_expires_in = token_data.get("refresh_expires_in")
|
||||||
refresh_expires_at = None
|
refresh_expires_at = None
|
||||||
if refresh_expires_in:
|
if refresh_expires_in:
|
||||||
refresh_expires_at = int(time.time()) + refresh_expires_in
|
# Some IdPs (e.g. AWS Cognito) return refresh_expires_in as a JSON
|
||||||
|
# string rather than an int; coerce to be safe.
|
||||||
|
refresh_expires_at = int(time.time()) + int(refresh_expires_in)
|
||||||
logger.info(" refresh_expires_in: %ss", refresh_expires_in)
|
logger.info(" refresh_expires_in: %ss", refresh_expires_in)
|
||||||
logger.info(" refresh_expires_at: %s", refresh_expires_at)
|
logger.info(" refresh_expires_at: %s", refresh_expires_at)
|
||||||
|
|
||||||
|
|||||||
@@ -432,21 +432,12 @@ async def _check_logged_in(ctx: Context, user_id: str) -> str:
|
|||||||
# Store state in session for validation on callback
|
# Store state in session for validation on callback
|
||||||
storage = await get_shared_storage()
|
storage = await get_shared_storage()
|
||||||
|
|
||||||
# Create OAuth session for Flow 2. Identifier is purely random
|
# The canonical Flow 2 oauth_session row is written inside
|
||||||
# so audit-log entries / DB rows don't carry the user_id in the
|
# generate_oauth_url_for_flow2 (keyed by `state`, with the PKCE
|
||||||
# session_id field (PR #758 round-4 review medium 3).
|
# verifier and nonce); the unified callback looks it up by `state`.
|
||||||
session_id = f"flow2_{secrets.token_hex(16)}"
|
# No additional row is needed here.
|
||||||
redirect_uri = f"{os.getenv('NEXTCLOUD_MCP_SERVER_URL', 'http://localhost:8000')}/oauth/callback"
|
redirect_uri = f"{os.getenv('NEXTCLOUD_MCP_SERVER_URL', 'http://localhost:8000')}/oauth/callback"
|
||||||
|
|
||||||
await storage.store_oauth_session(
|
|
||||||
session_id=session_id,
|
|
||||||
client_redirect_uri="", # No client redirect for Flow 2
|
|
||||||
state=state,
|
|
||||||
flow_type="flow2",
|
|
||||||
is_provisioning=True,
|
|
||||||
ttl_seconds=600, # 10 minute TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define scopes for Nextcloud access
|
# Define scopes for Nextcloud access
|
||||||
# Note: offline_access is only included when enabled in settings.
|
# Note: offline_access is only included when enabled in settings.
|
||||||
# The actual scope sent to the IdP is determined by
|
# The actual scope sent to the IdP is determined by
|
||||||
|
|||||||
Reference in New Issue
Block a user