Logout
The logout route terminates the current session by invalidating both the refresh token and the access token, then clearing the client cookies. The route requires both tokens to be present, verifies them independently, and uses two separate blocking mechanisms to prevent post-logout reuse.
Route
| Method | Path |
|---|---|
POST | /auth/logout |
Middleware chain:
requireRefreshToken, requireAccessToken, acceptCookieOnly, handleLogout
requireRefreshToken
Checks that the session cookie is present in req.cookies. Returns 401 if missing.
requireAccessToken
Checks that the Authorization: Bearer <token> header is present and well-formed. Extracts the token and sets it on req.token. Returns 401 if the header is missing or malformed.
acceptCookieOnly
Enforces that the request carries no body, no query string, and no Content-Type header. This prevents attackers from injecting data into what should be a pure cookie-based request.
Rejected conditions:
- No
sessioncookie (401) - Request has a parsed JSON body, a
Content-Lengthgreater than zero, orTransfer-Encoding: chunked(400) - Query string is present (400)
Content-Typeheader is present (400)
See Routes for how to mount the token rotation router and Middleware for details on each middleware.
The logout sequence
After the middleware chain passes, the handleLogout controller runs the following steps in order.
Rate limiting (IP)
A union limiter keyed on req.ip combines a burst guard (2 points per second, blocks 30 minutes) with a slow guard (3 points per 10 minutes, blocks 1 hour). A consecutive-violation cache (10-minute TTL) escalates the block duration on repeated abuse. See Rate Limiting for the guard() architecture.
Verify both tokens are present
The controller reads the raw refresh token from req.cookies.session and the access token from req.token (set by the requireAccessToken middleware). If either is missing at this point, the controller returns 400 { error: 'Missing token' }.
Rate limiting (token hash)
The refresh token is hashed with SHA-256 and used as the key for a second rate limiter: 3 points per 12 hours, blocking for 15 hours. This prevents an attacker from repeatedly calling logout with the same stolen token to probe for timing differences.
Consume the refresh token
The controller calls consumeAndVerifyRefreshToken(rawRefreshToken). This function atomically increments usage_count to 1 inside a transaction, but only if the token exists, is still valid, has not been consumed before (usage_count = 0), and has not expired. If the consumption fails, the controller returns the failure reason from the function (e.g. Token not found, Token has been revoked, or Token already used).
See Refresh Tokens for the full semantics of consumeAndVerifyRefreshToken, including the reuse detection path that revokes all sessions when a previously consumed token is presented.
Revoke the refresh token
After successful consumption, the controller calls revokeRefreshToken(rawRefreshToken) to set valid = 0 on the row. The row is not deleted. Keeping it in the table allows the reuse detection system to identify the token later if an attacker tries to use a copy.
Block the token hash in the rate limiter
The hashed refresh token is blocked in the rate limiter for the remaining refresh_ttl duration. Any future request using this token hash (whether for rotation, logout, or access) is rejected at the rate-limiter layer before reaching any database logic.
Verify the access token
The controller calls verifyAccessToken(accessToken) to decode the JWT and extract the jti (JWT ID). If the token has already expired, the controller still extracts the jti from the payload when the error type is TokenExpiredError. Other verification failures (invalid signature, malformed token) cause the controller to return 400.
Blacklist the JTI
The jti from the access token is blocked in the rate-limiter blacklist for 24 hours (86,400 seconds). The protectRoute middleware checks every incoming access token's jti against this blacklist. If found, the request is rejected with 401. This covers the window between logout and the access token's natural expiry.
Clear cookies
The controller clears both the session and iat cookies by setting them to empty strings with the same options used when they were created (httpOnly, secure, sameSite: strict, matching path and domain). This ensures the browser removes the cookies on the next response.
Response
Success (200)
{
"ok": true,
"message": "Logged out successfully"
}
Error responses
| Status | Condition |
|---|---|
400 | Missing token, invalid access token, body or query string present, Content-Type header present |
401 | Missing session cookie, missing Authorization header, refresh token not found or revoked |
429 | Rate limit exceeded (IP or token hash) |
Why both consume and revoke?
The two-step process (consumeAndVerifyRefreshToken then revokeRefreshToken) is deliberate:
- Consumption atomically sets
usage_count = 1. This is the reuse detection anchor. If a copy of the token exists elsewhere, any attempt to consume it again triggers the revoke-all-sessions path insideconsumeAndVerifyRefreshToken. - Revocation sets
valid = 0. This makes the token fail the basic validity check in any future query, providing an immediate block without relying on theusage_countcheck.
Together, these two operations ensure that a stolen token copy is caught by whichever check runs first: the validity check (fast) or the reuse detection check (comprehensive). See the Reuse Detection section in the refresh tokens doc for a full scenario walkthrough.
Rate limiter reference
| Limiter | Key | Points | Window | Block duration |
|---|---|---|---|---|
| IP burst | req.ip | 2 / 1 second | 1 s | 30 min |
| IP slow | req.ip | 3 / 10 minutes | 10 min | 1 h |
| Token hash | SHA-256 of refresh token | 3 / 12 hours | 12 h | 15 h |
| JTI blacklist | Access token jti | — | — | 24 h |
The JTI blacklist is not a traditional rate limiter. It is a key-value block list with a fixed TTL. The protectRoute middleware checks it on every authenticated request.
Lifecycle summary
The full session lifecycle from login to logout:
Login - issue refresh token (usage_count=0, valid=1)
- issue access token (JWT with jti)
- set session + iat cookies
... user makes authenticated requests ...
Logout - consume refresh token (usage_count= 0 - 1)
- revoke refresh token (valid= 1 - 0)
- block token hash in rate limiter
- blacklist access token jti for 24h
- clear session + iat cookies
After logout, the access token is unusable (JTI blacklisted), the refresh token is unusable (consumed, revoked, and hash-blocked), and the client has no cookies. The user must log in again to start a new session.