JWT Authentication · learncode.live

What is JWT?

JWT (JSON Web Token) is a compact, self-contained, stateless authentication mechanism. The token contains all the information the server needs - no session lookups required.

JWT Structure:
┌─────────────┬──────────────────────┬──────────────────────┐
│   Header    │       Payload        │      Signature       │
│  {          │  {                   │  HMACSHA256(         │
│   "alg":    │   "userId": 1,       │   base64(header) +   │
│   "HS256"   │   "role": "admin",   │   "." +              │
│  }          │   "iat": 1689000000, │   base64(payload),   │
│             │   "exp": 1689086400  │   secret             │
│             │  }                   │  )                   │
└─────────────┴──────────────────────┴──────────────────────┘
       │                │                       │
    base64url         base64url              signed hash

Stateless: The server doesn’t store the token - it just verifies the signature.

Browser                          Server
  │                                │
  │── POST /login ────────────────→│
  │   { username, password }       │ Verify credentials
  │                                │ Create JWT { userId, role }
  │←── { token: "eyJhbG..." } ────│ Sign with secret
  │                                │
  │── GET /api/users ─────────────→│
  │   Authorization: Bearer eyJ... │ Verify signature
  │                                │ Decode payload → userId
  │←── 200 JSON data ──────────────│

Installation

npm install jsonwebtoken bcrypt
  • jsonwebtoken - create and verify JWTs
  • bcrypt - hash passwords before storing

Creating a JWT

const jwt = require('jsonwebtoken');

const payload = {
  userId: user._id,
  username: user.username,
  role: user.role
};

const secret = process.env.JWT_SECRET || 'fallback-secret';

const token = jwt.sign(payload, secret, {
  expiresIn: '24h' // Token expires in 24 hours
});

Payload Best Practices

// ✅ Good - minimal, essential data
const payload = {
  sub: user._id,      // Subject (standard claim)
  role: user.role,    // For authorization
  iss: 'myapp',       // Issuer (standard claim)
  iat: Date.now()     // Issued at (auto-added)
};

// ❌ Bad - sensitive data
const payload = {
  password: user.password,  // Never include secrets!
  ssn: '123-45-6789',       // Never include PII!
  creditCard: '4111...'     // Never include financial data!
};

Token Expiration Options

// Short-lived access token
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });

// Long-lived refresh token
const refreshToken = jwt.sign(payload, refreshSecret, { expiresIn: '7d' });

// Numeric expiration (in seconds)
const token = jwt.sign(payload, secret, { expiresIn: 3600 });

// Custom expiration date
const token = jwt.sign(payload, secret, { expiresIn: '30 days' });

Verifying a JWT

function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    return { valid: true, decoded };
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return { valid: false, error: 'Token expired' };
    }
    if (err.name === 'JsonWebTokenError') {
      return { valid: false, error: 'Invalid token' };
    }
    return { valid: false, error: 'Authentication failed' };
  }
}

Auth Middleware

// middleware/auth.js
function authenticate(req, res, next) {
  const authHeader = req.headers['authorization'];

  if (!authHeader) {
    return res.status(401).json({ error: 'No authorization header' });
  }

  // Format: "Bearer <token>"
  const token = authHeader.startsWith('Bearer ')
    ? authHeader.slice(7)
    : authHeader;

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach user data to request
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(403).json({ error: 'Invalid token' });
  }
}

function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

module.exports = { authenticate, authorize };

Complete Auth Flow

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { authenticate, authorize } = require('./middleware/auth');

const app = express();
app.use(express.json());

// Mock user store
const users = [];

// Register
app.post('/register', async (req, res) => {
  const { username, password } = req.body;

  if (users.find(u => u.username === username)) {
    return res.status(409).json({ error: 'Username already exists' });
  }

  const hashedPassword = await bcrypt.hash(password, 10);
  const user = { id: users.length + 1, username, password: hashedPassword, role: 'user' };
  users.push(user);

  res.status(201).json({ message: 'User created' });
});

// Login
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username);

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { sub: user.id, username: user.username, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  res.json({
    token,
    user: { id: user.id, username: user.username, role: user.role }
  });
});

// Protected routes
app.get('/profile', authenticate, (req, res) => {
  res.json({ user: req.user });
});

app.get('/admin', authenticate, authorize('admin'), (req, res) => {
  res.json({ secret: 'Admin dashboard data' });
});

app.get('/moderator', authenticate, authorize('admin', 'moderator'), (req, res) => {
  res.json({ secret: 'Moderator panel' });
});

app.listen(3000);

Refresh Token Pattern

const refreshTokens = []; // Use Redis/DB in production

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username);

  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  refreshTokens.push(refreshToken);

  res.json({ accessToken, refreshToken });
});

// Refresh endpoint
app.post('/token/refresh', (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }

  if (!refreshTokens.includes(refreshToken)) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }

  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);

    const newAccessToken = jwt.sign(
      { sub: decoded.sub, role: decoded.role },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
});

// Logout - invalidate refresh token
app.post('/logout', (req, res) => {
  const { refreshToken } = req.body;
  const index = refreshTokens.indexOf(refreshToken);
  if (index > -1) refreshTokens.splice(index, 1);
  res.json({ message: 'Logged out' });
});

JWT vs Sessions

FeatureJWTSessions
StateStateless (no server storage)Stateful (server stores data)
ScalabilityEasy - no shared session storeNeeds Redis/DB shared across instances
PayloadVisible in token (don’t put secrets)Hidden on server
RevocationHard - can’t invalidate until expiryEasy - delete from session store
SizeLarger (sent with every request)Small (just session ID cookie)
Best forAPIs, mobile apps, microservicesServer-rendered web apps

Storing Tokens on the Client

// ✅ Option 1: localStorage (simple but XSS-vulnerable)
localStorage.setItem('token', token);
const token = localStorage.getItem('token');

// ✅ Option 2: httpOnly cookie (XSS-safe but CSRF-vulnerable)
res.cookie('token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000
});

// ✅ Option 3: Memory (most secure, but lost on refresh)
// Store in a closure variable

Token Blacklisting (for immediate revocation)

const blacklistedTokens = new Set();

function blacklistToken(token) {
  const decoded = jwt.decode(token);
  blacklistedTokens.add(token);

  // Auto-remove after expiry
  setTimeout(() => blacklistedTokens.delete(token),
    (decoded.exp * 1000) - Date.now());
}

function authenticate(req, res, next) {
  const token = req.headers['authorization']?.slice(7);

  if (blacklistedTokens.has(token)) {
    return res.status(401).json({ error: 'Token revoked' });
  }

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(403).json({ error: 'Invalid token' });
  }
}

Key Takeaways

  • JWT is stateless - no server-side storage needed
  • Use jwt.sign(payload, secret, { expiresIn }) to create tokens
  • Use authenticate middleware to verify tokens on protected routes
  • Use authorize middleware for role-based access control
  • Store only non-sensitive data in the payload (userId, role)
  • Use short-lived access tokens (15min) + long-lived refresh tokens (7d)
  • Hash passwords with bcrypt before storing
  • Pass tokens via Authorization: Bearer <token> header
  • JWT is best for APIs and mobile apps; sessions are better for server-rendered apps
Courses