JWT Authentication Implementation

Module 26: Advanced Backend & API Development

Introduction to JWT Authentication

JSON Web Tokens (JWT) have become the industry standard for token-based authentication in modern web applications and APIs. In this lecture, we'll take a deep dive into implementing JWT authentication in real-world applications.

flowchart TD A[User] -->|Submit Credentials| B[Authentication Server] B -->|Verify Credentials| B B -->|Generate JWT| C[Signed JWT] C -->|Return Token| A A -->|Include JWT in Request| D[API Server] D -->|Verify JWT Signature| D D -->|Extract User Info| D D -->|Return Protected Resources| A

JWTs offer a stateless authentication mechanism where the token itself contains all the necessary information about the authenticated user. Think of a JWT like a digital passport - it contains your identity details, is issued by a trusted authority, has security features to prevent forgery, and can be presented at multiple checkpoints without needing to verify your original credentials each time.

Anatomy of a JWT

Before implementing JWT authentication, let's understand the structure of a JSON Web Token.

A JWT consists of three parts, separated by dots:

header.payload.signature
graph TD A[JWT] --> B[Header] A --> C[Payload] A --> D[Signature] B --> B1[Algorithm] B --> B2[Token Type] C --> C1[Registered Claims] C --> C2[Public Claims] C --> C3[Private Claims] C1 --> C1a[iss - Issuer] C1 --> C1b[sub - Subject] C1 --> C1c[exp - Expiration Time] C1 --> C1d[iat - Issued At] D --> D1["HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret)"]

Header

The header typically consists of two parts: the type of token (JWT) and the signing algorithm being used, such as HMAC SHA256 or RSA.


{
  "alg": "HS256",
  "typ": "JWT"
}
            

Payload

The payload contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.


{
  "sub": "1234567890",     // Subject (user ID)
  "name": "John Doe",      // Custom claim
  "admin": true,           // Custom claim
  "iat": 1516239022,       // Issued At (timestamp)
  "exp": 1516242622        // Expiration Time (timestamp)
}
            

Signature

The signature is created by taking the encoded header, the encoded payload, a secret, and the algorithm specified in the header, and signing them.


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

This signature ensures that the message hasn't been altered along the way. If someone tries to modify the payload, the signature would no longer match, and the token would be invalidated.

Visualizing a Real JWT

Here's an example of a real JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MjQyNjIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Each part can be decoded from base64Url:

Important: Since JWTs are base64Url encoded but not encrypted, anyone can decode them to read the information inside. Never store sensitive information like passwords or credit card numbers in a JWT payload!

Setting Up JWT Authentication in Node.js

Let's implement JWT authentication in a Node.js application using Express.

Installing Required Packages


npm install express jsonwebtoken bcrypt mongoose dotenv cors
            

Project Structure


jwt-auth-demo/
  ├── .env                 # Environment variables
  ├── package.json         # Project dependencies
  ├── server.js            # Main Express server
  ├── config/
  │   └── db.js            # Database configuration
  ├── controllers/
  │   └── authController.js # Authentication logic
  ├── middleware/
  │   └── authMiddleware.js # JWT verification middleware
  ├── models/
  │   └── User.js          # User model
  └── routes/
      ├── authRoutes.js    # Authentication routes
      └── userRoutes.js    # Protected user routes
            

Environment Variables

Create a .env file to store your sensitive configurations:


MONGO_URI=mongodb://localhost:27017/jwt-auth-demo
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRE=1h
PORT=3000
            

Database Configuration

Let's create a MongoDB connection using Mongoose:


// config/db.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    
    console.log(`MongoDB Connected: ${conn.connection.host}`);
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
};

module.exports = connectDB;
            

User Model

Create a User model with password hashing:


// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const UserSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, 'Please provide a username'],
    unique: true,
    trim: true,
    maxlength: [50, 'Username cannot be more than 50 characters']
  },
  email: {
    type: String,
    required: [true, 'Please provide an email'],
    unique: true,
    match: [
      /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
      'Please provide a valid email'
    ]
  },
  password: {
    type: String,
    required: [true, 'Please provide a password'],
    minlength: [6, 'Password must be at least 6 characters'],
    select: false // Don't return password by default
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// Hash password before saving
UserSchema.pre('save', async function(next) {
  // Only hash the password if it's modified
  if (!this.isModified('password')) {
    next();
  }
  
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Method to check if password matches
UserSchema.methods.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

// Method to get JWT token for this user
UserSchema.methods.getSignedJwtToken = function() {
  return jwt.sign(
    { id: this._id, role: this.role }, 
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRE }
  );
};

module.exports = mongoose.model('User', UserSchema);
            

Authentication Controller

Create the authentication logic:


// controllers/authController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

// Register a new user
exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // Create user
    const user = await User.create({
      username,
      email,
      password
    });
    
    // Generate JWT
    sendTokenResponse(user, 201, res);
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
};

// Login user
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Validate email & password
    if (!email || !password) {
      return res.status(400).json({
        success: false,
        error: 'Please provide an email and password'
      });
    }
    
    // Check for user
    const user = await User.findOne({ email }).select('+password');
    
    if (!user) {
      return res.status(401).json({
        success: false,
        error: 'Invalid credentials'
      });
    }
    
    // Check if password matches
    const isMatch = await user.matchPassword(password);
    
    if (!isMatch) {
      return res.status(401).json({
        success: false,
        error: 'Invalid credentials'
      });
    }
    
    // Generate JWT
    sendTokenResponse(user, 200, res);
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Get current logged in user
exports.getMe = async (req, res) => {
  try {
    const user = await User.findById(req.user.id);
    
    res.status(200).json({
      success: true,
      data: user
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};

// Log user out / clear cookie
exports.logout = async (req, res) => {
  res.status(200).json({
    success: true,
    data: {}
  });
};

// Helper function to get token from model, create cookie and send response
const sendTokenResponse = (user, statusCode, res) => {
  // Create token
  const token = user.getSignedJwtToken();
  
  res.status(statusCode).json({
    success: true,
    token
  });
};
            

Authentication Middleware

Create middleware to protect routes:


// middleware/authMiddleware.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

exports.protect = async (req, res, next) => {
  let token;
  
  // Get token from Authorization header
  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    // Set token from Bearer token
    token = req.headers.authorization.split(' ')[1];
  }
  
  // Check if token exists
  if (!token) {
    return res.status(401).json({
      success: false,
      error: 'Not authorized to access this route'
    });
  }
  
  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Add user from payload
    req.user = await User.findById(decoded.id);
    
    next();
  } catch (error) {
    return res.status(401).json({
      success: false,
      error: 'Not authorized to access this route'
    });
  }
};

// Grant access to specific roles
exports.authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        error: `Role ${req.user.role} is not authorized to access this route`
      });
    }
    next();
  };
};
            

Authentication Routes

Create routes for authentication:


// routes/authRoutes.js
const express = require('express');
const { register, login, getMe, logout } = require('../controllers/authController');
const { protect } = require('../middleware/authMiddleware');

const router = express.Router();

router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
router.get('/logout', logout);

module.exports = router;
            

Protected Routes

Create some protected user routes:


// routes/userRoutes.js
const express = require('express');
const { protect, authorize } = require('../middleware/authMiddleware');

const router = express.Router();

router.get(
  '/',
  protect,
  authorize('admin'),
  (req, res) => {
    res.status(200).json({
      success: true,
      message: 'You have access to all users'
    });
  }
);

router.get(
  '/profile',
  protect,
  (req, res) => {
    res.status(200).json({
      success: true,
      message: 'Your profile data',
      user: req.user
    });
  }
);

module.exports = router;
            

Main Server File

Finally, create the main Express server file:


// server.js
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const connectDB = require('./config/db');

// Load env vars
dotenv.config();

// Connect to database
connectDB();

// Route files
const authRoutes = require('./routes/authRoutes');
const userRoutes = require('./routes/userRoutes');

const app = express();

// Body parser
app.use(express.json());

// Enable CORS
app.use(cors());

// Mount routers
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);

// Basic route
app.get('/', (req, res) => {
  res.send('API is running...');
});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
  console.log(`Error: ${err.message}`);
  // Close server & exit process
  server.close(() => process.exit(1));
});
            

Running the Application


npm start
            

Now you have a complete JWT authentication system with user registration, login, and protected routes!

Advanced JWT Implementation Techniques

Let's explore some advanced techniques for using JWT in production applications.

Refresh Token Implementation

As discussed in our previous lecture, it's a good practice to use short-lived access tokens with a refresh token mechanism.

sequenceDiagram participant Client participant Server participant Database Client->>Server: Login with credentials Server->>Client: Access Token (short-lived) + Refresh Token (long-lived) Note over Client,Server: When access token expires Client->>Server: Request with expired access token Server->>Client: 401 Unauthorized Client->>Server: Request new token with refresh token Server->>Database: Validate refresh token Database->>Server: Token valid Server->>Client: New access token

// Add to User model
UserSchema.methods.getRefreshToken = function() {
  return jwt.sign(
    { id: this._id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRE }
  );
};

// models/Token.js - For storing refresh tokens
const mongoose = require('mongoose');

const TokenSchema = new mongoose.Schema({
  token: {
    type: String,
    required: true
  },
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  type: {
    type: String,
    enum: ['refresh'],
    required: true
  },
  expiresAt: {
    type: Date,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model('Token', TokenSchema);
            

Modify the auth controller to include refresh tokens:


// Add to authController.js
exports.refreshToken = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (!refreshToken) {
      return res.status(400).json({
        success: false,
        error: 'No refresh token provided'
      });
    }
    
    // Verify refresh token
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
    
    // Check if token exists in database
    const storedToken = await Token.findOne({
      token: refreshToken,
      userId: decoded.id,
      type: 'refresh'
    });
    
    if (!storedToken) {
      return res.status(401).json({
        success: false,
        error: 'Invalid refresh token'
      });
    }
    
    if (new Date() > storedToken.expiresAt) {
      await Token.findByIdAndDelete(storedToken._id);
      return res.status(401).json({
        success: false,
        error: 'Refresh token expired'
      });
    }
    
    // Get user
    const user = await User.findById(decoded.id);
    
    if (!user) {
      return res.status(401).json({
        success: false,
        error: 'User not found'
      });
    }
    
    // Generate new access token
    const accessToken = user.getSignedJwtToken();
    
    res.status(200).json({
      success: true,
      accessToken
    });
  } catch (error) {
    return res.status(401).json({
      success: false,
      error: 'Invalid refresh token'
    });
  }
};

// Update sendTokenResponse function
const sendTokenResponse = async (user, statusCode, res) => {
  // Create tokens
  const accessToken = user.getSignedJwtToken();
  const refreshToken = user.getRefreshToken();
  
  // Save refresh token to database
  await Token.create({
    token: refreshToken,
    userId: user._id,
    type: 'refresh',
    expiresAt: new Date(Date.now() + parseInt(process.env.REFRESH_TOKEN_EXPIRE_MS))
  });
  
  res.status(statusCode).json({
    success: true,
    accessToken,
    refreshToken
  });
};

// Add to logout function
exports.logout = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (refreshToken) {
      // Delete refresh token from database
      await Token.findOneAndDelete({ token: refreshToken });
    }
    
    res.status(200).json({
      success: true,
      data: {}
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};
            

Add the refresh token route:


// Add to authRoutes.js
router.post('/refresh-token', refreshToken);
            

JWT Token Blacklisting

When access tokens have a longer lifespan, you might need to blacklist them in some cases (e.g., after logout).


// models/BlacklistedToken.js
const mongoose = require('mongoose');

const BlacklistedTokenSchema = new mongoose.Schema({
  token: {
    type: String,
    required: true,
    unique: true
  },
  expiresAt: {
    type: Date,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now,
    expires: 86400 // Auto-delete after 24 hours using TTL index
  }
});

module.exports = mongoose.model('BlacklistedToken', BlacklistedTokenSchema);

// Update authMiddleware.js
exports.protect = async (req, res, next) => {
  let token;
  
  // ... token extraction code ...
  
  try {
    // Check if token is blacklisted
    const blacklisted = await BlacklistedToken.findOne({ token });
    
    if (blacklisted) {
      return res.status(401).json({
        success: false,
        error: 'Token has been revoked'
      });
    }
    
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Add user from payload
    req.user = await User.findById(decoded.id);
    
    next();
  } catch (error) {
    // ... error handling ...
  }
};

// Update logout function
exports.logout = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    // Get access token from header
    const authHeader = req.headers.authorization;
    const accessToken = authHeader && authHeader.split(' ')[1];
    
    if (accessToken) {
      // Get token expiration from JWT
      const decoded = jwt.decode(accessToken);
      const expiresAt = new Date(decoded.exp * 1000);
      
      // Add to blacklist
      await BlacklistedToken.create({
        token: accessToken,
        expiresAt
      });
    }
    
    if (refreshToken) {
      // Delete refresh token from database
      await Token.findOneAndDelete({ token: refreshToken });
    }
    
    res.status(200).json({
      success: true,
      data: {}
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
};
            

Important: Token blacklisting defeats one of the main advantages of JWTs (statelessness) since you now need to check a database for every request. This is why refresh tokens are generally preferred - keep access tokens short-lived so blacklisting isn't needed.

Security Considerations for JWT

JWTs come with their own set of security considerations that you should address when implementing.

Signature Algorithm Selection

Choose the appropriate algorithm for your security needs:


// Using RS256 (asymmetric)
const fs = require('fs');
const jwt = require('jsonwebtoken');

// Read private key
const privateKey = fs.readFileSync('private.key');

// Sign token
const token = jwt.sign(
  { userId: user._id },
  privateKey,
  {
    algorithm: 'RS256',
    expiresIn: '1h'
  }
);

// On the verification side
const publicKey = fs.readFileSync('public.key');

// Verify token
try {
  const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  // Token is valid
} catch (error) {
  // Token is invalid
}
            

Asymmetric algorithms are particularly useful in microservice architectures where the service that verifies tokens is different from the service that issues them.

Token Storage on the Client

Where to store JWTs on the client side is a contentious topic with different security trade-offs:

Storage Method Pros Cons
Local Storage / Session Storage
  • Easy to access from JavaScript
  • Persists across page refreshes
  • Vulnerable to XSS attacks
  • Available to all JavaScript in the same origin
HTTP-only Cookies
  • Protected from JavaScript access
  • Sent automatically with requests
  • Vulnerable to CSRF attacks
  • Limited to the domain they're issued for
JavaScript Memory (Variables)
  • Protected from CSRF attacks
  • Lost on page refresh
  • Still vulnerable to XSS
  • Lost on page refresh

Recommended Approach

A common pattern is to store the access token in memory (or a JavaScript variable) and the refresh token in an HTTP-only cookie:


// Server-side - Setting HTTP-only cookie for refresh token
exports.login = async (req, res) => {
  // ... authentication logic ...
  
  // Create tokens
  const accessToken = user.getSignedJwtToken();
  const refreshToken = user.getRefreshToken();
  
  // Save refresh token to cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true, // Prevents JavaScript access
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    sameSite: 'strict', // CSRF protection
    maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
  });
  
  // Send access token in response
  res.status(200).json({
    success: true,
    accessToken
  });
};

// Client-side - Storing access token in memory
let accessToken = null;

async function login(email, password) {
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email, password }),
      credentials: 'include' // Necessary for cookies
    });
    
    const data = await response.json();
    
    if (data.success) {
      // Store access token in memory
      accessToken = data.accessToken;
      return true;
    }
    
    return false;
  } catch (error) {
    console.error('Login error:', error);
    return false;
  }
}

async function refreshAccessToken() {
  try {
    const response = await fetch('/api/auth/refresh-token', {
      method: 'POST',
      credentials: 'include' // Send cookies
    });
    
    const data = await response.json();
    
    if (data.success) {
      // Update access token in memory
      accessToken = data.accessToken;
      return true;
    }
    
    return false;
  } catch (error) {
    console.error('Token refresh error:', error);
    return false;
  }
}
            

Common JWT Security Pitfalls


// Bad - No algorithm restriction
jwt.verify(token, secret);

// Good - Restrict allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] });
            

Implementing JWT in Different Frontend Frameworks

Let's look at how to implement JWT authentication in popular frontend frameworks.

React with Axios


// src/services/authService.js
import axios from 'axios';

const API_URL = 'http://localhost:3000/api/auth/';

// Create axios instance
const api = axios.create({
  baseURL: 'http://localhost:3000/api',
  headers: {
    'Content-Type': 'application/json'
  }
});

// Add a request interceptor to add the auth token
api.interceptors.request.use(
  (config) => {
    const token = getAccessToken();
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Add a response interceptor to handle token refresh
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    // If error is not 401 or request has already been retried, reject
    if (error.response.status !== 401 || originalRequest._retry) {
      return Promise.reject(error);
    }
    
    originalRequest._retry = true;
    
    try {
      // Attempt to refresh the token
      const { data } = await axios.post(API_URL + 'refresh-token', {}, {
        withCredentials: true // Send cookies
      });
      
      // If token refresh is successful
      if (data.accessToken) {
        // Update the token in storage
        setAccessToken(data.accessToken);
        
        // Update the authorization header
        originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
        
        // Retry the original request
        return api(originalRequest);
      }
      
      // If refresh fails, logout
      logout();
      return Promise.reject(error);
    } catch (refreshError) {
      // If refresh fails, logout
      logout();
      return Promise.reject(refreshError);
    }
  }
);

// Access token management
let inMemoryToken = null;

function getAccessToken() {
  return inMemoryToken;
}

function setAccessToken(token) {
  inMemoryToken = token;
}

// Auth service functions
export const register = async (username, email, password) => {
  const response = await axios.post(API_URL + 'register', {
    username,
    email,
    password
  });
  
  if (response.data.accessToken) {
    setAccessToken(response.data.accessToken);
  }
  
  return response.data;
};

export const login = async (email, password) => {
  const response = await axios.post(API_URL + 'login', {
    email,
    password
  }, {
    withCredentials: true // For refresh token cookie
  });
  
  if (response.data.accessToken) {
    setAccessToken(response.data.accessToken);
  }
  
  return response.data;
};

export const logout = () => {
  axios.post(API_URL + 'logout', {}, {
    withCredentials: true
  });
  setAccessToken(null);
};

export const getCurrentUser = async () => {
  return api.get('/auth/me');
};

export default api;
            

Vue with Vuex


// src/store/auth.module.js
import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: { loggedIn: false }, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, { email, password }) {
      return AuthService.login(email, password).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, { username, email, password }) {
      return AuthService.register(username, email, password).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status.loggedIn = true;
      state.user = user;
    },
    loginFailure(state) {
      state.status.loggedIn = false;
      state.user = null;
    },
    logout(state) {
      state.status.loggedIn = false;
      state.user = null;
    },
    registerSuccess(state) {
      state.status.loggedIn = false;
    },
    registerFailure(state) {
      state.status.loggedIn = false;
    }
  }
};
            

Angular with HttpInterceptor


// src/app/helpers/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { 
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HTTP_INTERCEPTORS,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
import { TokenStorageService } from '../services/token-storage.service';
import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshTokenSubject: BehaviorSubject = new BehaviorSubject(null);

  constructor(
    private tokenService: TokenStorageService,
    private authService: AuthService
  ) {}

  intercept(req: HttpRequest, next: HttpHandler): Observable> {
    let authReq = req;
    const token = this.tokenService.getToken();
    
    if (token != null) {
      authReq = this.addTokenHeader(req, token);
    }
    
    return next.handle(authReq).pipe(
      catchError(error => {
        if (error instanceof HttpErrorResponse && error.status === 401) {
          return this.handle401Error(authReq, next);
        }
        return throwError(error);
      })
    );
  }

  private handle401Error(request: HttpRequest, next: HttpHandler) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      this.refreshTokenSubject.next(null);

      return this.authService.refreshToken().pipe(
        switchMap((token: any) => {
          this.isRefreshing = false;
          this.refreshTokenSubject.next(token.accessToken);
          return next.handle(this.addTokenHeader(request, token.accessToken));
        }),
        catchError((err) => {
          this.isRefreshing = false;
          this.tokenService.signOut();
          return throwError(err);
        })
      );
    }

    return this.refreshTokenSubject.pipe(
      filter(token => token !== null),
      take(1),
      switchMap(token => next.handle(this.addTokenHeader(request, token)))
    );
  }

  private addTokenHeader(request: HttpRequest, token: string) {
    return request.clone({
      headers: request.headers.set('Authorization', `Bearer ${token}`)
    });
  }
}

export const authInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];
            

Testing JWT Authentication

Proper testing is crucial for ensuring your JWT implementation is secure.

Unit Testing Authentication Controller


// tests/authController.test.js
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');

describe('Authentication Controller', () => {
  let user;
  
  beforeAll(async () => {
    await mongoose.connect(process.env.MONGO_URI_TEST);
    
    // Create a test user
    user = await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: 'password123'
    });
  });
  
  afterAll(async () => {
    await User.deleteMany({});
    await mongoose.connection.close();
  });
  
  describe('POST /api/auth/register', () => {
    it('should register a new user and return a token', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          username: 'newuser',
          email: 'new@example.com',
          password: 'password123'
        });
      
      expect(res.statusCode).toEqual(201);
      expect(res.body).toHaveProperty('token');
      expect(res.body).toHaveProperty('success', true);
      
      // Verify token is valid
      const decoded = jwt.verify(res.body.token, process.env.JWT_SECRET);
      expect(decoded).toHaveProperty('id');
    });
    
    it('should not register a user with existing email', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          username: 'duplicate',
          email: 'test@example.com', // Already exists
          password: 'password123'
        });
      
      expect(res.statusCode).toEqual(400);
      expect(res.body).toHaveProperty('success', false);
    });
  });
  
  describe('POST /api/auth/login', () => {
    it('should login user and return a token', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'password123'
        });
      
      expect(res.statusCode).toEqual(200);
      expect(res.body).toHaveProperty('token');
      expect(res.body).toHaveProperty('success', true);
    });
    
    it('should not login with incorrect password', async () => {
      const res = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'wrongpassword'
        });
      
      expect(res.statusCode).toEqual(401);
      expect(res.body).toHaveProperty('success', false);
    });
  });
  
  describe('GET /api/auth/me', () => {
    it('should get current user with auth token', async () => {
      // Login to get token
      const login = await request(app)
        .post('/api/auth/login')
        .send({
          email: 'test@example.com',
          password: 'password123'
        });
      
      const token = login.body.token;
      
      // Get current user
      const res = await request(app)
        .get('/api/auth/me')
        .set('Authorization', `Bearer ${token}`);
      
      expect(res.statusCode).toEqual(200);
      expect(res.body.data).toHaveProperty('email', 'test@example.com');
    });
    
    it('should not get user without auth token', async () => {
      const res = await request(app)
        .get('/api/auth/me');
      
      expect(res.statusCode).toEqual(401);
    });
  });
});
            

Integration Testing Protected Routes


// tests/userRoutes.test.js
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');

describe('User Routes', () => {
  let adminToken, userToken;
  
  beforeAll(async () => {
    await mongoose.connect(process.env.MONGO_URI_TEST);
    
    // Create admin user
    const admin = await User.create({
      username: 'admin',
      email: 'admin@example.com',
      password: 'password123',
      role: 'admin'
    });
    
    // Create normal user
    const user = await User.create({
      username: 'normaluser',
      email: 'user@example.com',
      password: 'password123',
      role: 'user'
    });
    
    // Generate tokens
    adminToken = jwt.sign(
      { id: admin._id, role: 'admin' },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    userToken = jwt.sign(
      { id: user._id, role: 'user' },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
  });
  
  afterAll(async () => {
    await User.deleteMany({});
    await mongoose.connection.close();
  });
  
  describe('GET /api/users', () => {
    it('should allow admin to access all users', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${adminToken}`);
      
      expect(res.statusCode).toEqual(200);
      expect(res.body).toHaveProperty('success', true);
    });
    
    it('should not allow normal user to access all users', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${userToken}`);
      
      expect(res.statusCode).toEqual(403);
    });
  });
  
  describe('GET /api/users/profile', () => {
    it('should allow authenticated user to access their profile', async () => {
      const res = await request(app)
        .get('/api/users/profile')
        .set('Authorization', `Bearer ${userToken}`);
      
      expect(res.statusCode).toEqual(200);
      expect(res.body).toHaveProperty('success', true);
      expect(res.body).toHaveProperty('user');
    });
    
    it('should not allow unauthenticated access', async () => {
      const res = await request(app)
        .get('/api/users/profile');
      
      expect(res.statusCode).toEqual(401);
    });
  });
});
            

Performance Testing

For production applications, it's important to test the performance impact of your JWT implementation, especially if using blacklisting:


// Load testing with Artillery
// config.yml
config:
  target: "http://localhost:3000"
  phases:
    - duration: 60
      arrivalRate: 20
  defaults:
    headers:
      Content-Type: "application/json"
scenarios:
  - name: "Login and access protected route"
    flow:
      - post:
          url: "/api/auth/login"
          json:
            email: "{{ $loopElement.email }}"
            password: "{{ $loopElement.password }}"
          capture:
            - json: "$.token"
              as: "authToken"
      - get:
          url: "/api/users/profile"
          headers:
            Authorization: "Bearer {{ authToken }}"
    config:
      payload:
        - path: "users.csv"
          fields:
            - "email"
            - "password"
          skipHeader: true
            

Best Practices and Common Pitfalls

JWT Best Practices Summary

Common Pitfalls to Avoid

Real-World Case Studies

Case Study 1: E-commerce Platform

An e-commerce platform with varying permission levels needs a robust authentication system:

graph TD A[Frontend SPA] -->|API Requests| B[API Gateway] B -->|Authentication| C[Auth Service] B -->|Product Data| D[Product Service] B -->|Order Management| E[Order Service] B -->|User Management| F[User Service] C -->|Verify Token| G[JWT/User Store] C -->|Log Events| H[Security Logs]

Implementation Approach:

Case Study 2: Mobile Banking App

A mobile banking application with high-security requirements:

graph TD A[Mobile App] -->|API Requests| B[API Gateway] B -->|Strong Authentication| C[Auth Service] C -->|Verify Credentials| D[Identity Provider] C -->|MFA Verification| E[MFA Service] C -->|Device Verification| F[Device Registry] G[Token Service] -->|Issue JWT| C G -->|Maintain Blacklist| H[Token Blacklist]

Implementation Approach:

Practical Activities

Activity 1: Basic JWT Implementation

Build a simple Express API with JWT authentication:

  1. Create an Express server with MongoDB connection
  2. Implement user registration and login with JWT token generation
  3. Create a middleware to protect routes
  4. Add at least three protected endpoints
  5. Test the implementation using Postman or curl

Activity 2: Advanced JWT with Refresh Tokens

Extend your implementation with refresh tokens:

  1. Modify the login route to provide both access and refresh tokens
  2. Create a token refresh endpoint
  3. Implement server-side refresh token storage
  4. Add logout functionality to invalidate tokens
  5. Handle token expiration on the client side

Activity 3: Role-Based Authorization

Add role-based access control to your JWT system:

  1. Add role field to the user model
  2. Include role information in JWT payload
  3. Create middleware to check for specific roles
  4. Implement different access levels (public, user, admin)
  5. Test access control with different user roles

Activity 4: Security Testing

Conduct security tests on your JWT implementation:

  1. Test invalid tokens and signature tampering
  2. Test token expiration handling
  3. Try to access protected routes with incorrect permissions
  4. Implement rate limiting for login attempts
  5. Document security vulnerabilities and fixes

Additional Resources

Libraries and Tools

Documentation and Specifications

Articles and Tutorials