Implementing JWT Authentication

Building secure authentication systems with JSON Web Tokens

Setting Up a Complete JWT Authentication System

Now that we understand what JWTs are and their benefits, let's build a complete authentication system using JWT in a Node.js application. We'll use Express.js for our server, MongoDB for storage, and implement secure practices.

Real-World Context

Imagine you're building a SaaS platform where users need to authenticate across multiple services:

  • A React-based dashboard
  • A mobile app for on-the-go access
  • Several microservices that need to verify the user

JWT authentication provides a unified solution for all these scenarios without requiring multiple logins or complex session management across services.

Project Setup

Required Dependencies

npm init -y
npm install express mongoose jsonwebtoken bcrypt dotenv cors

Dependency Overview

  • express: Web server framework
  • mongoose: MongoDB ODM for database operations
  • jsonwebtoken: Library for creating and verifying JWTs
  • bcrypt: For securely hashing passwords
  • dotenv: For managing environment variables
  • cors: To handle Cross-Origin Resource Sharing

Project Structure

auth-api/
├── .env                 # Environment variables
├── package.json         # Dependencies
├── server.js            # Entry point
├── config/
│   └── db.js            # Database connection
├── models/
│   ├── User.js          # User model
│   └── Token.js         # Refresh token model
├── controllers/
│   └── authController.js # Authentication logic
├── middleware/
│   └── auth.js          # JWT verification middleware
└── routes/
    ├── authRoutes.js    # Authentication routes
    └── userRoutes.js    # Protected user routes

Setting Up Environment Variables

Create a .env file in your project root to store sensitive information:

# .env
MONGODB_URI=mongodb://localhost:27017/jwt_auth_demo
JWT_SECRET=your_super_secret_key_should_be_long_and_complex
JWT_ACCESS_EXPIRATION=15m
JWT_REFRESH_EXPIRATION=7d
PORT=3000

Security Note

In a production environment:

  • Generate a strong, random JWT secret (at least 32 characters)
  • Never commit .env files to version control
  • Use different secrets for development and production
  • Consider using a secrets management service for production

Database Configuration

Let's set up our MongoDB connection using Mongoose:

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

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log('MongoDB connected successfully');
  } catch (error) {
    console.error('MongoDB connection error:', error.message);
    process.exit(1);
  }
};

module.exports = connectDB;

User Model

Create a user model to store user information and handle password hashing:

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

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true
});

// Pre-save middleware to hash password
userSchema.pre('save', async function(next) {
  // Only hash the password if it has been modified (or is new)
  if (!this.isModified('password')) return next();
  
  try {
    // Generate a salt
    const salt = await bcrypt.genSalt(10);
    
    // Hash the password along with the new salt
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// Method to compare password
userSchema.methods.comparePassword = async function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

const User = mongoose.model('User', userSchema);

module.exports = User;

Security Features

The User model includes several security features:

  • Automatic password hashing using bcrypt
  • Password comparison method for login
  • Validation for username, email, and password
  • Role-based access control (user vs. admin)

Token Model for Refresh Tokens

We'll create a model to store refresh tokens, which allows us to invalidate tokens when needed:

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

const tokenSchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  token: {
    type: String,
    required: true
  },
  expiryDate: {
    type: Date,
    required: true
  },
  createdAt: {
    type: Date,
    default: Date.now,
    expires: '7d' // Token document will be automatically deleted after 7 days
  }
});

const Token = mongoose.model('Token', tokenSchema);

module.exports = Token;

Why Store Refresh Tokens?

Unlike access tokens that are verified solely by their signature, refresh tokens are stored in the database because:

  • They need to be explicitly revoked when a user logs out
  • They can be invalidated if compromised
  • You can limit the number of active sessions per user
  • They enable tracking of device or IP information for security

Authentication Controller

Next, let's implement the authentication logic:

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

// Helper function to generate tokens
const generateTokens = async (userId) => {
  try {
    // Create payload for tokens
    const payload = { sub: userId };
    
    // Create access token that expires in 15 minutes
    const accessToken = jwt.sign(
      payload,
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '15m' }
    );
    
    // Create refresh token that expires in 7 days
    const refreshToken = jwt.sign(
      payload,
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_REFRESH_EXPIRATION || '7d' }
    );
    
    // Calculate expiry date for database
    const refreshExpiry = new Date();
    refreshExpiry.setDate(
      refreshExpiry.getDate() + 
      parseInt(process.env.JWT_REFRESH_EXPIRATION || '7d')
    );
    
    // Save refresh token in database
    await Token.create({
      userId,
      token: refreshToken,
      expiryDate: refreshExpiry
    });
    
    return {
      accessToken,
      refreshToken
    };
  } catch (error) {
    throw error;
  }
};

// Register a new user
exports.register = async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // Check if user already exists
    const existingUser = await User.findOne({
      $or: [{ email }, { username }]
    });
    
    if (existingUser) {
      return res.status(409).json({
        success: false,
        message: 'Username or email already exists'
      });
    }
    
    // Create new user
    const newUser = new User({
      username,
      email,
      password
    });
    
    await newUser.save();
    
    // Generate tokens
    const tokens = await generateTokens(newUser._id);
    
    res.status(201).json({
      success: true,
      message: 'User registered successfully',
      ...tokens
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Registration failed',
      error: error.message
    });
  }
};

// Login user
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Find user by email
    const user = await User.findOne({ email });
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }
    
    // Check password
    const isPasswordValid = await user.comparePassword(password);
    
    if (!isPasswordValid) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }
    
    // Generate tokens
    const tokens = await generateTokens(user._id);
    
    res.status(200).json({
      success: true,
      ...tokens,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        role: user.role
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Login failed',
      error: error.message
    });
  }
};

// Refresh token
exports.refreshToken = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (!refreshToken) {
      return res.status(401).json({
        success: false,
        message: 'Refresh token is required'
      });
    }
    
    // Find token in database
    const storedToken = await Token.findOne({ token: refreshToken });
    
    if (!storedToken) {
      return res.status(403).json({
        success: false,
        message: 'Refresh token not found or expired'
      });
    }
    
    // Verify token
    let payload;
    try {
      payload = jwt.verify(refreshToken, process.env.JWT_SECRET);
    } catch (err) {
      // Remove invalid token from database
      await Token.findByIdAndRemove(storedToken._id);
      
      return res.status(403).json({
        success: false,
        message: 'Invalid refresh token'
      });
    }
    
    // Check if token is expired in database
    if (storedToken.expiryDate < new Date()) {
      // Remove expired token
      await Token.findByIdAndRemove(storedToken._id);
      
      return res.status(403).json({
        success: false,
        message: 'Refresh token expired'
      });
    }
    
    // Create new access token
    const newAccessToken = jwt.sign(
      { sub: payload.sub },
      process.env.JWT_SECRET,
      { expiresIn: process.env.JWT_ACCESS_EXPIRATION || '15m' }
    );
    
    res.status(200).json({
      success: true,
      accessToken: newAccessToken
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Token refresh failed',
      error: error.message
    });
  }
};

// Logout user
exports.logout = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    
    if (!refreshToken) {
      return res.status(400).json({
        success: false,
        message: 'Refresh token is required'
      });
    }
    
    // Remove token from database
    await Token.findOneAndRemove({ token: refreshToken });
    
    res.status(200).json({
      success: true,
      message: 'Logged out successfully'
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Logout failed',
      error: error.message
    });
  }
};

Authentication Middleware

Create middleware to verify JWT access tokens for protected routes:

// middleware/auth.js
const jwt = require('jsonwebtoken');
require('dotenv').config();

exports.verifyToken = (req, res, next) => {
  // Get token from Authorization header
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      success: false,
      message: 'Access token is required'
    });
  }
  
  const token = authHeader.split(' ')[1];
  
  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Add user ID to request
    req.userId = decoded.sub;
    
    next();
  } catch (error) {
    let message = 'Invalid token';
    
    if (error.name === 'TokenExpiredError') {
      message = 'Token expired';
    }
    
    return res.status(401).json({
      success: false,
      message
    });
  }
};

// Role-based access control middleware
exports.checkRole = (role) => {
  return async (req, res, next) => {
    try {
      const User = require('../models/User');
      
      const user = await User.findById(req.userId);
      
      if (!user) {
        return res.status(404).json({
          success: false,
          message: 'User not found'
        });
      }
      
      if (user.role !== role) {
        return res.status(403).json({
          success: false,
          message: 'Access denied'
        });
      }
      
      next();
    } catch (error) {
      res.status(500).json({
        success: false,
        message: 'Authorization failed',
        error: error.message
      });
    }
  };
};

Using the Middleware

The auth middleware can be used in two ways:

  1. Basic authentication: router.get('/profile', verifyToken, userController.getProfile);
  2. Role-based access: router.get('/admin-panel', verifyToken, checkRole('admin'), adminController.getPanel);

Authentication Routes

Set up the routes for authentication operations:

// routes/authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');

// Register new user
router.post('/register', authController.register);

// Login user
router.post('/login', authController.login);

// Refresh access token
router.post('/refresh-token', authController.refreshToken);

// Logout user
router.post('/logout', authController.logout);

module.exports = router;

Protected User Routes

Create protected routes that require authentication:

// routes/userRoutes.js
const express = require('express');
const router = express.Router();
const { verifyToken, checkRole } = require('../middleware/auth');

// Protected route - Get user profile
router.get('/profile', verifyToken, async (req, res) => {
  try {
    const User = require('../models/User');
    const user = await User.findById(req.userId).select('-password');
    
    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found'
      });
    }
    
    res.status(200).json({
      success: true,
      user
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: 'Error fetching profile',
      error: error.message
    });
  }
});

// Admin-only route
router.get('/admin', verifyToken, checkRole('admin'), (req, res) => {
  res.status(200).json({
    success: true,
    message: 'Admin access granted',
    data: {
      sensitiveInfo: 'This data is only accessible to admins'
    }
  });
});

module.exports = router;

Server Setup

Now, let's set up the Express server:

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

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

// Initialize express app
const app = express();

// Connect to database
connectDB();

// Middleware
app.use(cors());
app.use(express.json());

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

// Root route
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to JWT Authentication API' });
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    success: false,
    message: 'Something went wrong',
    error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message
  });
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

JWT Authentication Flow

sequenceDiagram participant Client participant Server participant DB as Database Note over Client,Server: Registration/Login Flow Client->>Server: POST /api/auth/register or /login Server->>DB: Verify credentials DB->>Server: User data Server->>Server: Generate JWT access & refresh tokens Server->>DB: Store refresh token Server->>Client: Return both tokens + user data Note over Client,Server: Using Protected Resources Client->>Server: Request with access token Server->>Server: Verify access token alt Token valid Server->>Client: Protected resource else Token expired Server->>Client: 401 Unauthorized Client->>Server: POST /api/auth/refresh-token Server->>DB: Verify refresh token DB->>Server: Token valid Server->>Server: Generate new access token Server->>Client: New access token Client->>Server: Request with new access token Server->>Client: Protected resource end Note over Client,Server: Logout Flow Client->>Server: POST /api/auth/logout with refresh token Server->>DB: Remove refresh token Server->>Client: Logout successful

Storing JWTs on the Client

When implementing JWT authentication, secure storage on the client side is crucial:

Storage Method Security Accessibility Best For
HttpOnly Cookie High (protected from XSS) Automatic with requests Refresh tokens
localStorage Low (vulnerable to XSS) Easy, persists on refresh Development only
sessionStorage Low (vulnerable to XSS) Cleared on tab close Short lived access
In-memory (React state) High (cleared on refresh) Lost on page refresh Access tokens

Recommended Approach

A common secure pattern is:

  1. Store access token in memory (React state/Redux)
  2. Store refresh token in an HttpOnly cookie
  3. Implement a token refresh mechanism that activates when the access token expires

Client-Side Implementation with React

Here's a basic example of implementing the client side of JWT authentication in React:

// AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';

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

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [currentUser, setCurrentUser] = useState(null);
  const [accessToken, setAccessToken] = useState(null);
  const [loading, setLoading] = useState(true);

  // Initialize auth state from localStorage
  useEffect(() => {
    const user = localStorage.getItem('user');
    const token = localStorage.getItem('accessToken');
    
    if (user && token) {
      setCurrentUser(JSON.parse(user));
      setAccessToken(token);
      
      // Set axios default header
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
    }
    
    setLoading(false);
  }, []);

  // Configure axios interceptor for token refresh
  useEffect(() => {
    const interceptor = axios.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
        
        // If error is not 401 or request already tried, reject
        if (error.response.status !== 401 || originalRequest._retry) {
          return Promise.reject(error);
        }
        
        originalRequest._retry = true;
        
        try {
          // Get refresh token from localStorage
          const refreshToken = localStorage.getItem('refreshToken');
          
          if (!refreshToken) {
            throw new Error('No refresh token');
          }
          
          // Request new access token
          const response = await axios.post(`${API_URL}/auth/refresh-token`, {
            refreshToken
          });
          
          const newAccessToken = response.data.accessToken;
          
          // Update token in state and localStorage
          setAccessToken(newAccessToken);
          localStorage.setItem('accessToken', newAccessToken);
          
          // Update authorization header
          axios.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;
          originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
          
          // Retry original request
          return axios(originalRequest);
        } catch (refreshError) {
          // If refresh fails, log out user
          logout();
          return Promise.reject(refreshError);
        }
      }
    );
    
    return () => {
      axios.interceptors.response.eject(interceptor);
    };
  }, []);

  // Register function
  const register = async (username, email, password) => {
    try {
      const response = await axios.post(`${API_URL}/auth/register`, {
        username,
        email,
        password
      });
      
      const { accessToken, refreshToken, user } = response.data;
      
      // Save to state and localStorage
      setCurrentUser(user);
      setAccessToken(accessToken);
      
      localStorage.setItem('user', JSON.stringify(user));
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      
      // Set default authorization header
      axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
      
      return user;
    } catch (error) {
      throw error.response.data;
    }
  };

  // Login function
  const login = async (email, password) => {
    try {
      const response = await axios.post(`${API_URL}/auth/login`, {
        email,
        password
      });
      
      const { accessToken, refreshToken, user } = response.data;
      
      // Save to state and localStorage
      setCurrentUser(user);
      setAccessToken(accessToken);
      
      localStorage.setItem('user', JSON.stringify(user));
      localStorage.setItem('accessToken', accessToken);
      localStorage.setItem('refreshToken', refreshToken);
      
      // Set default authorization header
      axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
      
      return user;
    } catch (error) {
      throw error.response.data;
    }
  };

  // Logout function
  const logout = async () => {
    try {
      const refreshToken = localStorage.getItem('refreshToken');
      
      if (refreshToken) {
        await axios.post(`${API_URL}/auth/logout`, { refreshToken });
      }
    } catch (error) {
      console.error('Logout error:', error);
    } finally {
      // Clear state and localStorage
      setCurrentUser(null);
      setAccessToken(null);
      
      localStorage.removeItem('user');
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
      
      // Remove authorization header
      delete axios.defaults.headers.common['Authorization'];
    }
  };

  const value = {
    currentUser,
    accessToken,
    loading,
    register,
    login,
    logout
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// Custom hook to use the auth context
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

Using the Auth Context

// App.js
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { AuthProvider, useAuth } from './AuthContext';
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';
import AdminPanel from './pages/AdminPanel';

// Private route component
const PrivateRoute = ({ component: Component, ...rest }) => {
  const { currentUser, loading } = useAuth();
  
  if (loading) {
    return 
Loading...
; } return ( <Route {...rest} render={(props) => currentUser ? ( <Component {...props} /> ) : ( <Redirect to="/login" /> ) } /> ); }; // Admin route with role check const AdminRoute = ({ component: Component, ...rest }) => { const { currentUser, loading } = useAuth(); if (loading) { return
Loading...
; } return ( <Route {...rest} render={(props) => currentUser && currentUser.role === 'admin' ? ( <Component {...props} /> ) : ( <Redirect to="/dashboard" /> ) } /> ); }; const App = () => { return ( <AuthProvider> <Router> <Switch> <Route exact path="/login" component={Login} /> <Route exact path="/register" component={Register} /> <PrivateRoute exact path="/dashboard" component={Dashboard} /> <PrivateRoute exact path="/profile" component={Profile} /> <AdminRoute exact path="/admin" component={AdminPanel} /> <Redirect from="/" to="/dashboard" /> </Switch> </Router> </AuthProvider> ); }; export default App;

Login Component Example

// pages/Login.js
import React, { useState } from 'react';
import { useHistory, Link } from 'react-router-dom';
import { useAuth } from '../AuthContext';

const Login = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login } = useAuth();
  const history = useHistory();
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    
    try {
      setLoading(true);
      await login(email, password);
      history.push('/dashboard');
    } catch (error) {
      setError(error.message || 'Failed to log in');
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="login-container">
      <h2>Login</h2>
      {error && <div className="error">{error}</div>}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        
        <div className="form-group">
          <label>Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Logging in...' : 'Login'}
        </button>
      </form>
      
      <p>
        Don't have an account? <Link to="/register">Register</Link>
      &


export default Login;

Security Best Practices

JWT Security Checklist

  • Use HTTPS: Always transmit JWTs over HTTPS connections
  • Set proper expiration: Short-lived access tokens (15-60 min)
  • Implement refresh tokens: With longer expiration but revocable
  • Store securely: Memory for access tokens, HttpOnly cookies for refresh tokens
  • Keep payload minimal: Don't include sensitive data in tokens
  • Use strong secrets: Long, random strings generated with a CSPRNG
  • Whitelist algorithms: Prevent algorithm switching attacks
  • Validate all tokens: Always verify signature and expiration
  • Include issuer (iss) and audience (aud): For multi-service environments
  • Use CSRF protection: When using cookies for tokens
  • Implement rate limiting: Prevent brute-force attacks

Real-World Security Vulnerabilities to Avoid

Advanced JWT Features

Using Different Algorithms

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

// Read private and public keys
const privateKey = fs.readFileSync('private.key');
const publicKey = fs.readFileSync('public.key');

// Sign with private key
const token = jwt.sign({ sub: 'user123' }, privateKey, {
  algorithm: 'RS256',
  expiresIn: '1h'
});

// Verify with public key
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256'] // Whitelist algorithms
});

Token Blacklisting

// Redis implementation for blacklisting tokens
const redis = require('redis');
const util = require('util');
const client = redis.createClient();

// Promisify Redis commands
const setAsync = util.promisify(client.set).bind(client);
const getAsync = util.promisify(client.get).bind(client);
const existsAsync = util.promisify(client.exists).bind(client);

// Blacklist a token
const blacklistToken = async (token, expiryTime) => {
  // Store token in Redis with expiry time
  await setAsync(`bl_${token}`, 'true', 'EX', expiryTime);
};

// Check if token is blacklisted
const isTokenBlacklisted = async (token) => {
  const result = await existsAsync(`bl_${token}`);
  return result === 1;
};

// Updated verify middleware
const verifyToken = async (req, res, next) => {
  const token = req.headers.authorization.split(' ')[1];
  
  // Check blacklist first
  if (await isTokenBlacklisted(token)) {
    return res.status(401).json({
      success: false,
      message: 'Token has been revoked'
    });
  }
  
  // Continue with verification
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.sub;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: error.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token'
    });
  }
};

Practice Activities

Activity 1: Complete JWT Authentication System

Build the complete authentication system described in this lecture:

  1. Set up the Node.js/Express server
  2. Implement user registration and login
  3. Add refresh token functionality
  4. Create protected routes with role-based access
  5. Test all endpoints with Postman or similar tool

Activity 2: React Frontend Integration

Integrate the JWT authentication with a React frontend:

  1. Create login and registration forms
  2. Implement the AuthContext for state management
  3. Add protected routes with role-based access
  4. Set up automatic token refresh
  5. Create a user profile page that shows user data

Activity 3: Security Enhancement

Enhance the security of your JWT implementation:

  1. Implement token blacklisting for logout
  2. Add rate limiting to prevent brute force attacks
  3. Implement secure cookie storage for refresh tokens
  4. Add CSRF protection
  5. Switch from symmetric (HS256) to asymmetric (RS256) signing

Additional Resources

Summary

Next, we'll cover token refresh strategies and patterns for securely maintaining user sessions.