Skip to content

Build1 publisher3 min readPublished

Your JWT Login Probably Has Exactly One Kill Switch: Log Everyone Out

A dev.to walkthrough starts with a friend whose attacker survived a password reset. The tutorial path to JWT auth ships with no per-user revocation at all.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Your JWT Login Probably Has Exactly One Kill Switch: Log Everyone Out
Generated illustration

What happened

  • A dev.to article by mmushood is titled "JWT Authentication in Express That You Can Actually Revoke", subtitled: access tokens, refresh token rotation, and theft detection, the parts most Node.js tutorials leave out.
  • The author's friend messaged him about his side project: "Someone else is logged into my account. I changed my password. They're still in."
  • The friend had followed the tutorials to the letter: sign a JWT on login, send it to the frontend, keep it in localStorage, attach it to every request.
  • The setup has no way to un-log anyone in; a JWT stays valid until it expires, and the friend's tokens expired in 30 days.
  • Changing the password accomplished nothing, because the token had already been signed and nothing about it depended on the password.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A dev.to walkthrough titled "JWT Authentication in Express That You Can Actually Revoke" opens with a message its author got from a friend: someone else was logged into his account, he had changed his password, and they were still in [1][2]. He had followed the tutorials to the letter, signing a JWT at login, handing it to the frontend, keeping it in localStorage and attaching it to every request [3]. The bug was not in his code. It was in the design he copied.

A signed token stays valid until it expires, and his were set to expire in 30 days [4]. The password reset accomplished nothing, because nothing in the token depended on the password [5]. There was no list of active sessions to delete from and nothing to revoke [6]. His remaining move was rotating the signing secret, which logs out every user on the platform at once [7]. That is the whole incident response budget of a tutorial auth system: one switch, and it hits everybody.

The post names four things standing between the demo and production, and they compound [8]. localStorage is readable by any JavaScript on the page, which includes your analytics snippet, an npm package compromised upstream, and any XSS hole of your own; one getItem call and the attacker has a credential they can replay from their own machine, undetected [9]. Stateless verification, the reason people pick JWTs, is exactly what removes revocation: ban a user and they stay logged in, reset a password and the thief stays logged in [10]. Long TTLs get chosen because nobody wants to re-authenticate every fifteen minutes, which is also what makes a stolen token worth stealing [11]. And the payload is encoded, not encrypted; the author says he has read internal role hierarchies, email addresses, and on one occasion a live database connection string out of token payloads [12].

The fix in the piece is two tokens with two jobs [13]. The access token stays a JWT, 15 minutes, held in JS memory, verified by signature with no database call [13][15]. The refresh token is deliberately not a JWT: 64 random bytes in an httpOnly cookie, sent only to /auth/*, stored hashed so a database leak does not hand over live sessions, and replaced every time it is used [14]. The arithmetic on the exposure window is the part worth writing on a whiteboard. Going from a 30-day token to a 15-minute one cuts the useful life of a stolen access credential by a factor of about 2,880 [18], and the server-side refresh record is what finally gives you a row to delete.

Rotation is also what makes theft detectable, and the author's assessment is that it is the piece almost nobody implements [16]. Everything downstream of that claim is unglamorous plumbing: the dependency list is express, jsonwebtoken, bcrypt, cookie-parser, pg and dotenv, plus helmet, express-rate-limit and cors [19], and the thirty-second step people skip is generating two distinct 256-bit-plus secrets rather than the literal string "secret" [17].

Worth checking this week, before anyone writes code: can you log out one named user right now, without touching anyone else's session? If the answer is secret rotation, you do not have revocation [6][7]. If the answer is a sessions table you can delete a row from, check whether refresh tokens in it are hashed [14], and whether reuse of an already-rotated token raises anything at all [16].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories