File size: 4,804 Bytes
c1a683f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
"""HMAC-signed session cookies — no database, no session store.

The cookie payload is a base64 JSON blob {uid, username, exp} followed by an
HMAC-SHA256 signature over that blob using SESSION_SECRET. Tampering with the
payload invalidates the signature; expiry is checked server-side.

This is a stateless session: the cookie IS the session. Revocation means
rotating SESSION_SECRET (which invalidates every outstanding session).
"""
from __future__ import annotations   # defer annotation evaluation (PEP 563)

import base64   # url-safe base64 encoding of the payload + signature
import hashlib   # sha256 for the HMAC
import hmac   # constant-time signature comparison (prevents timing attacks)
import json   # serialize the session payload to JSON
import os   # read SESSION_SECRET env var
import time   # unix timestamps for expiry math

COOKIE_NAME = "dr_session"   # the browser cookie name
COOKIE_MAX_AGE = 7 * 24 * 60 * 60   # 7 days in seconds (cookie lifetime)

# The signing secret. MUST be set in production (HF Space secret). If unset,
# sessions are disabled and only Bearer-key auth works.
_SECRET = os.environ.get("SESSION_SECRET", "").encode("utf-8")   # bytes for HMAC


def _sign(payload_b64: str) -> str:   # HMAC-SHA256 over the base64 payload
    """Return a url-safe base64 HMAC signature over the payload string."""
    mac = hmac.new(_SECRET, payload_b64.encode("utf-8"), hashlib.sha256).digest()   # compute HMAC
    return base64.urlsafe_b64encode(mac).decode("ascii")   # encode signature to ascii string


def create_session(uid: str, username: str, reveal_key: str = "") -> str:   # build a signed session cookie value
    """Create a signed session cookie value for a Google user.

    reveal_key: an optional one-time plaintext API key to show the user at
    signup. Carried inside the HttpOnly signed cookie so it can't be forged
    or stolen by XSS. Cleared by /auth/clear-reveal after first display.
    """
    if not _SECRET:   # no secret configured
        raise RuntimeError("SESSION_SECRET is not set")   # refuse to issue sessions
    payload = {   # the session contents
        "uid": str(uid),   # provider user id (string for JSON safety)
        "username": username,   # display name or email (display handle)
        "exp": int(time.time()) + COOKIE_MAX_AGE,   # absolute expiry timestamp
    }
    if reveal_key:   # carry the one-time key reveal inside the signed cookie
        payload["reveal_key"] = reveal_key   # /me returns it once, then clear-reveal drops it
    payload_b64 = base64.urlsafe_b64encode(   # encode the JSON payload
        json.dumps(payload, separators=(",", ":")).encode("utf-8")   # compact JSON (no spaces)
    ).decode("ascii")   # to ascii string
    sig = _sign(payload_b64)   # sign the encoded payload
    return f"{payload_b64}.{sig}"   # payload.signature cookie value


def verify_session(cookie_value: str) -> dict | None:   # validate + decode a session cookie
    """Return the session payload if valid, else None.

    Checks: signature matches (constant-time), not expired, well-formed.
    Returns None on ANY failure so callers can treat it as 'not logged in'.
    """
    if not _SECRET or not cookie_value:   # no secret OR empty cookie
        return None   # cannot verify
    parts = cookie_value.split(".", 1)   # split payload.signature
    if len(parts) != 2:   # malformed cookie
        return None   # reject
    payload_b64, sig = parts   # unpack
    expected = _sign(payload_b64)   # recompute the expected signature
    if not hmac.compare_digest(sig, expected):   # constant-time compare (timing-safe)
        return None   # signature mismatch -> tampered or forged
    try:   # decode the payload
        raw = base64.urlsafe_b64decode(payload_b64.encode("ascii"))   # base64 decode
        payload = json.loads(raw)   # parse JSON
    except Exception:   # corrupt payload
        return None   # reject
    if not isinstance(payload, dict):   # not a JSON object
        return None   # reject
    if int(payload.get("exp", 0)) < int(time.time()):   # expired?
        return None   # session expired
    return payload   # valid session -> return {uid, username, exp}


def cookie_kwargs() -> dict:   # the cookie attributes we always set
    """Return the kwargs for setting the session cookie (security flags)."""
    return {   # starlette set_cookie kwargs
        "key": COOKIE_NAME,   # cookie name
        "httponly": True,   # JS cannot read it (XSS protection)
        "secure": os.environ.get("SESSION_COOKIE_SECURE", "1") != "0",   # HTTPS-only in prod (set 0 for local http)
        "samesite": "lax",   # allows top-level navigation (OAuth callback) but blocks CSRF
        "max_age": COOKIE_MAX_AGE,   # 7 days
        "path": "/",   # cookie visible on all paths
    }