Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions backend/src/apiserver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def handler_with_client(
frontend_origin: str,
smtp_config: SmtpConfig | None = None,
smtp_send: bool = False,
environment: str = "production",
) -> Response:
"""Dispatch a request to its handler based on the route table."""
if store_queue is None:
Expand All @@ -103,13 +104,14 @@ def h_set_sess():
smtp_config,
smtp_send,
frontend_origin,
environment,
)

def h_clear_sess():
return clear_session(req)
return clear_session(req, environment)

def h_sess_info():
return session_info(auth_client, req, headers, store_queue)
return session_info(auth_client, req, headers, store_queue, environment)

def h_add_perm():
return add_user_permission(req, store_queue)
Expand Down Expand Up @@ -432,6 +434,7 @@ def handler(req: Request, store_queue: StorageQueue | None) -> Response:
settings.frontend_origin,
settings.smtp,
settings.smtp_send,
settings.environment,
)

if ready_event is None:
Expand Down
17 changes: 9 additions & 8 deletions backend/src/apiserver/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
InvalidSessionToken,
SESSION_COOKIE_PRIMARY,
SESSION_COOKIE_SECONDARY,
apply_session_cookie_security,
get_cookie_value,
make_clear_session_cookie_header,
validate_session,
Expand Down Expand Up @@ -211,6 +212,7 @@ def set_session(
smtp_config=None,
smtp_send: bool = False,
frontend_origin: str = "",
environment: str = "production",
) -> Response:
"""Handle /auth/set_session/ — validate and set session cookie."""
try:
Expand All @@ -236,10 +238,7 @@ def set_session(

cookie = SimpleCookie()
cookie[cookie_name] = session_token
cookie[cookie_name]["httponly"] = True
cookie[cookie_name]["samesite"] = "None"
cookie[cookie_name]["secure"] = True
cookie[cookie_name]["path"] = "/"
apply_session_cookie_security(cookie, cookie_name, environment)
cookie[cookie_name]["max-age"] = max_session_age

cookie_header = cookie[cookie_name].OutputString()
Expand All @@ -251,7 +250,7 @@ def set_session(
)


def clear_session(req: Request) -> Response:
def clear_session(req: Request, environment: str = "production") -> Response:
"""Handle /auth/clear_session/ — clear session cookie."""
secondary = False
if req.body:
Expand All @@ -263,14 +262,16 @@ def clear_session(req: Request) -> Response:

cookie_name = SESSION_COOKIE_SECONDARY if secondary else SESSION_COOKIE_PRIMARY
logger.info(f"clear_session: Clearing {cookie_name} cookie")
return Response.empty(headers=[make_clear_session_cookie_header(cookie_name)])
header = make_clear_session_cookie_header(cookie_name, environment)
return Response.empty(headers=[header])


def session_info(
auth_client: AuthClient,
req: Request,
headers: dict[str, str],
store_queue: StorageQueue,
environment: str = "production",
) -> Response:
"""Handle /auth/session_info/ — returns current user info and permissions.

Expand All @@ -294,7 +295,7 @@ def session_info(
return Response.json(
{"error": "invalid_session"},
status_code=401,
headers=[make_clear_session_cookie_header(cookie_name)],
headers=[make_clear_session_cookie_header(cookie_name, environment)],
)

timestamp = int(time.time())
Expand All @@ -318,7 +319,7 @@ def get_session_data(
return Response.json(
{"error": "invalid_session"},
status_code=401,
headers=[make_clear_session_cookie_header(cookie_name)],
headers=[make_clear_session_cookie_header(cookie_name, environment)],
)

session = session_result
Expand Down
22 changes: 18 additions & 4 deletions backend/src/apiserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import enum
import logging
import sys
import time
from dataclasses import dataclass, field
from http.cookies import SimpleCookie
Expand Down Expand Up @@ -203,16 +204,29 @@ def get_cookie_value(headers: dict[str, str], cookie_name: str) -> str | None:
return None


def apply_session_cookie_security(
cookie: SimpleCookie, cookie_name: str, environment: str
) -> None:
"""Apply HttpOnly / SameSite / Secure / Path to a session cookie.

Secure is dropped only on macOS in the test env: Safari rejects Secure
cookies on the HTTP loopback (https://bugs.webkit.org/show_bug.cgi?id=281149).
"""
cookie[cookie_name]["httponly"] = True
cookie[cookie_name]["samesite"] = "Strict"
cookie[cookie_name]["path"] = "/"
if not (environment == "test" and sys.platform == "darwin"):
cookie[cookie_name]["secure"] = True


def make_clear_session_cookie_header(
cookie_name: str = SESSION_COOKIE_PRIMARY,
environment: str = "production",
) -> tuple[bytes, bytes]:
"""Create a Set-Cookie header that clears a session cookie."""
cookie = SimpleCookie()
cookie[cookie_name] = ""
cookie[cookie_name]["httponly"] = True
cookie[cookie_name]["samesite"] = "None"
cookie[cookie_name]["secure"] = True
cookie[cookie_name]["path"] = "/"
apply_session_cookie_security(cookie, cookie_name, environment)
cookie[cookie_name]["max-age"] = 0
return (b"Set-Cookie", cookie[cookie_name].OutputString().encode("utf-8"))

Expand Down