SECURE_NODE // ATHENS_GR
← BACK TO RESEARCH Technique · Authentication · Session Management

Session Token Not Invalidated on Logout

10 JUN 2026| ≈ 3 MIN READ| SessionManagementAuthenticationBrokenAuth
SANITIZED ▸ Methodology only. All hostnames, endpoint paths, tokens, and organizational details are lab placeholders. No client name, real token, real user data, or live credential appears here. Assessment was authorized and scoped in writing.

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.

FINAL SEVERITY
High
session persists post-logout
SCOPE
Grey-box
web and mobile app
LIFETIME
Indefinite
no server-side invalidation

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

DAY 1 — AUTH REVIEW

JWT structure decoded. No exp claim. Subject and issued-at only. No server-side session store identifiable from API surface.

DAY 1 — LOGOUT NON-INVALIDATION

Token replayed successfully after logout. GET /api/documents/pending returns 200 with data using a token explicitly logged out seconds prior.

DAY 1 — PASSWORD CHANGE TEST

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.

DAY 1 — CURRENT PASSWORD BYPASS

Password change accepts new password without verifying old. POST /api/account/password takes only newPassword — no current credential verification required.

DAY 2 — REPORT

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 without exp is 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.