📦 EqualifyEverything / equalify-reflow

📄 cookies.py · 71 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71"""Cookie set/clear helpers.

Centralised so login, logout, and the middleware's sliding-window re-issue
all set the same flags. The ``X-Auth-Cookie-Set`` response header is a
sentinel: middleware checks it before re-issuing so that logout's
``Max-Age=0`` is never overwritten by a refresh in the same response cycle.
"""

from __future__ import annotations

from fastapi import Response

from ..config import settings
from . import csrf
from .base import Identity
from .factory import get_session_store


def set_session_cookies(*, response: Response, identity: Identity) -> None:
    """Set both the signed session cookie and its CSRF companion."""
    store = get_session_store()
    secret = settings.auth_secret_key
    if secret is None:
        raise RuntimeError("auth_secret_key required to set session cookies")

    session_value = store.encode(identity)
    csrf_token = csrf.make_token(session_value, secret.get_secret_value())

    # Session cookie: HttpOnly so JS can't steal it; SameSite=Lax so the
    # OIDC redirect-back from the IdP carries it through (Strict would drop
    # the cookie on the post-redirect first request and silently break login).
    response.set_cookie(
        key=settings.auth_session_cookie_name,
        value=session_value,
        max_age=settings.auth_session_ttl_seconds,
        secure=settings.auth_cookie_secure,
        httponly=True,
        samesite="lax",
        path="/",
    )
    # CSRF companion: NOT HttpOnly — the SPA needs to read it to echo as a
    # header on non-GET requests. Same Secure/SameSite/path as the session.
    response.set_cookie(
        key=f"{settings.auth_session_cookie_name}_csrf",
        value=csrf_token,
        max_age=settings.auth_session_ttl_seconds,
        secure=settings.auth_cookie_secure,
        httponly=False,
        samesite="lax",
        path="/",
    )
    response.headers["X-Auth-Cookie-Set"] = "1"


def clear_session_cookies(response: Response) -> None:
    """Expire both cookies on logout."""
    for name in (
        settings.auth_session_cookie_name,
        f"{settings.auth_session_cookie_name}_csrf",
    ):
        response.set_cookie(
            key=name,
            value="",
            max_age=0,
            secure=settings.auth_cookie_secure,
            httponly=(name == settings.auth_session_cookie_name),
            samesite="lax",
            path="/",
        )
    response.headers["X-Auth-Cookie-Set"] = "1"