Session Token Not Invalidated on Logout
Session tokens should expire when the user explicitly ends their session. When a
logout endpoint returns {"success": true} but the server continues to accept
the same token on authenticated endpoints, logout is a UI affordance, not a security
event. The user believes they are no longer authenticated. The server disagrees. Any
token captured before logout — via network interception, browser history, XSS, or
exposure in logs — remains valid indefinitely.
Logout that doesn't log out
The test sequence is three steps: authenticate and note the token, call the logout endpoint, immediately replay the same token against an authenticated endpoint. On a correctly implemented session store, step 3 returns a 401. Here it returned 200 with data:
# step 1: authenticate
POST /api/auth/login
{"username":"lab_user@acme-lab.test","password":"<lab-password>"}
HTTP/1.1 200 OK
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJsYWJfdXNlciIsImlhdCI6MTcwMDAwMDAwMH0.lab_sig"}
# step 2: logout
POST /api/auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
HTTP/1.1 200 OK
{"success": true}
# step 3: replay the same token after logout
GET /api/documents/pending
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
HTTP/1.1 200 OK
{"documents":[{"id":1041,"title":"Lab Document A","status":"pending_review"}]}
# token accepted post-logout — no server-side session invalidation
The JWT had no exp claim — the payload contained only a subject and an
iat (issued-at). Without an expiry and without server-side tracking of
valid tokens, there is no mechanism by which a logout can invalidate the credential.
The logout endpoint returned success and discarded the client's local copy. The server
had no record to revoke.
The companion chain
Two additional findings from the same assessment: password change not invalidating prior sessions (tokens issued before the password change remained valid for authenticated operations after it), and the password change endpoint accepting the new password without verifying the current one. Together the three findings allow a session captured through any channel to persist regardless of any user action taken to terminate it.
The test timeline
JWT structure decoded. No exp claim. Subject and issued-at only. No server-side session store identifiable from API surface.
Token replayed successfully after logout. GET /api/documents/pending returns 200 with data using a token explicitly logged out seconds prior.
Prior session persists after password change. Token issued before the password change accepted on authenticated endpoints after the change completes. No session invalidation on credential rotation.
Password change accepts new password without verifying old. POST /api/account/password takes only newPassword — no current credential verification required.
Three-finding chain documented. Session persistence scope established. Indefinite token lifetime confirmed.
Fixing session management
- Implement a server-side session store with explicit invalidation. Maintain a denylist or allowlist of valid token identifiers (JTI claim). On logout, mark the token's JTI as revoked. On any authenticated request, verify the token is not on the denylist before processing. Redis is a common choice for a fast token revocation store.
- Add an expiry claim (
exp) and a short-lived refresh pattern. A JWT withoutexpis valid forever from any holder's perspective. Set access tokens to expire in 15–60 minutes and issue refresh tokens separately. This does not replace server-side revocation but bounds the blast radius of a captured token. - Invalidate all prior sessions on password change. A password change is an explicit re-authentication event. Any session established under the previous password must be invalidated. Generate a per-user revocation generation counter — incrementing it on password change causes all prior tokens to fail validation on the next request.
- Require current password verification on the change-password endpoint. The endpoint must accept
currentPassword, verify it against the stored credential, and reject the request before changing anything if verification fails. Without this, any authenticated session can change the account password regardless of how it was acquired.
The takeaway
Logout that does not invalidate the session is not logout — it is a UI animation that misleads the user while the server remains accessible to anyone who still holds the token.