Add host-based authentication, UTC timestamps, session management, and secure cookies; fix styling issues; refactor to remove module; update database schema for sessions and reset tokens.

This commit is contained in:
Leo Vasanko
2025-10-03 18:31:54 -06:00
parent 963ab06664
commit 591ea626bf
29 changed files with 1489 additions and 611 deletions

View File

@@ -5,9 +5,9 @@ from uuid import UUID
from fastapi import Cookie, FastAPI, WebSocket, WebSocketDisconnect
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from ..authsession import create_session, expires, get_reset, get_session
from ..authsession import create_session, get_reset, get_session
from ..globals import db, passkey
from ..util import passphrase
from ..util import hostutil, passphrase
from ..util.tokens import create_token, session_key
from .session import infodict
@@ -56,7 +56,10 @@ async def register_chat(
@app.websocket("/register")
@websocket_error_handler
async def websocket_register_add(
ws: WebSocket, reset: str | None = None, name: str | None = None, auth=Cookie(None)
ws: WebSocket,
reset: str | None = None,
name: str | None = None,
auth=Cookie(None, alias="__Host-auth"),
):
"""Register a new credential for an existing user.
@@ -65,6 +68,9 @@ async def websocket_register_add(
- Reset token supplied as ?reset=... (auth cookie ignored)
"""
origin = ws.headers["origin"]
host = hostutil.normalize_host(ws.headers.get("host"))
if host is None:
raise ValueError("Missing host header")
if reset is not None:
if not passphrase.is_well_formed(reset):
raise ValueError("Invalid reset token")
@@ -72,7 +78,7 @@ async def websocket_register_add(
else:
if not auth:
raise ValueError("Authentication Required")
s = await get_session(auth)
s = await get_session(auth, host=host)
user_uuid = s.user_uuid
# Get user information and determine effective user_name for this registration
@@ -89,14 +95,16 @@ async def websocket_register_add(
# Create a new session and store everything in database
token = create_token()
metadata = infodict(ws, "authenticated")
await db.instance.create_credential_session( # type: ignore[attr-defined]
user_uuid=user_uuid,
credential=credential,
reset_key=(s.key if reset is not None else None),
session_key=session_key(token),
session_expires=expires(),
session_info=infodict(ws, "authenticated"),
display_name=user_name,
host=host,
ip=metadata.get("ip"),
user_agent=metadata.get("user_agent"),
)
auth = token
@@ -115,6 +123,9 @@ async def websocket_register_add(
@websocket_error_handler
async def websocket_authenticate(ws: WebSocket):
origin = ws.headers["origin"]
host = hostutil.normalize_host(ws.headers.get("host"))
if host is None:
raise ValueError("Missing host header")
options, challenge = passkey.instance.auth_generate_options()
await ws.send_json(options)
# Wait for the client to use his authenticator to authenticate
@@ -128,10 +139,13 @@ async def websocket_authenticate(ws: WebSocket):
# Create a session token for the authenticated user
assert stored_cred.uuid is not None
metadata = infodict(ws, "auth")
token = await create_session(
user_uuid=stored_cred.user_uuid,
info=infodict(ws, "auth"),
credential_uuid=stored_cred.uuid,
host=host,
ip=metadata.get("ip") or "",
user_agent=metadata.get("user_agent") or "",
)
await ws.send_json(