Skip to content
Node.js8 min read

JWT Authentication in Node.js: Access and Refresh Tokens

Short-lived access tokens, rotating refresh tokens, and where to store each. The JWT patterns we use in production Node.js APIs, with the pitfalls flagged.

JSON Web Tokens are simple to mint and easy to get wrong. The failure mode is rarely the cryptography; it is the lifecycle — tokens that live too long, refresh flows that cannot be revoked, and secrets stored where any injected script can read them. This is the pattern we deploy on client projects, and the reasoning behind each decision.

Two tokens, two jobs

The access token is a signed JWT carrying the user ID and roles, with a lifetime of five to fifteen minutes. Because it is verified statelessly, every service in your estate can check it without a database call. The refresh token is the opposite: an opaque random string, stored server-side against the user and device, with a lifetime of days or weeks. Its only job is to obtain new access tokens, and because it lives in a database it can be revoked instantly.

js
import jwt from 'jsonwebtoken';
import { randomBytes, createHash } from 'node:crypto';

function issueTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, roles: user.roles },
    process.env.JWT_SECRET,
    { expiresIn: '10m', issuer: 'api.strcli.com' }
  );
  const refreshToken = randomBytes(32).toString('hex');
  const refreshHash = createHash('sha256')
    .update(refreshToken).digest('hex');
  return { accessToken, refreshToken, refreshHash };
}

Note that we store only a hash of the refresh token. If the database leaks, the attacker holds hashes they cannot replay — the same reasoning as password storage, applied to tokens.

Rotation and reuse detection

Every time a refresh token is used, invalidate it and issue a new one. This rotation gives you a powerful signal: if a token that was already rotated arrives again, either the client retried oddly or the token was stolen and both parties are now using the family. The safe response is to revoke the entire token family and force a fresh login. It is a few extra columns — family ID, rotated-at, revoked flag — for a meaningful detection capability most APIs lack.

Where tokens should live in the browser

  • Refresh token: httpOnly, Secure, SameSite cookie scoped to the refresh endpoint path. JavaScript can never read it.
  • Access token: kept in memory only. A page refresh simply triggers a silent refresh call.
  • Never localStorage for either — any XSS on your page becomes full account takeover.
  • Mobile and server-to-server clients can use secure platform storage instead; the cookie rules are a browser concern.

Verification details that bite

Always pass an explicit algorithms allow-list to jwt.verify — historic attacks have relied on servers accepting a token that declares alg: none or swaps RS256 for HS256. Validate issuer and audience so a token minted for one service cannot be replayed against another. And keep clock tolerance small; a generous clockTolerance quietly extends every token's lifetime.

js
const payload = jwt.verify(token, process.env.JWT_SECRET, {
  algorithms: ['HS256'],
  issuer: 'api.strcli.com',
  audience: 'strcli-web',
});

Key management is the part that outlives the code. Prefer asymmetric signing — RS256 or EdDSA — once more than one service verifies tokens, so verifiers hold only the public key. Publish keys with a key ID so rotation is a deploy rather than an outage: sign new tokens with the new key while accepting both until the old one ages out. If you use a hosted identity provider, the JWKS endpoint pattern automates exactly this.

One final trade-off to be honest about: access tokens cannot be revoked mid-lifetime without giving up statelessness. A ten-minute lifetime bounds that exposure, and for genuinely sensitive actions — changing email, initiating payouts — re-verify against the database regardless of what the token claims.

Get the lifecycle right and JWTs are a fine tool. Get it wrong and they are unrevocable bearer credentials with your users' names on them. The pattern above has held up well across audits; steal it freely.

Start your project

Have an idea? Let's ship it together.

Tell us what you're building — we'll reply within one business day with an honest take and a clear next step.