NestJS — Enterprise Node.js

Refresh Tokens & Secure Sessions

18 min Lesson 28 of 48

Refresh Tokens & Secure Sessions

Short-lived access tokens are secure but inconvenient — a 15-minute token means the user is logged out every 15 minutes. Refresh tokens solve this: a long-lived token that can mint new access tokens without re-entering a password. Getting this pattern right is what makes a production auth system both secure and usable.

The two-token pattern

  • Access token — short-lived (e.g. 15 min), sent on every request, stateless JWT.
  • Refresh token — long-lived (e.g. 7 days), used only to obtain a new access token, and stored server-side so it can be revoked.

When the access token expires, the client calls a /refresh endpoint with the refresh token and receives a fresh access token — no password needed.

Issuing both on login

async login(user: { id: number }) { const payload = { sub: user.id }; const accessToken = this.jwt.sign(payload, { expiresIn: '15m' }); const refreshToken = this.jwt.sign(payload, { expiresIn: '7d' }); // store a HASH of the refresh token, never the raw token await this.users.setRefreshHash(user.id, await bcrypt.hash(refreshToken, 10)); return { accessToken, refreshToken }; }
Store only a HASH of the refresh token, never the raw value. If your database leaks, hashed refresh tokens cannot be used by an attacker — exactly like passwords. Compare the incoming token against the stored hash on refresh.

The refresh endpoint

On refresh, verify the refresh token's signature, confirm it matches the stored hash for that user, then issue new tokens:

async refresh(userId: number, presentedToken: string) { const user = await this.users.findById(userId); const valid = user?.refreshHash && (await bcrypt.compare(presentedToken, user.refreshHash)); if (!valid) throw new UnauthorizedException(); return this.login(user); // issue a fresh pair (and rotate the refresh token) }

Refresh token rotation

Rotate on every refresh. Each time a refresh token is used, issue a NEW refresh token and invalidate the old one. If a stolen token is replayed after the real user already rotated, the reuse is detectable — a strong signal to revoke the whole session.

Where to store tokens on the client

Storage choice matters for security:

  • httpOnly cookie — not readable by JavaScript, so it resists XSS token theft. Preferred for the refresh token (pair with CSRF protection).
  • Memory — the access token can live in memory; lost on refresh, which is fine since it is short-lived.
  • localStorage — convenient but readable by any script, so vulnerable to XSS. Avoid for long-lived tokens.

Logout

Because access tokens are stateless, "logout" really means invalidating the refresh token server-side: clear the stored refresh hash so it can no longer mint new access tokens. The short-lived access token then simply expires on its own.

This is the trade-off of stateless auth. You cannot instantly kill a live access token, but with short expiry plus a revocable, hashed, rotating refresh token, you get a secure and practical session system.

Summary

Pair a short-lived access token with a long-lived, server-stored refresh token. Store only the refresh token's hash, verify and rotate it on each /refresh, prefer an httpOnly cookie for it, and "logout" by clearing the stored refresh hash. This delivers security (short access tokens, revocable refresh) and usability (no constant re-login). Next: authorization — deciding what an authenticated user may do.