Advertisement
programming JWT is stateless, so how do you log users out securely? Learn how to implement proper logout in Node.js using access & refresh tokens, token blacklists, and secure revocation.

Node.js JWT Logout Done Right — Securely Invalidating Access & Refresh Tokens

5 Min Read Verified Content

JWT authentication feels simple — until logout enters the chat.

With traditional sessions, logout is easy:

Delete session → user logged out.

But JWT is stateless.
Once you issue a token…

✔ it lives until it expires
✔ the server doesn’t store it
✔ deleting it on the client doesn’t invalidate it

So the BIG question is:

How do you securely log a user out when using JWT?

Let’s walk through the correct options — including code examples.


🧠 First — How JWT Login Usually Works

You normally issue:

Access token (short life, e.g., 15 mins)

Used on each request

Refresh token (long life, e.g., 7–30 days)

Used to get new access tokens

So logout must at least revoke the refresh token
and ideally also handle access tokens.


Wrong Logout (But Many Apps Do This)

Some tutorials say:

localStorage.removeItem("token");

This only deletes the token on the browser.

But…

If the attacker already stole the token — it still works.

So this is NOT real logout.


Correct JWT Logout Approaches

There are 3 accepted strategies.
Pick the one that fits your app.


Method 1 — Delete Refresh Token From Backend (Most Common)

This assumes:

✔ access token expires quickly
✔ refresh token is stored server-side (DB or Redis)

When user logs out — remove refresh token from DB

Example login — storing refresh token:

await db.refreshTokens.insert({ userId: user.id, token: refreshToken });

Logout route

app.post("/logout", async (req, res) => { const { refreshToken } = req.body; await db.refreshTokens.delete({ token: refreshToken }); res.sendStatus(204); });

Client also deletes local tokens

Now refresh token is dead forever.

Access token will expire soon — so risk window is limited.

This is the most widely-used real-world solution.


Method 2 — Token Blacklist (Enterprise / High Security)

Use when:

✔ tokens must be revoked immediately
✔ security matters (banking, admin dashboards)
✔ access tokens cannot be trusted after logout

You store invalid tokens in Redis until they expire.
Every request checks the blacklist.

Add token to blacklist when logging out

app.post("/logout", async (req, res) => { const token = req.headers.authorization?.split(" ")[1]; const decoded = jwt.decode(token); const exp = decoded.exp; // unix seconds const ttl = exp - Math.floor(Date.now() / 1000); await redis.setEx(`bl_${token}`, ttl, "1"); res.sendStatus(204); });

Middleware checks blacklist

async function auth(req, res, next) { const token = req.headers.authorization?.split(" ")[1]; const isBlacklisted = await redis.get(`bl_${token}`); if (isBlacklisted) return res.sendStatus(401); jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); }

This gives instant logout — even for access tokens.


Method 3 — Rotate Refresh Tokens (Best Practice Security)

Each refresh issues a new token
and invalidates the old one.

So if an attacker steals it — it's useless later.

Logout = delete current refresh token.

This is what Auth0, Firebase, and Amazon Cognito do.


🔐 HTTP-Only Cookie Logout (If You Store JWT in Cookies)

If you use secure cookies — logout is simple:

res.clearCookie("accessToken"); res.clearCookie("refreshToken"); res.sendStatus(204);

Just make sure cookies are:

✔ httpOnly
✔ secure
✔ sameSite=strict

Never store JWT in normal cookies or localStorage for high-risk apps.


🚨 Security Rules You Should Follow

1. Short access token lifetime

Recommended:

5–15 minutes

2. Long refresh token lifetime (but revocable)

7–30 days

3. Store refresh tokens securely

Best:

✔ Database
✔ Redis
✔ Encrypted storage

Worst:

❌ localStorage
❌ insecure cookies
❌ memory variables

4. Always rotate refresh tokens

So an attacker can’t reuse old ones.


🧪 Logout Flow Example (Clean & Modern)

Client sends refresh token to /logout

{ "refreshToken": "xxx.yyy.zzz" }

Server deletes it from DB

Client deletes local tokens

Access token expires soon

User = logged out.


🧩 Common Bugs & Fixes

“User still logged in after logout”

Access token is still valid.
Solution: shorten access token lifetime.


“Refresh token reuse attack risk”

Fix: rotate refresh tokens always.


“403 after logout but before refresh expiry”

That’s correct — token is invalid 👍


Simple Reference Implementation

Login

  • issue access token

  • issue refresh token

  • store refresh token in DB

Protected route

  • verify access token

  • deny if blacklisted

Refresh

  • verify refresh token exists in DB

  • rotate refresh token

  • issue new access token

Logout

  • delete refresh token from DB

  • (optional) blacklist current access token


Final Thoughts

JWT logout is not automatic —
you have to design it.

The best balance for most apps is:

✔ short-life access tokens
✔ refresh tokens stored in DB
✔ delete refresh token on logout
✔ optional blacklist for high-security apps

Simple. Secure. Predictable.

Advertisement
Back to Programming