ASVS 5.0 V7.4.1 (Level 1) requires that when session termination is triggered, by logout or by expiration, the application refuses any further use of that session. For reference tokens and stateful sessions, that means invalidating the session data on the backend. For self contained tokens you need a way to deny tokens that are still cryptographically valid.
Why it matters
A logout button that only deletes the cookie in the browser gives a false sense of safety. The token is still good. Anyone who copied it, through malware, a proxy log, a shared machine or a leaked HAR file, can keep using it until it expires.
A correct logout makes a captured token worthless the moment the user leaves. That is what turns "log out on the public computer" from a ritual into a control.
How to implement it
Reference tokens
Delete or flag the session row, and make the lookup fail for revoked or expired entries.
app.post("/logout", requireSession, async (req, res) => {
await db.sessions.updateOne({ id: req.session.id }, { $set: { revoked: true } });
res.clearCookie("sid", { httpOnly: true, secure: true, sameSite: "lax", path: "/" });
res.sendStatus(204);
});
The server side change is the control. The cookie clearing is housekeeping.
Self contained tokens
A signed token cannot be deleted, so pick one of three patterns:
- Denylist: store the jti of terminated tokens until their natural expiry, and check it on each request.
- Per user not before time: store a timestamp for each user, and reject tokens whose iat is older.
- Key rotation per user: rotate a per user signing key on logout, which kills all tokens at once.
def token_is_allowed(claims, db):
if db.denylist_contains(claims["jti"]):
return False
nb = db.user_not_before(claims["sub"])
return claims["iat"] >= nb
Keep access tokens short, a few minutes, and put the revocation check on the refresh path. Revoke refresh tokens on logout too, or the client will simply mint a new access token.
Expiry
Expired sessions must be rejected by the server, not merely hidden by the interface. A background job that purges old rows is good for hygiene, but validation must not depend on it running.
Beyond the browser
Terminate related state: websocket connections, long polling streams, cached authorization decisions, and any downstream sessions you created on the user's behalf. If you use SSO, decide how logout propagates (see V7.1.3).
How to test it
The replay test is simple and decisive.
- Log in and capture the session cookie or bearer token with your proxy.
- Log out through the interface.
- Send a protected request using the captured value.
- Expect a 401 or a redirect to login. Any success is a failure of this requirement.
Repeat after the idle timeout expires and after the absolute lifetime ends. Test the refresh token too: after logout, exchange it and confirm denial.
An automated crawler that logs in, logs out and replays is a good regression test. See how a scanner reports it:
Click through it at your own pace, or open it full screen.
Common mistakes
- Clearing the cookie and nothing else.
- Logout as an unauthenticated GET that a prefetcher can trigger. Use POST.
- Denylist entries that expire before the token does, leaving a window.
- A cache in front of the session store that keeps answering "valid" after revocation.
- Revoking the access token but leaving the refresh token alive.
Try it yourself
Pick your most sensitive endpoint. With curl, authenticate, call it, log out, and call it again with the same token. Put the exact commands in your runbook and run them after each release.
Quick check
We use JWTs only. Are we exempt?
No. The requirement names self contained tokens explicitly and expects a denylist, a per user cutoff time or key rotation.
Is a short token lifetime enough?
It limits exposure, but a user who logs out expects immediate effect. Short lifetimes complement revocation rather than replace it.