Skip to content

Why Are Users Still Logged In After They Click Log Out?

6 min read

Users click Log out, the screen sends them back to the login page, and yet their session still works. Copy the old token into a fresh request and it is accepted. The direct answer: your logout button is clearing the browser but not invalidating the session on the server, so the token it deleted from one place still opens the door everywhere else. That is a genuine security hole, and it is worth closing before real users share devices.

Why this happens

Most vibe-coded logout buttons do one thing: they wipe the token out of the browser (localStorage, a cookie, or memory) and redirect. That feels like logging out, and in the demo it looks perfect, because you only ever test it in the one browser you are sitting at.

The problem is that a token is a bearer credential. Whoever holds it is treated as logged in until it expires, no matter where it came from. Deleting your copy does not tell the server "this token is dead." So a token that was captured, left in a shared computer's history, synced to another device, or simply copied before logout keeps working right up to its natural expiry, which for a lot of setups is days or weeks away.

The agent built exactly what you asked for. You said "add a logout button," and a button that clears the browser satisfies that literally. You did not ask it to revoke the session server-side, so it didn't, and nothing in the happy path ever revealed the gap.

How to check

You can prove this in about a minute without any code.

  1. Log in to your app in a browser.

  2. Open the network tab, find an authenticated request, and copy the token or session cookie it sends (look for Authorization: Bearer ... or an auth cookie).

  3. Click Log out in the app.

  4. Replay that same request with the old token, for example:

    curl "https://YOUR-APP.com/api/me" \
      -H "Authorization: Bearer OLD_TOKEN_YOU_COPIED"
    

If it still returns your data, logout did not invalidate the session. A correct logout returns 401 Unauthorized here.

The fix

The goal is that logout kills the session on the server, not just in the browser.

  1. Call your auth provider's real sign-out. On Supabase use supabase.auth.signOut(), which revokes the refresh token server-side. On Firebase, revoke refresh tokens with the Admin SDK (revokeRefreshTokens(uid)), not just client signOut(). If you rolled your own sessions, delete the session row from your sessions table.
  2. Clear the cookie server-side. Send a Set-Cookie that expires the auth cookie immediately, with HttpOnly, Secure, and SameSite set, so the browser cannot keep sending it.
  3. Keep access tokens short-lived. A stateless JWT cannot be un-issued, so its window of validity is its whole risk. Set access-token lifetime to minutes and rely on refresh-token revocation for the real cut-off.
  4. Re-run the replay test from "How to check." The old token must now return 401. Test it in a second browser too, not just the one you logged out in.

The trap to avoid

The common wrong fix is to make the logout button clear more things: remove more localStorage keys, drop the cookie client-side, reload the page harder. None of that touches the server, so a copied token still works. Clearing the browser is housekeeping, not revocation. The session is only truly gone when the server refuses the token.

Where this fits

A logout that does not log out is the kind of gap that never shows up in a demo and lands hard the first time two people share a laptop. The free Readiness Report runs this exact replay test against your real project and tells you whether your logout revokes the session or just tidies the browser. If you would rather have the fixes handed to you in order, that is what the Finishing Pass is for. It pairs closely with how long a login session should last and whether it is safe to store your token in localStorage, since token lifetime and storage decide how much damage a missed logout can do.