From fbecbb98b5476d93bb9d0588dedcc317e48a5bfd Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 24 Jun 2026 03:55:39 +0530 Subject: [PATCH] fix(auth):harden session cookie transport --- surfsense_backend/app/app.py | 2 +- surfsense_backend/app/auth/session_cookies.py | 63 +++++++++++++++---- surfsense_backend/app/config/__init__.py | 1 + 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index c2830ed98..9122e5d43 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -807,6 +807,7 @@ allowed_origins.extend( ] ) +app.add_middleware(CsrfOriginMiddleware) app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, @@ -821,7 +822,6 @@ app.add_middleware( # FRONTEND_URL to BACKEND_URL. max_age=86400, ) -app.add_middleware(CsrfOriginMiddleware) # Password / email-based auth routers are only mounted when not running in # Google-OAuth-only mode. Mounting them in OAuth-only prod previously left diff --git a/surfsense_backend/app/auth/session_cookies.py b/surfsense_backend/app/auth/session_cookies.py index 4e5be6131..024e4a0a9 100644 --- a/surfsense_backend/app/auth/session_cookies.py +++ b/surfsense_backend/app/auth/session_cookies.py @@ -3,13 +3,20 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta +from enum import Enum from typing import Any +import jwt from fastapi import Request, Response from app.config import config +class TransportMode(Enum): + COOKIE = "cookie" + HEADER = "header" + + def _cookie_secure(request: Request | None = None) -> bool: policy = config.SESSION_COOKIE_SECURE_POLICY if policy == "always": @@ -49,7 +56,7 @@ def _set_persistent_cookie( def write_session( response: Response, access: str, - refresh: str, + refresh: str | None = None, request: Request | None = None, ) -> None: _set_persistent_cookie( @@ -59,13 +66,14 @@ def write_session( max_age=config.ACCESS_TOKEN_LIFETIME_SECONDS, request=request, ) - _set_persistent_cookie( - response, - key=config.REFRESH_COOKIE_NAME, - value=refresh, - max_age=config.REFRESH_TOKEN_LIFETIME_SECONDS, - request=request, - ) + if refresh is not None: + _set_persistent_cookie( + response, + key=config.REFRESH_COOKIE_NAME, + value=refresh, + max_age=config.REFRESH_TOKEN_LIFETIME_SECONDS, + request=request, + ) def clear_session(response: Response, request: Request | None = None) -> None: @@ -80,10 +88,41 @@ def clear_session(response: Response, request: Request | None = None) -> None: ) -def read_refresh(request: Request, body: Any | None = None) -> str | None: +def read_refresh(request: Request, body: Any | None = None) -> tuple[str | None, TransportMode]: cookie = request.cookies.get(config.REFRESH_COOKIE_NAME) if cookie: - return cookie + return cookie, TransportMode.COOKIE if body is None: - return None - return getattr(body, "refresh_token", None) + return None, TransportMode.HEADER + return getattr(body, "refresh_token", None), TransportMode.HEADER + + +def access_expires_at(access_token: str) -> int: + payload = jwt.decode( + access_token, + config.SECRET_KEY, + algorithms=["HS256"], + options={"verify_aud": False}, + ) + return int(payload["exp"]) + + +def issue( + response: Response, + mode: TransportMode, + *, + access: str, + refresh: str | None, + access_expires_at: int, + request: Request | None = None, +) -> dict: + if mode is TransportMode.COOKIE: + write_session(response, access, refresh, request) + return {"authenticated": True, "access_expires_at": access_expires_at} + + return { + "access_token": access, + "refresh_token": refresh, + "token_type": "bearer", + "access_expires_at": access_expires_at, + } diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 0cfe0818a..f4f44a385 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -918,6 +918,7 @@ class Config: ACCESS_TOKEN_LIFETIME_SECONDS = int( os.getenv("ACCESS_TOKEN_LIFETIME_SECONDS", str(30 * 60)) # 30 minutes ) + MIN_ISSUED_AT = int(os.getenv("MIN_ISSUED_AT", "0")) REFRESH_TOKEN_LIFETIME_SECONDS = int( os.getenv("REFRESH_TOKEN_LIFETIME_SECONDS", str(14 * 24 * 60 * 60)) # 2 weeks )