Skip to content
Vulnerabilities

What logging out should actually do

A user signs out. The interface returns to the login screen. The token they were using still works. This is one of the most common findings in application testing, and it is rarely an oversight: it is what happens when a stateless design meets an action that requires state.

7 min read

Key takeaways

  • Signing out should end the session on the server. Clearing it from the browser only ends the appearance of one.
  • Stateless tokens cannot be withdrawn by design. If you issue them, you need a deliberate answer for logout, not the default one.
  • A short access token with a revocable refresh token gives you both properties, and is the usual answer.
  • Logout has to reach every credential the session created, including any long-lived remember-me token.
  • On a shared or mobile device, local cached data survives sign-out unless somebody clears it explicitly.

What actually goes wrong

The user presses sign out. The application deletes the token from local storage or expires the cookie, redirects to the login page, and reports success. Nothing was communicated to the server, or something was and the server did not act on it.

Anybody holding a copy of that token can keep using it until it expires on its own. That includes a token captured earlier, one left in a log, one on a shared machine, and one on a device the user has since sold.

It is easy to miss in testing because the interface behaves correctly. Signing out looks like it worked, and it did, from the browser point of view. Finding it means replaying an old token after signing out, which is a deliberate act nobody performs by accident.

Why stateless tokens cause this

A signed, self-contained token is attractive because verifying it needs no lookup. That property is the point, and it is also exactly why you cannot cancel one. The server does not consult a list, so there is no list on which to mark it invalid.

Teams reach this conclusion in the wrong order. The token design is chosen for performance, logout is implemented as a client-side delete because that is all the design allows, and the gap is only noticed when somebody tests for it or an account is compromised and cannot be cut off.

The question worth asking during design

If an account is compromised right now, how do we end its access, and how long does that take to take effect? A design where the honest answer is "when the token expires" is a design decision, and it should be made deliberately rather than discovered later.

What to invalidate, and where

The usual answer keeps the performance benefit and restores control: a short-lived access token that is never checked against a store, paired with a long-lived refresh token that is.

  1. Keep access tokens short, in minutes rather than days. The exposure window after logout is then bounded by that lifetime.
  2. Store refresh tokens server side and delete them on logout, so the session cannot be renewed.
  3. Rotate the refresh token on each use, and treat reuse of an old one as theft: end the whole session family.
  4. Keep a revocation list for access tokens if minutes is still too long for your risk, accepting the lookup it costs.
  5. End every credential the session created, including remember-me tokens, device tokens and any active socket connection.
  6. Offer a way to end sessions on other devices, and use it yourself when a password changes.

A password change or reset should invalidate everything outstanding. If it does not, a user who changes their password because they think somebody is in their account has not removed them.

Expiry, idle timeouts and the other half

The companion finding is a session that never ends on its own. A token valid for a year is a credential with a year of exposure, and most users never sign out at all, so expiry is the control that actually runs.

  • An absolute lifetime, after which re-authentication is required regardless of activity.
  • An idle timeout, so an abandoned session on a shared machine closes itself.
  • Re-authentication before genuinely sensitive actions, independent of session age.
  • A new session identifier issued at login, so a value set before authentication cannot be reused after it.

The right numbers depend on what the application does. A banking session and a reading application should not have the same timeout, and copying one from the other is how a control ends up either useless or infuriating.

Testing it yourself

Like the rate limiting check, this is something a developer can verify in a few minutes and should not be paying to discover.

  1. Sign in and capture the token or cookie your client is sending.
  2. Sign out through the interface.
  3. Replay a request with the captured value, from a separate client.
  4. If it succeeds, logout did not reach the server.

Then repeat it after a password change, which is the case that matters more. And on mobile, check what is still readable in local storage once the user has signed out, which is covered in the mobile controls guide.

FAQ

Questions this raises

Why does our session still work after logging out?

Almost always because logout is implemented in the client only. The token is deleted from the browser or app and nothing invalidates it on the server, so any copy of it keeps working until it expires. With self-contained signed tokens this is the default behaviour rather than a bug in your code.

How do you invalidate a JWT?

You cannot invalidate a self-contained token directly, which is the trade-off you accepted when you chose one. The practical answer is to keep access tokens short-lived and pair them with a server-side refresh token you can delete, so logout stops renewal and the access token expires quickly on its own.

How long should a session last?

It depends entirely on what the application does, which is why copying a number from somewhere else goes wrong in both directions. What matters more is having an absolute lifetime as well as an idle timeout, and requiring re-authentication before actions that would hurt if performed by somebody else.

Should changing a password end other sessions?

Yes. A user changing their password because they suspect somebody is in their account has not removed that person unless outstanding sessions and refresh tokens are invalidated too. This is one of the most consequential versions of the finding and one of the most frequently missed.

Is this something a scanner picks up?

Generally not. The sign-out flow returns a success response and the interface behaves correctly. Detecting it means replaying a captured token after signing out and noticing that it still works, which is a deliberate test rather than a pattern to match.