From 643785ad1ed5b9768064956ac690dd7d6b2da8a9 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Wed, 20 May 2026 22:39:46 +0200 Subject: [PATCH] backend: complete P0 session rotation hardening --- TODO.md | 4 ++-- backend/main.py | 25 ++++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/TODO.md b/TODO.md index d166022..9931cd7 100644 --- a/TODO.md +++ b/TODO.md @@ -4,10 +4,10 @@ Concrete follow-up work for Jellomator, prioritized by implementation risk and u ## P0 - Security and Reliability -- [ ] Add session expiry and rotation. +- [x] Add session expiry and rotation. - [x] Add `expires_at` and `last_seen_at` to `sessions`. - [x] Reject expired tokens in `current_user`. - - [ ] Rotate session token on login and periodically on use. + - [x] Rotate session token on login and periodically on use. - [x] Harden auth endpoints. - [x] Add login rate limiting by IP + username pair. - [x] Add brute-force lockout window with clear error message. diff --git a/backend/main.py b/backend/main.py index 85c08ef..a52ea14 100644 --- a/backend/main.py +++ b/backend/main.py @@ -194,22 +194,21 @@ def current_user(request: Request, response: Response | None = None): should_rotate = (now - datetime.fromisoformat(last_seen_at)).total_seconds() >= SESSION_ROTATE_SECONDS except ValueError: should_rotate = True - if should_rotate: + if should_rotate and response is not None: new_token = secrets.token_urlsafe(32) cur.execute( "update sessions set token=%s,last_seen_at=%s,expires_at=%s where token=%s", (new_token, now_iso, new_expires_at, token), ) - if response is not None: - response.set_cookie( - SESSION_COOKIE, - new_token, - httponly=True, - samesite="lax", - secure=SESSION_COOKIE_SECURE, - max_age=SESSION_TTL_SECONDS, - path="/", - ) + response.set_cookie( + SESSION_COOKIE, + new_token, + httponly=True, + samesite="lax", + secure=SESSION_COOKIE_SECURE, + max_age=SESSION_TTL_SECONDS, + path="/", + ) else: cur.execute( "update sessions set last_seen_at=%s,expires_at=%s where token=%s", @@ -358,6 +357,10 @@ def login(request: Request, inp: LoginIn): raise HTTPException(401, "Invalid credentials") login_attempts.pop(key, None) login_lockouts.pop(key, None) + old_token = request.cookies.get(SESSION_COOKIE) + if old_token: + with c.cursor() as cur: + cur.execute("delete from sessions where token=%s", (old_token,)) token = secrets.token_urlsafe(32) with c.cursor() as cur: now = utc_now_iso()