How to Secure Your Node.js API with JWT Authentication

NodeJS|SEPTEMBER 15, 2025|0 VIEWS
Learn how to implement robust JWT authentication in your Node.js API with proper security practices, token refresh mechanisms, and middleware protection

Introduction

Security is paramount in modern web applications, and authentication is the first line of defense. JSON Web Tokens (JWT) have become the gold standard for securing APIs due to their stateless nature, scalability, and ease of implementation.

In this comprehensive guide, we'll build a complete JWT authentication system for a Node.js API that includes:

  • User registration and login
  • JWT token generation and validation
  • Secure middleware implementation
  • Token refresh mechanisms
  • Password hashing and security best practices
  • Error handling and security considerations

By the end of this tutorial, you'll have a production-ready authentication system that secures your Node.js API endpoints effectively.

Understanding JWT Authentication

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

A JWT consists of three parts separated by dots:

  • Header: Contains the token type and signing algorithm
  • Payload: Contains the claims (user data)
  • Signature: Used to verify the token's authenticity
// JWT Structure
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c;

Why Use JWT for API Authentication?

Advantages:

  • Stateless: No need to store sessions server-side
  • Scalable: Works perfectly with microservices
  • Cross-domain: Can be used across different domains
  • Mobile-friendly: Perfect for mobile applications
  • Performance: No database lookups for each request

Use Cases:

  • RESTful APIs
  • Single Page Applications (SPAs)
  • Mobile applications
  • Microservices architecture

Project Setup

Let's start by setting up our Node.js project with the necessary dependencies.

Initialize the Project

mkdir jwt-auth-api
cd jwt-auth-api
npm init -y

Install Dependencies

# Production dependencies
npm install express mongoose bcryptjs jsonwebtoken dotenv cors helmet express-rate-limit
npm install express-validator

# Development dependencies
npm install -D nodemon @types/node

Dependencies explained:

  • express: Web framework
  • mongoose: MongoDB ODM
  • bcryptjs: Password hashing
  • jsonwebtoken: JWT implementation
  • dotenv: Environment variables
  • cors: Cross-origin resource sharing
  • helmet: Security headers
  • express-rate-limit: Rate limiting
  • express-validator: Input validation

Project Structure

jwt-auth-api/
├── src/
│   ├── controllers/
│   │   ├── authController.js
│   │   └── userController.js
│   ├── middleware/
│   │   ├── auth.js
│   │   ├── rateLimiter.js
│   │   └── validation.js
│   ├── models/
│   │   └── User.js
│   ├── routes/
│   │   ├── auth.js
│   │   └── users.js
│   ├── utils/
│   │   └── tokenUtils.js
│   └── app.js
├── .env
├── .gitignore
├── package.json
└── server.js

Database Setup

User Model

First, let's create our User model with Mongoose:

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

const userSchema = new mongoose.Schema(
  {
    username: {
      type: String,
      required: [true, 'Username is required'],
      unique: true,
      trim: true,
      minlength: [3, 'Username must be at least 3 characters long'],
      maxlength: [30, 'Username cannot exceed 30 characters'],
    },
    email: {
      type: String,
      required: [true, 'Email is required'],
      unique: true,
      trim: true,
      lowercase: true,
      match: [
        /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
        'Please enter a valid email',
      ],
    },
    password: {
      type: String,
      required: [true, 'Password is required'],
      minlength: [6, 'Password must be at least 6 characters long'],
      select: false, // Don't include password in queries by default
    },
    role: {
      type: String,
      enum: ['user', 'admin'],
      default: 'user',
    },
    isActive: {
      type: Boolean,
      default: true,
    },
    refreshTokens: [
      {
        token: String,
        createdAt: {
          type: Date,
          default: Date.now,
          expires: 604800, // 7 days
        },
      },
    ],
  },
  {
    timestamps: true,
  }
);

// Hash password before saving
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();

  try {
    const salt = await bcrypt.genSalt(12);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

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

// Remove password from JSON output
userSchema.methods.toJSON = function () {
  const user = this.toObject();
  delete user.password;
  delete user.refreshTokens;
  return user;
};

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

Token Utilities

Create utility functions for JWT operations:

// src/utils/tokenUtils.js
const jwt = require('jsonwebtoken');

class TokenUtils {
  // Generate access token (short-lived)
  static generateAccessToken(payload) {
    return jwt.sign(payload, process.env.JWT_ACCESS_SECRET, {
      expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
      issuer: 'your-app-name',
      audience: 'your-app-users',
    });
  }

  // Generate refresh token (long-lived)
  static generateRefreshToken(payload) {
    return jwt.sign(payload, process.env.JWT_REFRESH_SECRET, {
      expiresIn: process.env.JWT_REFRESH_EXPIRE || '7d',
      issuer: 'your-app-name',
      audience: 'your-app-users',
    });
  }

  // Verify access token
  static verifyAccessToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_ACCESS_SECRET);
    } catch (error) {
      throw new Error('Invalid access token');
    }
  }

  // Verify refresh token
  static verifyRefreshToken(token) {
    try {
      return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
    } catch (error) {
      throw new Error('Invalid refresh token');
    }
  }

  // Extract token from Authorization header
  static extractTokenFromHeader(authHeader) {
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new Error('No token provided');
    }
    return authHeader.substring(7);
  }

  // Generate token pair
  static generateTokenPair(payload) {
    const accessToken = this.generateAccessToken(payload);
    const refreshToken = this.generateRefreshToken(payload);

    return {
      accessToken,
      refreshToken,
      expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
    };
  }
}

module.exports = TokenUtils;

Authentication Middleware

Create middleware to protect routes:

// src/middleware/auth.js
const TokenUtils = require('../utils/tokenUtils');
const User = require('../models/User');

// Middleware to verify JWT token
const authenticateToken = async (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
      return res.status(401).json({
        success: false,
        message: 'Access token required',
      });
    }

    const token = TokenUtils.extractTokenFromHeader(authHeader);
    const decoded = TokenUtils.verifyAccessToken(token);

    // Check if user still exists and is active
    const user = await User.findById(decoded.userId);
    if (!user || !user.isActive) {
      return res.status(401).json({
        success: false,
        message: 'User not found or inactive',
      });
    }

    // Add user to request object
    req.user = {
      userId: user._id,
      username: user.username,
      email: user.email,
      role: user.role,
    };

    next();
  } catch (error) {
    console.error('Authentication error:', error.message);

    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({
        success: false,
        message: 'Access token expired',
        code: 'TOKEN_EXPIRED',
      });
    }

    return res.status(403).json({
      success: false,
      message: 'Invalid access token',
    });
  }
};

// Middleware to check user roles
const authorizeRoles = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({
        success: false,
        message: 'Authentication required',
      });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        success: false,
        message: 'Insufficient permissions',
      });
    }

    next();
  };
};

// Optional authentication (doesn't fail if no token)
const optionalAuth = async (req, res, next) => {
  try {
    const authHeader = req.headers.authorization;

    if (authHeader) {
      const token = TokenUtils.extractTokenFromHeader(authHeader);
      const decoded = TokenUtils.verifyAccessToken(token);

      const user = await User.findById(decoded.userId);
      if (user && user.isActive) {
        req.user = {
          userId: user._id,
          username: user.username,
          email: user.email,
          role: user.role,
        };
      }
    }

    next();
  } catch (error) {
    // Continue without authentication
    next();
  }
};

module.exports = {
  authenticateToken,
  authorizeRoles,
  optionalAuth,
};

Input Validation Middleware

// src/middleware/validation.js
const { body, validationResult } = require('express-validator');

// Validation rules for registration
const validateRegistration = [
  body('username')
    .trim()
    .isLength({ min: 3, max: 30 })
    .withMessage('Username must be between 3 and 30 characters')
    .matches(/^[a-zA-Z0-9_]+$/)
    .withMessage('Username can only contain letters, numbers, and underscores'),

  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Please provide a valid email address'),

  body('password')
    .isLength({ min: 6 })
    .withMessage('Password must be at least 6 characters long')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage(
      'Password must contain at least one uppercase letter, one lowercase letter, and one number'
    ),
];

// Validation rules for login
const validateLogin = [
  body('email')
    .isEmail()
    .normalizeEmail()
    .withMessage('Please provide a valid email address'),

  body('password').notEmpty().withMessage('Password is required'),
];

// Validation rules for password change
const validatePasswordChange = [
  body('currentPassword')
    .notEmpty()
    .withMessage('Current password is required'),

  body('newPassword')
    .isLength({ min: 6 })
    .withMessage('New password must be at least 6 characters long')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage(
      'New password must contain at least one uppercase letter, one lowercase letter, and one number'
    ),
];

// Handle validation errors
const handleValidationErrors = (req, res, next) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    const extractedErrors = errors.array().map((error) => ({
      field: error.param,
      message: error.msg,
    }));

    return res.status(400).json({
      success: false,
      message: 'Validation failed',
      errors: extractedErrors,
    });
  }

  next();
};

module.exports = {
  validateRegistration,
  validateLogin,
  validatePasswordChange,
  handleValidationErrors,
};

Rate Limiting Middleware

// src/middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');

// General API rate limiter
const createRateLimiter = (windowMs, max, message) => {
  return rateLimit({
    windowMs,
    max,
    message: {
      success: false,
      message,
      retryAfter: Math.ceil(windowMs / 1000),
    },
    standardHeaders: true,
    legacyHeaders: false,
    // Custom key generator (can be used for user-specific limits)
    keyGenerator: (req) => {
      return req.user?.userId || req.ip;
    },
  });
};

// Strict rate limiter for auth endpoints
const authLimiter = createRateLimiter(
  15 * 60 * 1000, // 15 minutes
  5, // 5 attempts
  'Too many authentication attempts, please try again later'
);

// General API rate limiter
const apiLimiter = createRateLimiter(
  15 * 60 * 1000, // 15 minutes
  100, // 100 requests
  'Too many API requests, please try again later'
);

// Password reset limiter
const passwordResetLimiter = createRateLimiter(
  60 * 60 * 1000, // 1 hour
  3, // 3 attempts
  'Too many password reset attempts, please try again later'
);

module.exports = {
  authLimiter,
  apiLimiter,
  passwordResetLimiter,
};

Authentication Controller

Now let's implement the authentication logic:

// src/controllers/authController.js
const User = require('../models/User');
const TokenUtils = require('../utils/tokenUtils');

class AuthController {
  // User registration
  static async register(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(400).json({
          success: false,
          message:
            existingUser.email === email
              ? 'Email already registered'
              : 'Username already taken',
        });
      }

      // Create new user
      const user = new User({
        username,
        email,
        password,
      });

      await user.save();

      // Generate tokens
      const tokenPayload = {
        userId: user._id,
        username: user.username,
        email: user.email,
        role: user.role,
      };

      const tokens = TokenUtils.generateTokenPair(tokenPayload);

      // Store refresh token
      user.refreshTokens.push({
        token: tokens.refreshToken,
      });
      await user.save();

      res.status(201).json({
        success: true,
        message: 'User registered successfully',
        data: {
          user: {
            id: user._id,
            username: user.username,
            email: user.email,
            role: user.role,
          },
          tokens,
        },
      });
    } catch (error) {
      console.error('Registration error:', error);

      if (error.code === 11000) {
        return res.status(400).json({
          success: false,
          message: 'Email or username already exists',
        });
      }

      res.status(500).json({
        success: false,
        message: 'Registration failed',
        error:
          process.env.NODE_ENV === 'development' ? error.message : undefined,
      });
    }
  }

  // User login
  static async login(req, res) {
    try {
      const { email, password } = req.body;

      // Find user and include password field
      const user = await User.findOne({ email }).select('+password');

      if (!user) {
        return res.status(401).json({
          success: false,
          message: 'Invalid credentials',
        });
      }

      if (!user.isActive) {
        return res.status(401).json({
          success: false,
          message: 'Account is deactivated',
        });
      }

      // Check password
      const isPasswordValid = await user.comparePassword(password);

      if (!isPasswordValid) {
        return res.status(401).json({
          success: false,
          message: 'Invalid credentials',
        });
      }

      // Generate tokens
      const tokenPayload = {
        userId: user._id,
        username: user.username,
        email: user.email,
        role: user.role,
      };

      const tokens = TokenUtils.generateTokenPair(tokenPayload);

      // Store refresh token (limit to 5 active tokens)
      user.refreshTokens.push({
        token: tokens.refreshToken,
      });

      // Keep only the 5 most recent refresh tokens
      if (user.refreshTokens.length > 5) {
        user.refreshTokens = user.refreshTokens.slice(-5);
      }

      await user.save();

      res.json({
        success: true,
        message: 'Login successful',
        data: {
          user: {
            id: user._id,
            username: user.username,
            email: user.email,
            role: user.role,
          },
          tokens,
        },
      });
    } catch (error) {
      console.error('Login error:', error);
      res.status(500).json({
        success: false,
        message: 'Login failed',
        error:
          process.env.NODE_ENV === 'development' ? error.message : undefined,
      });
    }
  }

  // Refresh access token
  static async refreshToken(req, res) {
    try {
      const { refreshToken } = req.body;

      if (!refreshToken) {
        return res.status(401).json({
          success: false,
          message: 'Refresh token required',
        });
      }

      // Verify refresh token
      const decoded = TokenUtils.verifyRefreshToken(refreshToken);

      // Find user and check if refresh token exists
      const user = await User.findById(decoded.userId);

      if (!user || !user.isActive) {
        return res.status(401).json({
          success: false,
          message: 'Invalid refresh token',
        });
      }

      // Check if refresh token exists in user's tokens
      const tokenExists = user.refreshTokens.some(
        (tokenObj) => tokenObj.token === refreshToken
      );

      if (!tokenExists) {
        return res.status(401).json({
          success: false,
          message: 'Invalid refresh token',
        });
      }

      // Generate new access token
      const tokenPayload = {
        userId: user._id,
        username: user.username,
        email: user.email,
        role: user.role,
      };

      const accessToken = TokenUtils.generateAccessToken(tokenPayload);

      res.json({
        success: true,
        message: 'Token refreshed successfully',
        data: {
          accessToken,
          expiresIn: process.env.JWT_ACCESS_EXPIRE || '15m',
        },
      });
    } catch (error) {
      console.error('Token refresh error:', error);
      res.status(401).json({
        success: false,
        message: 'Invalid refresh token',
      });
    }
  }

  // Logout (invalidate refresh token)
  static async logout(req, res) {
    try {
      const { refreshToken } = req.body;
      const userId = req.user?.userId;

      if (userId && refreshToken) {
        // Remove the specific refresh token
        await User.updateOne(
          { _id: userId },
          { $pull: { refreshTokens: { token: refreshToken } } }
        );
      }

      res.json({
        success: true,
        message: 'Logged out successfully',
      });
    } catch (error) {
      console.error('Logout error:', error);
      res.status(500).json({
        success: false,
        message: 'Logout failed',
      });
    }
  }

  // Logout from all devices
  static async logoutAll(req, res) {
    try {
      const userId = req.user.userId;

      // Remove all refresh tokens
      await User.updateOne({ _id: userId }, { $set: { refreshTokens: [] } });

      res.json({
        success: true,
        message: 'Logged out from all devices successfully',
      });
    } catch (error) {
      console.error('Logout all error:', error);
      res.status(500).json({
        success: false,
        message: 'Logout failed',
      });
    }
  }

  // Get current user profile
  static async getProfile(req, res) {
    try {
      const user = await User.findById(req.user.userId);

      if (!user) {
        return res.status(404).json({
          success: false,
          message: 'User not found',
        });
      }

      res.json({
        success: true,
        data: {
          user: {
            id: user._id,
            username: user.username,
            email: user.email,
            role: user.role,
            createdAt: user.createdAt,
            updatedAt: user.updatedAt,
          },
        },
      });
    } catch (error) {
      console.error('Get profile error:', error);
      res.status(500).json({
        success: false,
        message: 'Failed to fetch profile',
      });
    }
  }

  // Change password
  static async changePassword(req, res) {
    try {
      const { currentPassword, newPassword } = req.body;
      const userId = req.user.userId;

      // Find user with password
      const user = await User.findById(userId).select('+password');

      if (!user) {
        return res.status(404).json({
          success: false,
          message: 'User not found',
        });
      }

      // Verify current password
      const isCurrentPasswordValid = await user.comparePassword(
        currentPassword
      );

      if (!isCurrentPasswordValid) {
        return res.status(400).json({
          success: false,
          message: 'Current password is incorrect',
        });
      }

      // Update password
      user.password = newPassword;
      await user.save();

      // Invalidate all refresh tokens (force re-login)
      user.refreshTokens = [];
      await user.save();

      res.json({
        success: true,
        message: 'Password changed successfully. Please login again.',
      });
    } catch (error) {
      console.error('Change password error:', error);
      res.status(500).json({
        success: false,
        message: 'Failed to change password',
      });
    }
  }
}

module.exports = AuthController;

Routes Setup

Create the authentication routes:

// src/routes/auth.js
const express = require('express');
const AuthController = require('../controllers/authController');
const { authenticateToken } = require('../middleware/auth');
const { authLimiter } = require('../middleware/rateLimiter');
const {
  validateRegistration,
  validateLogin,
  validatePasswordChange,
  handleValidationErrors,
} = require('../middleware/validation');

const router = express.Router();

// Public routes (with rate limiting)
router.post(
  '/register',
  authLimiter,
  validateRegistration,
  handleValidationErrors,
  AuthController.register
);

router.post(
  '/login',
  authLimiter,
  validateLogin,
  handleValidationErrors,
  AuthController.login
);

router.post('/refresh-token', authLimiter, AuthController.refreshToken);

// Protected routes
router.use(authenticateToken);

router.get('/profile', AuthController.getProfile);

router.post('/logout', AuthController.logout);

router.post('/logout-all', AuthController.logoutAll);

router.post(
  '/change-password',
  validatePasswordChange,
  handleValidationErrors,
  AuthController.changePassword
);

module.exports = router;

Create protected user routes:

// src/routes/users.js
const express = require('express');
const User = require('../models/User');
const { authenticateToken, authorizeRoles } = require('../middleware/auth');

const router = express.Router();

// All routes require authentication
router.use(authenticateToken);

// Get all users (admin only)
router.get('/', authorizeRoles('admin'), async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;

    const users = await User.find({ isActive: true })
      .select('-refreshTokens')
      .skip(skip)
      .limit(limit)
      .sort({ createdAt: -1 });

    const total = await User.countDocuments({ isActive: true });

    res.json({
      success: true,
      data: {
        users,
        pagination: {
          page,
          limit,
          total,
          pages: Math.ceil(total / limit),
        },
      },
    });
  } catch (error) {
    console.error('Get users error:', error);
    res.status(500).json({
      success: false,
      message: 'Failed to fetch users',
    });
  }
});

// Get user by ID (admin only)
router.get('/:id', authorizeRoles('admin'), async (req, res) => {
  try {
    const user = await User.findById(req.params.id).select('-refreshTokens');

    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found',
      });
    }

    res.json({
      success: true,
      data: { user },
    });
  } catch (error) {
    console.error('Get user error:', error);
    res.status(500).json({
      success: false,
      message: 'Failed to fetch user',
    });
  }
});

// Deactivate user (admin only)
router.patch('/:id/deactivate', authorizeRoles('admin'), async (req, res) => {
  try {
    const user = await User.findById(req.params.id);

    if (!user) {
      return res.status(404).json({
        success: false,
        message: 'User not found',
      });
    }

    user.isActive = false;
    user.refreshTokens = []; // Invalidate all tokens
    await user.save();

    res.json({
      success: true,
      message: 'User deactivated successfully',
    });
  } catch (error) {
    console.error('Deactivate user error:', error);
    res.status(500).json({
      success: false,
      message: 'Failed to deactivate user',
    });
  }
});

module.exports = router;

Application Setup

Create the main application file:

// src/app.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();

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

// Import middleware
const { apiLimiter } = require('./middleware/rateLimiter');

const app = express();

// Security middleware
app.use(helmet());
app.use(
  cors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || [
      'http://localhost:3000',
    ],
    credentials: true,
  })
);

// Rate limiting
app.use(apiLimiter);

// Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Database connection
mongoose
  .connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/jwt-auth', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error('MongoDB connection error:', err));

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

// Health check endpoint
app.get('/api/health', (req, res) => {
  res.json({
    success: true,
    message: 'API is running',
    timestamp: new Date().toISOString(),
  });
});

// 404 handler
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    message: 'Route not found',
  });
});

// Global error handler
app.use((error, req, res, next) => {
  console.error('Unhandled error:', error);

  res.status(error.status || 500).json({
    success: false,
    message: error.message || 'Internal server error',
    ...(process.env.NODE_ENV === 'development' && { stack: error.stack }),
  });
});

module.exports = app;

Server entry point:

// server.js
const app = require('./src/app');

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

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
});

Environment Configuration

Create your environment configuration:

# .env
# Server Configuration
PORT=3000
NODE_ENV=development

# Database
MONGODB_URI=mongodb://localhost:27017/jwt-auth

# JWT Secrets (use strong, random strings in production)
JWT_ACCESS_SECRET=your-super-secret-access-key-change-this-in-production
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production

# JWT Expiration
JWT_ACCESS_EXPIRE=15m
JWT_REFRESH_EXPIRE=7d

# CORS
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001

# Rate Limiting (requests per window)
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

Testing the API

Package.json Scripts

Add these scripts to your package.json:

{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

Testing with cURL

Start your server:

npm run dev

Register a new user:

curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "johndoe",
    "email": "john@example.com",
    "password": "Password123"
  }'

Login:

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "password": "Password123"
  }'

Access protected route:

curl -X GET http://localhost:3000/api/auth/profile \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Refresh token:

curl -X POST http://localhost:3000/api/auth/refresh-token \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "YOUR_REFRESH_TOKEN"
  }'

Frontend Integration

React Example

Here's how to integrate with a React frontend:

// authService.js
class AuthService {
  constructor() {
    this.baseURL = 'http://localhost:3000/api';
    this.accessToken = localStorage.getItem('accessToken');
    this.refreshToken = localStorage.getItem('refreshToken');
  }

  // Set authentication headers
  getAuthHeaders() {
    return {
      'Content-Type': 'application/json',
      ...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),
    };
  }

  // Register user
  async register(userData) {
    const response = await fetch(`${this.baseURL}/auth/register`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData),
    });

    const data = await response.json();

    if (data.success) {
      this.setTokens(data.data.tokens);
    }

    return data;
  }

  // Login user
  async login(credentials) {
    const response = await fetch(`${this.baseURL}/auth/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials),
    });

    const data = await response.json();

    if (data.success) {
      this.setTokens(data.data.tokens);
    }

    return data;
  }

  // Refresh access token
  async refreshAccessToken() {
    if (!this.refreshToken) {
      throw new Error('No refresh token available');
    }

    const response = await fetch(`${this.baseURL}/auth/refresh-token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken: this.refreshToken }),
    });

    const data = await response.json();

    if (data.success) {
      this.accessToken = data.data.accessToken;
      localStorage.setItem('accessToken', this.accessToken);
    } else {
      this.logout();
      throw new Error('Token refresh failed');
    }

    return data;
  }

  // Make authenticated API call
  async authenticatedFetch(url, options = {}) {
    let response = await fetch(`${this.baseURL}${url}`, {
      ...options,
      headers: {
        ...this.getAuthHeaders(),
        ...options.headers,
      },
    });

    // If access token expired, try to refresh
    if (response.status === 401) {
      const errorData = await response.json();

      if (errorData.code === 'TOKEN_EXPIRED') {
        try {
          await this.refreshAccessToken();

          // Retry the original request
          response = await fetch(`${this.baseURL}${url}`, {
            ...options,
            headers: {
              ...this.getAuthHeaders(),
              ...options.headers,
            },
          });
        } catch (error) {
          this.logout();
          throw new Error('Authentication failed');
        }
      }
    }

    return response;
  }

  // Logout
  async logout() {
    if (this.refreshToken) {
      try {
        await fetch(`${this.baseURL}/auth/logout`, {
          method: 'POST',
          headers: this.getAuthHeaders(),
          body: JSON.stringify({ refreshToken: this.refreshToken }),
        });
      } catch (error) {
        console.error('Logout request failed:', error);
      }
    }

    this.clearTokens();
  }

  // Set tokens
  setTokens(tokens) {
    this.accessToken = tokens.accessToken;
    this.refreshToken = tokens.refreshToken;

    localStorage.setItem('accessToken', tokens.accessToken);
    localStorage.setItem('refreshToken', tokens.refreshToken);
  }

  // Clear tokens
  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;

    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  }

  // Check if user is authenticated
  isAuthenticated() {
    return !!this.accessToken;
  }
}

export default new AuthService();

React Hook Example

// useAuth.js
import { useState, useEffect, useContext, createContext } from 'react';
import authService from './authService';

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const initAuth = async () => {
      if (authService.isAuthenticated()) {
        try {
          const response = await authService.authenticatedFetch(
            '/auth/profile'
          );
          const data = await response.json();

          if (data.success) {
            setUser(data.data.user);
          } else {
            authService.logout();
          }
        } catch (error) {
          console.error('Auth initialization failed:', error);
          authService.logout();
        }
      }
      setLoading(false);
    };

    initAuth();
  }, []);

  const login = async (credentials) => {
    const result = await authService.login(credentials);
    if (result.success) {
      setUser(result.data.user);
    }
    return result;
  };

  const register = async (userData) => {
    const result = await authService.register(userData);
    if (result.success) {
      setUser(result.data.user);
    }
    return result;
  };

  const logout = async () => {
    await authService.logout();
    setUser(null);
  };

  const value = {
    user,
    login,
    register,
    logout,
    loading,
    isAuthenticated: !!user,
  };

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

Security Best Practices

1. Token Security

Use Strong Secrets:

# Generate strong secrets
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

Token Expiration:

  • Access tokens: Short-lived (15-30 minutes)
  • Refresh tokens: Longer-lived (7 days to 30 days)

2. Password Security

Password Requirements:

  • Minimum 8 characters
  • Mix of uppercase, lowercase, numbers, and symbols
  • Not commonly used passwords

Hashing:

// Use high salt rounds (12+ for production)
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);

3. Rate Limiting

Implementation Levels:

  • Global API rate limiting
  • Endpoint-specific limits
  • User-specific limits
  • IP-based limits

4. Input Validation

Always Validate:

  • Email format
  • Username format
  • Password strength
  • Request body size
  • File uploads (if applicable)

5. HTTPS Only

Production Setup:

// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.header('x-forwarded-proto') !== 'https') {
      return res.redirect(`https://${req.header('host')}${req.url}`);
    }
    next();
  });
}

6. Security Headers

// Enhanced security headers
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        scriptSrc: ["'self'"],
        imgSrc: ["'self'", 'data:', 'https:'],
      },
    },
    hsts: {
      maxAge: 31536000,
      includeSubDomains: true,
      preload: true,
    },
  })
);

7. Token Blacklisting

For enhanced security, implement token blacklisting:

// Token blacklist (use Redis in production)
const tokenBlacklist = new Set();

// Middleware to check blacklisted tokens
const checkBlacklist = (req, res, next) => {
  const token = TokenUtils.extractTokenFromHeader(req.headers.authorization);

  if (tokenBlacklist.has(token)) {
    return res.status(401).json({
      success: false,
      message: 'Token has been revoked',
    });
  }

  next();
};

// Add token to blacklist on logout
const addToBlacklist = (token) => {
  tokenBlacklist.add(token);

  // Clean up expired tokens periodically
  setTimeout(() => {
    tokenBlacklist.delete(token);
  }, 15 * 60 * 1000); // 15 minutes
};

Production Deployment

Environment Variables

# Production .env
NODE_ENV=production
PORT=3000

# Database
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/proddb

# JWT Secrets (use crypto.randomBytes(64).toString('hex'))
JWT_ACCESS_SECRET=your-production-access-secret
JWT_REFRESH_SECRET=your-production-refresh-secret

# Shorter expiration for production
JWT_ACCESS_EXPIRE=15m
JWT_REFRESH_EXPIRE=7d

# Production domains
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

PM2 Configuration

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'jwt-auth-api',
      script: 'server.js',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      error_file: './logs/err.log',
      out_file: './logs/out.log',
      log_file: './logs/combined.log',
      time: true,
    },
  ],
};

Docker Configuration

# Dockerfile
FROM node:16-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodeuser -u 1001

# Change ownership
RUN chown -R nodeuser:nodejs /app
USER nodeuser

EXPOSE 3000

CMD ["node", "server.js"]

Conclusion

You now have a comprehensive JWT authentication system for your Node.js API that includes:

Secure user registration and login
JWT token generation and validation
Refresh token mechanism
Password hashing with bcrypt
Rate limiting and security middleware
Input validation and sanitization
Role-based authorization
Frontend integration examples
Production-ready security practices

Key Takeaways

  1. Security First: Always prioritize security in authentication systems
  2. Token Strategy: Use short-lived access tokens with refresh tokens
  3. Validation: Validate all inputs and implement proper error handling
  4. Rate Limiting: Protect against brute force attacks
  5. Monitoring: Log security events and monitor for suspicious activity

Next Steps

  • Implement password reset functionality
  • Add two-factor authentication (2FA)
  • Implement OAuth integration
  • Add comprehensive logging and monitoring
  • Set up automated security testing
  • Consider implementing WebAuthn for passwordless authentication

This authentication system provides a solid foundation for securing your Node.js APIs while maintaining scalability and user experience. Remember to keep your dependencies updated and regularly review security practices as your application grows.