From caf4b2e028f4bc6b1d368826aaa29d4100a9729a Mon Sep 17 00:00:00 2001 From: Timothy Oluwatobi Ojebiyi Date: Tue, 20 Oct 2026 14:49:52 +0100 Subject: [PATCH] fix: a login lasts a week, not a quarter of an hour /login called create_access_token without an expiry, so every token used its 15-minute default, though auth.ACCESS_TOKEN_EXPIRE_MINUTES says 7 days. A quarter of an hour after signing in, the next request got a 401, and api.ts dropped the token and sent the browser to /login: logged out over and over. create_access_token now defaults to ACCESS_TOKEN_EXPIRE_MINUTES. The tests' client_for fixture skips authentication, so nothing caught it; test_auth.py signs in for real, checks the token lasts 7 days and works, and that a wrong password gets none. The first test fails on the old default. --- api/app/auth.py | 7 +++---- api/tests/test_auth.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 api/tests/test_auth.py diff --git a/api/app/auth.py b/api/app/auth.py index 2fbb6fe..01b1c26 100644 --- a/api/app/auth.py +++ b/api/app/auth.py @@ -48,10 +48,9 @@ def get_password_hash(password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() - if expires_delta: - expire = datetime.now(timezone.utc) + expires_delta - else: - expire = datetime.now(timezone.utc) + timedelta(minutes=15) + # A login lasts ACCESS_TOKEN_EXPIRE_MINUTES. The default was 15 minutes, which /login used, so the browser was + # logged out a quarter of an hour after every sign-in (api.ts drops the token on the first 401). + expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/api/tests/test_auth.py b/api/tests/test_auth.py new file mode 100644 index 0000000..b5e108b --- /dev/null +++ b/api/tests/test_auth.py @@ -0,0 +1,23 @@ +"""Signing in: how long the token the browser keeps lasts.""" +from datetime import datetime, timezone + +from fastapi.testclient import TestClient +from jose import jwt + +from app import auth, main, tracker + + +def test_a_login_lasts_a_week_not_a_quarter_of_an_hour(db): + tracker.create_user("ada@example.com", auth.get_password_hash("correct horse")) + client = TestClient(main.app) + token = client.post("/login", data={"username": "ada@example.com", "password": "correct horse"}).json()["access_token"] + expires = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM])["exp"] + days = (datetime.fromtimestamp(expires, timezone.utc) - datetime.now(timezone.utc)).total_seconds() / 86400 + assert 6.99 < days <= 7 # not the 15-minute default that logged people out + assert client.get("/persona", headers={"Authorization": f"Bearer {token}"}).status_code == 200 + + +def test_a_wrong_password_gets_no_token(db): + tracker.create_user("ada@example.com", auth.get_password_hash("correct horse")) + reply = TestClient(main.app).post("/login", data={"username": "ada@example.com", "password": "wrong"}) + assert reply.status_code == 401 and "access_token" not in reply.json()