Files
paskia/paskia/fastapi/session.py
T
LeoVasanko 3ff41ca354 Review fixes: validation hardening, dead code, stale comments
- validate_config: reject multiple auth-host marks per domain;
  sanitize_config clears extras (first wins) and coerces junk entry
  values to presence-only
- origin_key: lowercase keys, strip trailing dots (bare hosts/wildcards)
- Passkey._allowlisted: tolerate trailing-dot wildcard bases
- wschat: stamp remote-flow sessions with the session host's domain,
  not the approver's
- auth_host redirects: keep the port (redirect to the configured auth
  host instead of the normalized, port-less current host)
- update_domain: required fields (wholesale replace) — no silent wipes
- admin: fix pre-existing lockout-guard order in org permission removal;
  permission PATCH keeps domain restriction when omitted; 400 instead of
  500 on unknown permission UUIDs
- Drop dead code: db.update_config/set_session_host/delete_reset_token,
  Session.metadata, oidjwt.clear_key, background aliases,
  avatar.current_avatar_url/media_root, wsutil.require_pow
- Prune stale/duplicated comments and docstrings
2026-09-07 07:53:27 +00:00

87 lines
2.5 KiB
Python

"""
FastAPI-specific session management for WebAuthn authentication.
This module provides FastAPI-specific session management functionality:
- Extracting client information from FastAPI requests
- Setting and clearing HTTP-only cookies via FastAPI Response objects
"""
from ipaddress import IPv4Address, IPv6Address
from fastapi import Cookie, Request, Response, WebSocket
from paskia.authsession import EXPIRES
AUTH_COOKIE_NAME = "__Host-paskia"
AUTH_COOKIE = Cookie(None, alias=AUTH_COOKIE_NAME)
def normalize_ip(ip: str) -> str:
"""Normalize IP address, stripping brackets and validating format.
Proxies may pass IPv6 in brackets like [::1] or with zone IDs.
IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) are converted to plain IPv4.
Returns empty string for invalid addresses.
"""
if not ip:
return ""
# Strip brackets that some proxies add around IPv6
ip = ip.strip("[]")
# Strip zone ID (e.g., fe80::1%eth0)
if "%" in ip:
ip = ip.split("%")[0]
try:
# Validate and normalize
if ":" in ip:
addr = IPv6Address(ip)
# Convert IPv4-mapped addresses to plain IPv4
if addr.ipv4_mapped:
return str(addr.ipv4_mapped)
return str(addr)
return str(IPv4Address(ip))
except ValueError:
return ip # Return as-is if not a valid IP (could be hostname)
def get_client_ip(request: Request | WebSocket) -> str:
"""Get client IP from request, normalized."""
if not request.client:
return ""
return normalize_ip(request.client.host)
def infodict(request: Request | WebSocket, type: str) -> dict:
"""Extract client information from request."""
return {
"ip": get_client_ip(request),
"user_agent": request.headers.get("user-agent", "")[:500],
"session_type": type,
}
def set_session_cookie(response: Response, token: str) -> None:
"""Set the session token as an HTTP-only cookie."""
response.set_cookie(
key=AUTH_COOKIE_NAME,
value=token,
max_age=int(EXPIRES.total_seconds()),
httponly=True,
secure=True,
path="/",
samesite="strict",
)
def clear_session_cookie(response: Response) -> None:
# FastAPI's delete_cookie does not set the secure attribute
response.set_cookie(
key=AUTH_COOKIE_NAME,
value="",
max_age=0,
expires=0,
httponly=True,
secure=True,
path="/",
samesite="strict",
)