100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js & Express Backend
35 minintermediate

JWT Access and Refresh Token Flow

JWT Access and Refresh Token Flow

JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims between two parties. In an Express API, JWTs are the dominant mechanism for stateless authentication: the server issues a signed token after login, the client stores it and attaches it to subsequent requests, and the server verifies the signature without consulting a database. The access/refresh token pattern extends this by issuing two tokens — a short-lived access token and a long-lived refresh token — to balance security and user experience.

Analogy🏏Cricket
Think of it like cricket: When a player is selected for the national squad, the BCCI issues two credentials: a match-day pass (access token) valid only for the current series, and a player contract (refresh token) valid for the entire season. The match-day pass gets you through the player entrance every game day; when it expires between series, you show your contract to get a new match-day pass without having to re-audition. If you lose the contract (token theft), the BCCI can revoke it centrally. Jasprit Bumrah's contract lets him keep playing across series; his daily pass is freshly issued each morning.

JWT Structure and Signing

A JWT consists of three base64url-encoded parts separated by dots: Header.Payload.Signature. The header identifies the algorithm (HS256, RS256). The payload carries claims: iss (issuer), sub (subject/user ID), exp (expiry), iat (issued at), and any custom claims you add. The signature is a HMAC or RSA hash of the header and payload using your secret key. Verification re-computes the hash and compares it, confirming the token has not been tampered with.

bash
npm install jsonwebtoken
javascript
// utils/jwt.js
const jwt = require('jsonwebtoken');

const ACCESS_SECRET  = process.env.JWT_ACCESS_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

function signAccess(payload) {
  return jwt.sign(payload, ACCESS_SECRET, { expiresIn: '15m' });
}

function signRefresh(payload) {
  return jwt.sign(payload, REFRESH_SECRET, { expiresIn: '7d' });
}

function verifyAccess(token) {
  return jwt.verify(token, ACCESS_SECRET); // throws on invalid/expired
}

function verifyRefresh(token) {
  return jwt.verify(token, REFRESH_SECRET);
}

module.exports = { signAccess, signRefresh, verifyAccess, verifyRefresh };
Analogy🏏Cricket
Think of it like cricket: Think of the JWT signature as the match ball's Kookaburra stamp. Anyone can inspect the ball's condition (read the payload), but only the manufacturer can stamp it authentically (sign it). An umpire can instantly verify the stamp is genuine without calling Kookaburra's office (no database lookup). Tamper with the ball and the stamp breaks — the umpire rejects it on sight.

Login and Token Issuance

The login endpoint verifies credentials, then issues both an access token (returned in the response body) and a refresh token (stored in an HttpOnly cookie or returned in the body for mobile clients). HttpOnly cookies prevent JavaScript from reading the refresh token, reducing XSS risk. The access token is sent as a Bearer token in the Authorization header on subsequent requests.

javascript
// routes/auth.js
const router   = require('express').Router();
const bcrypt   = require('bcrypt');
const User     = require('../models/User');
const { signAccess, signRefresh } = require('../utils/jwt');

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const payload      = { sub: user.id, role: user.role };
  const accessToken  = signAccess(payload);
  const refreshToken = signRefresh({ sub: user.id });

  // Store hashed refresh token in DB for revocation
  await User.updateOne({ _id: user.id }, { refreshTokenHash: await bcrypt.hash(refreshToken, 10) });

  res.cookie('refreshToken', refreshToken, {
    httpOnly: true, secure: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000
  });

  res.json({ accessToken });
});
Analogy🏏Cricket
Think of it like cricket: The dressing room manager (login route) checks the player's ID badge and biometrics (email + password), then issues two items: a day-pass wristband (access token) worn visibly, and a sealed envelope with a season contract (refresh token in HttpOnly cookie) locked in the player's personal safe. The wristband is checked at every gate; the envelope stays hidden.
Lesson 19 of 36
0% complete