51 lines
1.8 KiB
Python
51 lines
1.8 KiB
Python
"""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)]
|