← Back to Guides

JWT Authentication & Authorization Guide

📖 13 min read | 📅 Updated: January 2025 | 🏷️ Backend & APIs

Introduction

JSON Web Tokens (JWT) provide a stateless authentication mechanism perfect for modern APIs and microservices. This guide covers JWT implementation, refresh tokens, role-based access control (RBAC), and security best practices.

1. JWT Basics

JWT Structure

JWT Format: header.payload.signature

Header (Algorithm & Type):
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (Claims):
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

Signature:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

2. Generate & Verify JWT

// Install jsonwebtoken
npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken

// src/utils/jwt.ts
import jwt from 'jsonwebtoken';

interface TokenPayload {
  userId: string;
  email: string;
  role: string;
}

export const generateAccessToken = (payload: TokenPayload): string => {
  return jwt.sign(
    payload,
    process.env.JWT_SECRET!,
    { expiresIn: '15m' } // Short-lived
  );
};

export const generateRefreshToken = (payload: TokenPayload): string => {
  return jwt.sign(
    payload,
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' } // Long-lived
  );
};

export const verifyAccessToken = (token: string): TokenPayload => {
  return jwt.verify(token, process.env.JWT_SECRET!) as TokenPayload;
};

export const verifyRefreshToken = (token: string): TokenPayload => {
  return jwt.verify(token, process.env.JWT_REFRESH_SECRET!) as TokenPayload;
};

3. Authentication Flow

Login Endpoint

// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import User from '../models/user.model';
import { generateAccessToken, generateRefreshToken } from '../utils/jwt';

export const login = async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
    
    // Find user
    const user = await User.findOne({ email }).select('+password');
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Verify password
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Generate tokens
    const payload = {
      userId: user.id,
      email: user.email,
      role: user.role,
    };
    
    const accessToken = generateAccessToken(payload);
    const refreshToken = generateRefreshToken(payload);
    
    // Store refresh token in DB
    await User.findByIdAndUpdate(user.id, {
      refreshToken,
      lastLogin: new Date(),
    });
    
    // Set refresh token in httpOnly cookie
    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    });
    
    res.json({
      accessToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
    });
  } catch (error) {
    res.status(500).json({ error: 'Server error' });
  }
};

Refresh Token Endpoint

export const refreshToken = async (req: Request, res: Response) => {
  try {
    const refreshToken = req.cookies.refreshToken;
    
    if (!refreshToken) {
      return res.status(401).json({ error: 'Refresh token not provided' });
    }
    
    // Verify refresh token
    const decoded = verifyRefreshToken(refreshToken);
    
    // Check if token exists in DB
    const user = await User.findOne({
      _id: decoded.userId,
      refreshToken,
    });
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    // Generate new tokens
    const payload = {
      userId: user.id,
      email: user.email,
      role: user.role,
    };
    
    const newAccessToken = generateAccessToken(payload);
    const newRefreshToken = generateRefreshToken(payload);
    
    // Update refresh token in DB
    await User.findByIdAndUpdate(user.id, {
      refreshToken: newRefreshToken,
    });
    
    // Update cookie
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });
    
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
};

Logout Endpoint

export const logout = async (req: Request, res: Response) => {
  try {
    const userId = req.user.id; // From auth middleware
    
    // Remove refresh token from DB
    await User.findByIdAndUpdate(userId, {
      $unset: { refreshToken: 1 },
    });
    
    // Clear cookie
    res.clearCookie('refreshToken');
    
    res.json({ message: 'Logged out successfully' });
  } catch (error) {
    res.status(500).json({ error: 'Server error' });
  }
};

4. Authentication Middleware

// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';
import User from '../models/user.model';

declare global {
  namespace Express {
    interface Request {
      user?: any;
    }
  }
}

export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    // Get token from header
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ error: 'No token provided' });
    }
    
    const token = authHeader.substring(7);
    
    // Verify token
    const decoded = verifyAccessToken(token);
    
    // Get user from database
    const user = await User.findById(decoded.userId).select('-password');
    
    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }
    
    // Attach user to request
    req.user = user;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'Token expired',
        code: 'TOKEN_EXPIRED'
      });
    }
    
    res.status(401).json({ error: 'Invalid token' });
  }
};

5. Role-Based Access Control (RBAC)

Authorization Middleware

// src/middleware/authorize.middleware.ts
import { Request, Response, NextFunction } from 'express';

export const authorize = (...allowedRoles: string[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    
    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({ 
        error: 'Access denied',
        message: `Requires one of: ${allowedRoles.join(', ')}`
      });
    }
    
    next();
  };
};

// Usage in routes
import { authenticate } from './middleware/auth.middleware';
import { authorize } from './middleware/authorize.middleware';

// Only admins can access
router.delete('/users/:id', 
  authenticate,
  authorize('admin'),
  deleteUser
);

// Admins and moderators
router.patch('/posts/:id/approve',
  authenticate,
  authorize('admin', 'moderator'),
  approvePost
);

Permission-Based Authorization

// More granular permissions
enum Permission {
  CREATE_USER = 'create:user',
  READ_USER = 'read:user',
  UPDATE_USER = 'update:user',
  DELETE_USER = 'delete:user',
  MANAGE_POSTS = 'manage:posts',
}

interface Role {
  name: string;
  permissions: Permission[];
}

const roles: Record = {
  admin: {
    name: 'admin',
    permissions: [
      Permission.CREATE_USER,
      Permission.READ_USER,
      Permission.UPDATE_USER,
      Permission.DELETE_USER,
      Permission.MANAGE_POSTS,
    ],
  },
  moderator: {
    name: 'moderator',
    permissions: [
      Permission.READ_USER,
      Permission.MANAGE_POSTS,
    ],
  },
  user: {
    name: 'user',
    permissions: [
      Permission.READ_USER,
    ],
  },
};

export const requirePermission = (...permissions: Permission[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = roles[req.user.role];
    
    const hasPermission = permissions.every(permission =>
      userRole.permissions.includes(permission)
    );
    
    if (!hasPermission) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
};

// Usage
router.delete('/users/:id',
  authenticate,
  requirePermission(Permission.DELETE_USER),
  deleteUser
);

6. Token Blacklisting

// Using Redis for token blacklist
import Redis from 'ioredis';

const redis = new Redis();

export const blacklistToken = async (token: string, expiresIn: number) => {
  await redis.setex(`blacklist:${token}`, expiresIn, '1');
};

export const isTokenBlacklisted = async (token: string): Promise => {
  const result = await redis.get(`blacklist:${token}`);
  return result === '1';
};

// Update authenticate middleware
export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const token = req.headers.authorization?.substring(7);
    
    // Check blacklist
    if (await isTokenBlacklisted(token!)) {
      return res.status(401).json({ error: 'Token has been revoked' });
    }
    
    const decoded = verifyAccessToken(token!);
    req.user = await User.findById(decoded.userId);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Logout with blacklist
export const logout = async (req: Request, res: Response) => {
  const token = req.headers.authorization?.substring(7);
  
  if (token) {
    const decoded = verifyAccessToken(token);
    const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
    await blacklistToken(token, expiresIn);
  }
  
  res.json({ message: 'Logged out successfully' });
};

7. Security Best Practices

✓ JWT Security Checklist:

8. Client-Side Implementation

// React auth context
import { createContext, useState, useEffect } from 'react';
import axios from 'axios';

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [accessToken, setAccessToken] = useState(null);
  
  // Axios interceptor for token refresh
  useEffect(() => {
    const interceptor = axios.interceptors.response.use(
      response => response,
      async error => {
        const originalRequest = error.config;
        
        if (error.response?.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          
          try {
            const { data } = await axios.post('/api/auth/refresh', {}, {
              withCredentials: true
            });
            
            setAccessToken(data.accessToken);
            originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
            
            return axios(originalRequest);
          } catch (refreshError) {
            setUser(null);
            setAccessToken(null);
            window.location.href = '/login';
            return Promise.reject(refreshError);
          }
        }
        
        return Promise.reject(error);
      }
    );
    
    return () => axios.interceptors.response.eject(interceptor);
  }, []);
  
  const login = async (email, password) => {
    const { data } = await axios.post('/api/auth/login', { email, password });
    setUser(data.user);
    setAccessToken(data.accessToken);
  };
  
  const logout = async () => {
    await axios.post('/api/auth/logout', {}, {
      headers: { Authorization: `Bearer ${accessToken}` }
    });
    setUser(null);
    setAccessToken(null);
  };
  
  return (
    
      {children}
    
  );
};

Conclusion

JWT provides a scalable, stateless authentication mechanism ideal for modern APIs. By implementing refresh tokens, proper token rotation, role-based access control, and following security best practices, you can build secure authentication systems that protect your users and scale effectively.

💡 Pro Tip: Always use short-lived access tokens (15 minutes or less) combined with refresh tokens. Implement token rotation where each refresh generates a new refresh token, making stolen tokens useless after one use.