Back to blog
Node.jsTypeScriptREST

JWT Authentication: What Most Tutorials Get Wrong

Most JWT tutorials show you the happy path. Here's what they skip — token refresh, secure storage, invalidation, and why 'stateless' is more complicated than it sounds.

September 8, 20243 min read

Every authentication tutorial I've seen follows the same script: sign a token on login, verify it on protected routes, done. Ship it.

What they don't cover is what happens when you actually use this in production.

The stateless myth

JWTs are sold as "stateless" — no database lookup needed to verify a request. That's true. But the moment you need to log a user out or invalidate a specific token, you're back to maintaining state somewhere.

The common workarounds are a token blocklist (defeats the purpose) or very short expiry times combined with refresh tokens (which is the right approach but significantly more complex).

Refresh token rotation

The pattern that actually works in production:

When the access token expires, the frontend silently requests a new one using the refresh token. When a refresh token is used, it gets rotated — the old one is invalidated and a new one is issued.

// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  
  // Verify the refresh token exists in DB
  const stored = await db.refreshToken.findUnique({
    where: { token: refreshToken }
  });
  
  if (!stored || stored.expiresAt < new Date()) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
  
  // Rotate: delete old, create new
  await db.refreshToken.delete({ where: { id: stored.id } });
  
  const newRefreshToken = generateRefreshToken();
  await db.refreshToken.create({
    data: { token: newRefreshToken, userId: stored.userId, expiresAt: thirtyDaysFromNow() }
  });
  
  const accessToken = signAccessToken({ userId: stored.userId });
  
  res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: true, sameSite: 'strict' });
  res.json({ accessToken });
});

Where to store tokens

The localStorage vs httpOnly cookie debate comes up in every auth discussion. My take:

Store access tokens in memory (a React context variable). Store refresh tokens in httpOnly cookies. Never store anything sensitive in localStorage.

localStorage is accessible to any JavaScript on your page. If you have an XSS vulnerability — and at some scale, you will — everything in localStorage is compromised. httpOnly cookies can't be read by JavaScript at all.

The parts most tutorials skip

A production auth system also needs to handle:

None of this is in the tutorial. All of it matters.

Practical advice

Start simple. For most projects, a basic JWT setup with reasonable expiry is fine. Add complexity only when you need it — when you have real users who need "log out everywhere" or when you're dealing with sensitive enough data that token theft is a real concern.

The architecture above is what I use in production projects. It took me a few iterations of bugs and edge cases to get to this point, which is why I wrote it down.

All posts