Logout

How the IAM service terminates a session by consuming and revoking the refresh token, blacklisting the access token, blocking the token hash in the rate limiter, and clearing cookies.

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

MethodPath
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 session cookie (401)
  • Request has a parsed JSON body, a Content-Length greater than zero, or Transfer-Encoding: chunked (400)
  • Query string is present (400)
  • Content-Type header 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

StatusCondition
400Missing token, invalid access token, body or query string present, Content-Type header present
401Missing session cookie, missing Authorization header, refresh token not found or revoked
429Rate limit exceeded (IP or token hash)

Why both consume and revoke?

The two-step process (consumeAndVerifyRefreshToken then revokeRefreshToken) is deliberate:

  1. 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 inside consumeAndVerifyRefreshToken.
  2. Revocation sets valid = 0. This makes the token fail the basic validity check in any future query, providing an immediate block without relying on the usage_count check.

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

LimiterKeyPointsWindowBlock duration
IP burstreq.ip2 / 1 second1 s30 min
IP slowreq.ip3 / 10 minutes10 min1 h
Token hashSHA-256 of refresh token3 / 12 hours12 h15 h
JTI blacklistAccess token jti24 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.

Logo