Skip to content
intermediate Phase 9 · Backend with Node.js

JWT Authentication & Authorization

Implement JWT-based auth with access/refresh tokens, bcrypt hashing, RBAC, and secure middleware.

1h 30m
0 problems
Topic Progress 0%

JWT Authentication Flow

JWT Authentication Flow

How JWT Works

Client                          Server
  │                               │
  │  POST /api/auth/login         │
  │  { email, password }          │
  │  ─────────────────────────►   │
  │                               │  Verify credentials
  │                               │  Generate access token (15min)
  │                               │  Generate refresh token (7 days)
  │  { accessToken, refreshToken} │
  │  ◄─────────────────────────   │
  │                               │
  │  GET /api/data                │
  │  Authorization: Bearer <jwt>  │
  │  ─────────────────────────►   │
  │                               │  Verify token
  │  { data }                     │
  │  ◄─────────────────────────   │

Token Generation

import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';

const config = {
  accessTokenSecret: process.env.JWT_SECRET,
  refreshTokenSecret: process.env.REFRESH_SECRET,
  accessTokenExpiry: '15m',
  refreshTokenExpiry: '7d',
};

export function generateTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    config.accessTokenSecret,
    { expiresIn: config.accessTokenExpiry }
  );

  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    config.refreshTokenSecret,
    { expiresIn: config.refreshTokenExpiry }
  );

  return { accessToken, refreshToken };
}

export function verifyAccessToken(token) {
  return jwt.verify(token, config.accessTokenSecret);
}

export function verifyRefreshToken(token) {
  return jwt.verify(token, config.refreshTokenSecret);
}

Login Controller

import { comparePassword } from '../utils/crypto.js';
import { generateTokens } from '../utils/jwt.js';
import * as usersRepo from '../repositories/users.js';
import { AppError } from '../middleware/errorHandler.js';

export async function login(req, res, next) {
  try {
    const { email, password } = req.body;
    const user = await usersRepo.findByEmail(email);
    if (!user) throw new AppError('Invalid credentials', 401);

    const valid = await comparePassword(password, user.password);
    if (!valid) throw new AppError('Invalid credentials', 401);

    const { accessToken, refreshToken } = generateTokens(user);

    // Store refresh token hash in database
    await usersRepo.storeRefreshToken(user.id, refreshToken);

    res.json({ accessToken, refreshToken });
  } catch (error) {
    next(error);
  }
}

Password Security and Hashing

Password Security and Hashing

bcrypt Hashing

import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

export async function hashPassword(password) {
  return bcrypt.hash(password, SALT_ROUNDS);
}

export async function comparePassword(password, hash) {
  return bcrypt.compare(password, hash);
}

Registration with Validation

import { z } from 'zod';
import { hashPassword } from '../utils/crypto.js';

const registerSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  password: z.string().min(8).max(128)
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/,
      'Must include uppercase, lowercase, number, and special character'),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
});

export async function register(req, res, next) {
  try {
    const validated = registerSchema.parse(req.body);
    const { confirmPassword, ...userData } = validated;

    const existing = await usersRepo.findByEmail(userData.email);
    if (existing) throw new AppError('Email already registered', 409);

    const hashedPassword = await hashPassword(userData.password);
    const user = await usersRepo.create({
      ...userData,
      password: hashedPassword,
      role: 'user',
    });

    const tokens = generateTokens(user);
    res.status(201).json({ user: { id: user.id, name: user.name, email: user.email }, ...tokens });
  } catch (error) {
    next(error);
  }
}

Password Reset Flow

import { randomBytes } from 'crypto';

export async function requestPasswordReset(req, res, next) {
  try {
    const user = await usersRepo.findByEmail(req.body.email);
    if (!user) return res.json({ message: 'If email exists, reset link sent' });

    const token = randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour

    await usersRepo.storeResetToken(user.id, token, expires);
    await emailService.sendPasswordReset(user.email, token);

    res.json({ message: 'If email exists, reset link sent' });
  } catch (error) {
    next(error);
  }
}

export async function resetPassword(req, res, next) {
  try {
    const { token, newPassword } = req.body;
    const user = await usersRepo.findByResetToken(token);
    if (!user || user.resetExpires < new Date()) {
      throw new AppError('Invalid or expired token', 400);
    }

    const hashed = await hashPassword(newPassword);
    await usersRepo.updatePassword(user.id, hashed);
    await usersRepo.clearResetToken(user.id);

    res.json({ message: 'Password updated' });
  } catch (error) {
    next(error);
  }
}

Authentication Middleware

Authentication Middleware

JWT Verification Middleware

// middleware/auth.js
import { verifyAccessToken } from '../utils/jwt.js';
import { AppError } from './errorHandler.js';

export function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    throw new AppError('No token provided', 401, 'UNAUTHORIZED');
  }

  const token = authHeader.split(' ')[1];
  try {
    const payload = verifyAccessToken(token);
    req.user = payload;  // { sub, email, role, iat, exp }
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new AppError('Token expired', 401, 'TOKEN_EXPIRED');
    }
    throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
  }
}

Role-Based Access Control

export function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user) {
      throw new AppError('Not authenticated', 401);
    }
    if (!allowedRoles.includes(req.user.role)) {
      throw new AppError('Insufficient permissions', 403, 'FORBIDDEN');
    }
    next();
  };
}

// Usage
router.get('/admin/users', authenticate, authorize('admin'), adminController.listUsers);
router.patch('/posts/:id', authenticate, authorize('admin', 'editor'), postsController.update);
router.delete('/posts/:id', authenticate, postsController.remove);  // owner check in controller

Resource Ownership Check

export async function remove(req, res, next) {
  try {
    const post = await postsService.findById(req.params.id);
    if (!post) throw new AppError('Post not found', 404);

    // Only author or admin can delete
    if (post.author_id !== req.user.sub && req.user.role !== 'admin') {
      throw new AppError('Not authorized', 403);
    }

    await postsService.remove(req.params.id);
    res.status(204).end();
  } catch (error) {
    next(error);
  }
}

Token Refresh Endpoint

export async function refreshToken(req, res, next) {
  try {
    const { refreshToken } = req.body;
    if (!refreshToken) throw new AppError('Refresh token required', 400);

    const payload = verifyRefreshToken(refreshToken);
    const storedToken = await usersRepo.findRefreshToken(payload.sub, refreshToken);
    if (!storedToken) throw new AppError('Invalid refresh token', 401));

    // Rotate refresh token
    await usersRepo.removeRefreshToken(payload.sub, refreshToken);
    const user = await usersRepo.findById(payload.sub);
    const tokens = generateTokens(user);
    await usersRepo.storeRefreshToken(user.id, tokens.refreshToken);

    res.json(tokens);
  } catch (error) {
    next(error);
  }
}

Security Best Practices

Security Best Practices

Environment Configuration

// config/index.js
import { z } from 'zod';

const configSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),
  JWT_SECRET: z.string().min(32),
  REFRESH_SECRET: z.string().min(32),
  DATABASE_URL: z.string().url(),
  CORS_ORIGINS: z.string().transform(s => s.split(',')),
  RATE_LIMIT_WINDOW: z.coerce.number().default(900000),
  RATE_LIMIT_MAX: z.coerce.number().default(100),
});

export const config = configSchema.parse(process.env);

Security Headers and Hardening

import helmet from 'helmet';
import hpp from 'hpp';

// Security middleware
app.use(helmet());
app.use(hpp());  // HTTP Parameter Pollution protection

// Disable X-Powered-By
app.disable('x-powered-by');

// Trust proxy (if behind load balancer)
app.set('trust proxy', 1);

// Content Security Policy
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
  },
}));

Audit Logging

function auditLog(action) {
  return async (req, res, next) => {
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    
    await db.query(
      `INSERT INTO audit_logs (user_id, action, resource, resource_id, ip, duration_ms, status)
       VALUES ($1, $2, $3, $4, $5, $6, $7)`,
      [
        req.user?.sub || null,
        action,
        req.baseUrl,
        req.params.id || null,
        req.ip,
        duration,
        res.statusCode,
      ]
    );
  };
}

router.delete('/posts/:id', authenticate, auditLog('DELETE_POST'), postsController.remove);

Security Checklist

  • Use HTTPS in production
  • Store secrets in environment variables, never in code
  • Hash passwords with bcrypt (12+ rounds)
  • Set HttpOnly, Secure, SameSite cookies for refresh tokens
  • Implement rate limiting on auth endpoints
  • Validate and sanitize all input
  • Use parameterized queries (prevent SQL injection)
  • Set appropriate CORS origins
  • Enable security headers (Helmet)
  • Log security-relevant events for auditing