Files
webmail-client/backend/app/identity/deps.py
T

51 lines
1.8 KiB
Python
Raw Normal View History

2026-07-17 23:20:34 +02:00
"""Auth dependencies: current user and CSRF enforcement (SPEC §6.1, §10.3)."""
from __future__ import annotations
from typing import Annotated
from fastapi import Depends, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.errors import raise_api_error
from app.db import get_session
from app.identity.cookies import CSRF_HEADER, csrf_cookie_name, read_session_token
from app.identity.sessions import Session, resolve_session
from app.models import User
async def current_session(request: Request) -> Session:
"""Resolve the caller's session or raise 401."""
token = read_session_token(request)
session = await resolve_session(token)
if session is None:
raise_api_error(401, "not_authenticated", "authentication required")
return session
async def current_user(
session: Annotated[Session, Depends(current_session)],
db: Annotated[AsyncSession, Depends(get_session)],
) -> User:
"""Load the authenticated :class:`User` (I-7: scoped by the session's user_id)."""
user = await db.scalar(select(User).where(User.id == session.user_id))
if user is None:
# Session references a deleted user; treat as unauthenticated.
raise_api_error(401, "not_authenticated", "authentication required")
return user
async def require_csrf(request: Request) -> None:
"""Enforce the double-submit CSRF token on mutating requests (SPEC §10.3)."""
if request.method in ("GET", "HEAD", "OPTIONS"):
return
header = request.headers.get(CSRF_HEADER, "")
cookie = request.cookies.get(csrf_cookie_name(request), "")
if not header or not cookie or header != cookie:
raise_api_error(403, "csrf_failed", "missing or invalid CSRF token")
CurrentUser = Annotated[User, Depends(current_user)]
CurrentSession = Annotated[Session, Depends(current_session)]