From 58b56a09a489293592f1c982379369cd49fc6985 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 29 Jan 2026 20:24:26 +0000 Subject: [PATCH] Restrict remote auth to the existing granting user's credentials. --- paskia/fastapi/remote.py | 6 +++--- paskia/fastapi/ws.py | 4 +--- paskia/fastapi/wschat.py | 11 ++++++++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/paskia/fastapi/remote.py b/paskia/fastapi/remote.py index bd19ac6..6556a1f 100644 --- a/paskia/fastapi/remote.py +++ b/paskia/fastapi/remote.py @@ -17,7 +17,7 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect from paskia import db, remoteauth from paskia.authsession import expires -from paskia.fastapi.session import infodict +from paskia.fastapi.session import AUTH_COOKIE, infodict from paskia.fastapi.wschat import authenticate_and_login from paskia.fastapi.wsutil import validate_origin, websocket_error_handler from paskia.util import passphrase, pow, useragent @@ -252,7 +252,7 @@ async def websocket_remote_auth_request(ws: WebSocket): @app.websocket("/permit") @websocket_error_handler -async def websocket_remote_auth_permit(ws: WebSocket): +async def websocket_remote_auth_permit(ws: WebSocket, auth=AUTH_COOKIE): """Complete a remote authentication request using a 3-word pairing code. This endpoint is called from the user's profile on the authenticating device. @@ -310,7 +310,7 @@ async def websocket_remote_auth_permit(ws: WebSocket): # Handle authenticate request (no PoW needed - already validated during lookup) if msg.get("authenticate") and request is not None: - ctx = await authenticate_and_login(ws) + ctx = await authenticate_and_login(ws, auth) session_token = ctx.session.key reset_token = None diff --git a/paskia/fastapi/ws.py b/paskia/fastapi/ws.py index 97264c0..11d9ac6 100644 --- a/paskia/fastapi/ws.py +++ b/paskia/fastapi/ws.py @@ -89,14 +89,12 @@ async def websocket_authenticate(ws: WebSocket, auth=AUTH_COOKIE): # If there's an existing session, restrict to that user's credentials (reauth) session_user_uuid = None - credential_ids = None if auth: existing_ctx = db.data().session_ctx(auth, host) if existing_ctx: session_user_uuid = existing_ctx.user.uuid - credential_ids = db.get_user_credential_ids(session_user_uuid) or None - ctx = await authenticate_and_login(ws, credential_ids) + ctx = await authenticate_and_login(ws, auth) # If reauth mode, verify the credential belongs to the session's user if session_user_uuid and ctx.user.uuid != session_user_uuid: diff --git a/paskia/fastapi/wschat.py b/paskia/fastapi/wschat.py index 0f1c680..27ae74c 100644 --- a/paskia/fastapi/wschat.py +++ b/paskia/fastapi/wschat.py @@ -68,10 +68,12 @@ async def authenticate_chat( async def authenticate_and_login( ws: WebSocket, - credential_ids: list[bytes] | None = None, + auth: str | None = None, ) -> SessionContext: """Run WebAuthn authentication flow, create session, and return the session context. + If auth is provided, restrict authentication to credentials of that session's user. + Returns: SessionContext for the authenticated session """ @@ -86,6 +88,13 @@ async def authenticate_and_login( raise ValueError(f"Host must be the same as or a subdomain of {rp_id}") metadata = infodict(ws, "auth") + # Get credential IDs if restricting to a user's credentials + credential_ids = None + if auth: + existing_ctx = db.data().session_ctx(auth, host) + if existing_ctx: + credential_ids = db.get_user_credential_ids(existing_ctx.user.uuid) or None + cred, new_sign_count = await authenticate_chat(ws, credential_ids) # Create session and update user/credential