Skip to content

auth

tit.server.auth

Authentication helpers: token → session cookie exchange, bearer/cookie checks.

The launcher calls /auth/session?token=… exactly once; the server mints a random session id, remembers it in app.state.sessions and sets it as an HttpOnly; SameSite=Strict; Path=/ cookie before redirecting to /. The cookie never carries the token itself, and a server restart forgets every session (the UI then gets 401 and returns to the launcher). Every other /api/* and /ws/* route accepts that cookie or Authorization: Bearer <token> (scripts). POST /auth/logout forgets the session and clears the cookie.

new_session_id

new_session_id() -> str

Random, unguessable session id (never derived from the token).

Source code in tit/server/auth.py
def new_session_id() -> str:
    """Random, unguessable session id (never derived from the token)."""
    return secrets.token_urlsafe(32)

is_authorized

is_authorized(token: str, sessions: set[str], *, cookie: str | None, authorization: str | None) -> bool

True if the cookie names a live session or the bearer credential matches.

Source code in tit/server/auth.py
def is_authorized(
    token: str,
    sessions: set[str],
    *,
    cookie: str | None,
    authorization: str | None,
) -> bool:
    """True if the cookie names a live session or the bearer credential matches."""
    if cookie and cookie in sessions:
        return True
    return _token_matches(_bearer(authorization), token)

require_auth

require_auth(request: Request) -> None

FastAPI dependency: 401 unless the request carries valid credentials.

A matching Authorization: Bearer is always sufficient. A cookie is sufficient for a "safe" method outright, and for any other method only once :func:_cookie_csrf_ok also passes -- otherwise this is a 403, not a 401 (the credential is valid; the request is just not trusted to have come from this app).

Source code in tit/server/auth.py
def require_auth(request: Request) -> None:
    """FastAPI dependency: 401 unless the request carries valid credentials.

    A matching ``Authorization: Bearer`` is always sufficient. A cookie is sufficient for a
    "safe" method outright, and for any other method only once :func:`_cookie_csrf_ok` also
    passes -- otherwise this is a 403, not a 401 (the credential is valid; the request is just
    not trusted to have come from this app).
    """
    state = request.app.state
    if _token_matches(
        _bearer(request.headers.get("authorization")), state.settings.token
    ):
        return
    cookie = request.cookies.get(COOKIE_NAME)
    if cookie and cookie in state.sessions:
        if request.method.upper() in _SAFE_METHODS or _cookie_csrf_ok(request):
            return
        raise HTTPException(
            status_code=403,
            detail="Cross-site request blocked: missing or foreign Origin",
        )
    raise HTTPException(status_code=401, detail="Unauthorized")

origin_allowed

origin_allowed(origin: str | None, host: str | None, dev_origins: tuple[str, ...] = (), *, secure: bool = False) -> bool

WebSocket origin policy.

Accepted: no Origin at all (curl, tests, scripts); an origin whose scheme matches the connection (http for ws, https for wss) and whose host[:port] equals the request's Host header (same origin); or an exact entry of dev_origins (--dev-origin / TIT_DEV_ORIGINS, e.g. the Vite dev server).

Source code in tit/server/auth.py
def origin_allowed(
    origin: str | None,
    host: str | None,
    dev_origins: tuple[str, ...] = (),
    *,
    secure: bool = False,
) -> bool:
    """WebSocket origin policy.

    Accepted: no ``Origin`` at all (curl, tests, scripts); an origin whose
    scheme matches the connection (``http`` for ``ws``, ``https`` for
    ``wss``) and whose ``host[:port]`` equals the request's ``Host`` header
    (same origin); or an exact entry of *dev_origins* (``--dev-origin`` /
    ``TIT_DEV_ORIGINS``, e.g. the Vite dev server).
    """
    if origin is None:
        return True
    if origin in dev_origins:
        return True
    parts = urlsplit(origin)
    if parts.scheme != ("https" if secure else "http") or not parts.netloc:
        return False
    return bool(host) and parts.netloc.lower() == host.lower()

websocket_authorized

websocket_authorized(ws: WebSocket) -> bool

Cookie, bearer header or ?token= for WebSocket handshakes.

Source code in tit/server/auth.py
def websocket_authorized(ws: WebSocket) -> bool:
    """Cookie, bearer header or ``?token=`` for WebSocket handshakes."""
    state = ws.app.state
    if is_authorized(
        state.settings.token,
        state.sessions,
        cookie=ws.cookies.get(COOKIE_NAME),
        authorization=ws.headers.get("authorization"),
    ):
        return True
    return _token_matches(ws.query_params.get("token"), state.settings.token)