Log out users automatically when their JWT expires. #11
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/expired-token-logout"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Log users out automatically when their JWT expires, instead of leaving them stuck on pages that silently fail to load.
Why
Previously an expired token left the frontend in a broken state: the router guard only checked token presence (never the
expclaim), so the user could still navigate to protected pages, and every API call then failed with a generic "Kunde inte hämta…" message while the header kept showing the logged-in UI. There was no global response interceptor, and the backend returned an ambiguous 403 (no body) for unauthenticated requests, making 403 mean both "no/invalid token" and "forbidden".Changes
Backend
SecurityConfig: add anAuthenticationEntryPoint(401 + Swedish{message}body) for unauthenticated/expired-token requests, and anAccessDeniedHandler(403 + body) for genuine authorization failures. Now 401 = not authenticated/expired, 403 = authenticated but forbidden.JwtService: make the(secret, expirationMs)constructor public so integration tests can mint expired tokens.Frontend
utils/jwt.ts:isTokenExpired()using the previously-unusedexpclaim.api/client.ts: global 401 interceptor — on a 401 from any non-/auth/endpoint, callauth.logout()and redirect to/logga-in?redirect=<path>. AddsisSessionExpired/isForbiddenhelpers.router/index.ts: guard rejects expired tokens (logout + redirect); expired-token users can still open/logga-inand/registrera.Tests
shouldReturn403ForNonAdminUserkept as 403, +2 new (expired-token→401, entry-point body).client.spec.ts(7),authStore+5,Router+5,OrdersPage+1.expired-token.spec.ts(2 scenarios); mocked the API in 2 pre-existing fake-JWT E2E tests that broke because the backend now correctly 401s unsigned test-sig tokens.Verification
./gradlew checkpasses (frontend lint + 267 unit tests, backend tests + coverage, Flyway, 92 E2E tests in Docker);./gradlew coveragethresholds maintained (jwt.ts at 100%).Previously an expired token left the frontend in a stuck state: the router guard only checked token presence (never the exp claim), so the user could still navigate to protected pages, and every API call then failed with a generic Swedish "Kunde inte hämta…" message while the header kept showing the logged-in UI. There was no global response interceptor, and the backend returned an ambiguous 403 (no body) for unauthenticated requests because no AuthenticationEntryPoint was configured, making 403 mean both "no/invalid token" and "forbidden". Backend: - Add an AuthenticationEntryPoint in SecurityConfig that returns 401 with a Swedish {"message": ...} ErrorResponse body for unauthenticated/expired-token requests, and an AccessDeniedHandler returning 403 with the same body shape for genuine authorization failures. This makes 401 = not authenticated/expired and 403 = authenticated but forbidden, the standard REST convention. - Make JwtService(String, long) constructor public so integration tests can mint expired tokens (was package-private). - Update the 6 no-auth controller tests from 403 to 401 (OrderControllerTest, AdminControllerTest, PaymentControllerTest, AuthControllerTest change-password/change-email) and assert the message body exists; keep shouldReturn403ForNonAdminUser as 403. - Add OrderControllerTest.shouldReturn401WithSwedishMessageWhenTokenExpired (expired JWT via TTL -1000ms) and shouldReturn401WithMessageWhenNoAuthHeader. Frontend: - Add isTokenExpired() to utils/jwt.ts using the previously-unused exp claim, and expose it on the auth store. - Add a global 401 interceptor in api/client.ts: on a 401 from any non-/auth/ endpoint, call auth.logout() and redirect to /logga-in?redirect=<currentPath>. Skip /auth/ so wrong-password 401s on login/change-password stay handled locally. Add isSessionExpired and isForbidden helpers for per-page catch blocks. - Harden the router guard to reject tokens whose exp is in the past (logout + redirect to login with ?redirect=), and let expired-token users open /logga-in and /registrera instead of bouncing to home. - Refactor the generic-error catch blocks on OrdersPage, EditOrderPage, ComposePage, PaymentRedirect, useAdminOrders, and useAdminOrderActions to skip the generic Swedish message on 401 (handled globally) while preserving wrong-password 401 handling on change-pw/email pages. Tests: - New frontend/src/__tests__/client.spec.ts covering 401 -> logout + redirect, 401 from /auth/ -> no logout, 403 -> no logout, no-token 401 -> no redirect, and isSessionExpired/isForbidden helpers. - Add authStore.spec.ts cases for isTokenExpired (no token, past exp, future exp, missing exp, after logout). - Add Router.spec.ts cases for expired-token redirects, token clearing, future-exp access, and guest pages not bouncing expired users. - Add OrdersPage.spec.ts case asserting 401 triggers no generic error and the global logout/redirect. - New E2E expired-token.spec.ts (Docker) covering both the router-guard expired-token redirect and the API-401 redirect, with logged-out header and cleared localStorage assertions. - Mock the API in two pre-existing fake-JWT E2E tests (auth-guards admin access, header-auth logout redirect) that broke because the backend now correctly 401s their unsigned test-sig tokens. Verified with ./gradlew check (frontend lint + 267 unit tests, backend tests + coverage, Flyway, 92 E2E tests in Docker) and ./gradlew coverage; all coverage thresholds maintained (jwt.ts at 100%).Review: feature/expired-token-logout
Verdict: Approve ✅
Well-scoped fix that correctly separates 401 (unauthenticated/expired) from 403 (forbidden), enforces
expin the router guard, and adds a global 401 interceptor without breaking existing wrong-password handling.Verified
Suggestions (non-blocking)
handleExpiredSession()will callauth.logout()androuter.push()for each one. Consider adding a guard (e.g., checkingrouter.currentRoute.value.name === 'login'or a short-lived flag) to avoid redundant navigations./auth/change-passwordand/auth/change-email401s are handled locally as "wrong password", a user whose session expired while on those pages will see the wrong-password message instead of being logged out. This is consistent with the PR description, but a future improvement could distinguish the two cases (e.g., a specific error code from the backend).WWW-Authenticateheader on 401 responses would be more RFC-compliant, but not required for this app.Looks good
ErrorResponsebodies.shouldReturn403ForNonAdminUseras 403.client.spec.ts,authStore.spec.ts,Router.spec.ts), and E2E (expired-token.spec.ts).No blockers — LGTM.
489fb0335ato81e3968e31