From 3cb4d01073f748ddf35bbcbe3f6162119e3edd0f Mon Sep 17 00:00:00 2001 From: coder-bot Date: Wed, 19 Aug 2026 22:15:55 +0000 Subject: [PATCH] Fix access token collision and cross-file username collision - auth.py: _create_access_token had no jti claim, so two refreshes within the same wall-clock second produced byte-identical access tokens (same failure mode already fixed for refresh tokens earlier) - test_refresh_endpoint.py: usernames (jwttoken, refreshuser, etc.) collided with test_auth_endpoints.py's own test usernames once run in the same pytest session against a shared DB with no per-test isolation -- exactly how run_checkpoint_tests.py invokes pytest. Namespaced this file's usernames with a refep_ prefix. Full backend suite (63 tests) passes clean end-to-end against a real Postgres with real migrations applied. --- backend/app/routers/auth.py | 9 ++++++++- backend/tests/test_refresh_endpoint.py | 12 ++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 79a5f91..61e99ae 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -66,8 +66,15 @@ class UserResponse(BaseModel): # --- Helpers --- def _create_access_token(data: dict) -> str: - """Create a signed access token using RS256.""" + """Create a signed access token using RS256. + + Includes a unique ``jti`` claim so that concurrent/same-second calls + (e.g. back-to-back /refresh requests) still produce distinct tokens — + without it, two tokens minted for the same user within the same + second are byte-identical since ``sub``/``exp`` are the only claims. + """ to_encode = data.copy() + to_encode["jti"] = str(uuid.uuid4()) expire = datetime.now(timezone.utc) + timedelta( minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES ) diff --git a/backend/tests/test_refresh_endpoint.py b/backend/tests/test_refresh_endpoint.py index 995bf0f..92e4322 100644 --- a/backend/tests/test_refresh_endpoint.py +++ b/backend/tests/test_refresh_endpoint.py @@ -67,7 +67,7 @@ class TestRefresh: def test_refresh_success(self): """A valid refresh token returns a new access token.""" - refresh_token = self._register_user("refreshuser", "secret123") + refresh_token = self._register_user("refep_refreshuser", "secret123") client = TestClient(app) resp = client.post( "/api/auth/refresh", @@ -82,7 +82,7 @@ class TestRefresh: def test_refresh_returns_different_access_token(self): """Refreshing produces a new access token (different from any previous).""" - refresh_token = self._register_user("refreshtoken", "secret123") + refresh_token = self._register_user("refep_refreshtoken", "secret123") client = TestClient(app) # First refresh @@ -140,7 +140,7 @@ class TestRefresh: client.post( "/api/auth/register", json={ - "username": "loginrefuser", + "username": "refep_loginrefuser", "email": "loginref@example.com", "password": "secret123", "date_of_birth": "2000-01-01", @@ -149,7 +149,7 @@ class TestRefresh: # Login to get a refresh token login_resp = client.post( "/api/auth/login", - data={"username": "loginrefuser", "password": "secret123"}, + data={"username": "refep_loginrefuser", "password": "secret123"}, ) assert login_resp.status_code == 200 refresh_token = login_resp.json()["refresh_token"] @@ -166,7 +166,7 @@ class TestRefresh: """The access token returned by /refresh is a valid JWT.""" from jose import jwt - refresh_token = self._register_user("jwttoken", "secret123") + refresh_token = self._register_user("refep_jwttoken", "secret123") client = TestClient(app) resp = client.post( "/api/auth/refresh", @@ -190,7 +190,7 @@ class TestRefresh: def test_refresh_expires_minutes_in_response(self): """The response includes access_token_expires_minutes.""" - refresh_token = self._register_user("expiretest", "secret123") + refresh_token = self._register_user("refep_expiretest", "secret123") client = TestClient(app) resp = client.post( "/api/auth/refresh",