DEV Community

Cover image for Ultimate JWT Authentication In Node(Express.js) With Examples
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Edited on • Originally published at practicaldev.online

Ultimate JWT Authentication In Node(Express.js) With Examples

JSON Web Tokens (JWT) are a popular method for implementing authentication in REST APIs. In this guide, we'll implement a complete JWT authentication system using Express.js, including user registration, login, password hashing with bcrypt, and protected routes.

Authentication is one of those topics that seems simple until you actually have to implement it securely. I've seen too many applications with authentication vulnerabilities—from storing plain text passwords to improperly validating tokens. When I first built a JWT authentication system, I made plenty of mistakes, but those mistakes taught me what actually matters in production.

JSON Web Tokens have become the standard for stateless authentication in REST APIs, and for good reason. They're compact, can be verified without database lookups, and work perfectly with microservices architectures. But implementing them correctly requires understanding not just how to generate tokens, but also how to secure them, handle expiration, and protect your routes properly.

What is JWT Authentication?

JWT (JSON Web Token) is a compact, URL-safe token format used for securely transmitting information between parties. In authentication:

  • Stateless - No need to store sessions on the server
  • Scalable - Works across microservices
  • Self-contained - Token includes user information
  • Secure - Signed with a secret key
  • Expirable - Tokens can have expiration times

Installation

Before we start coding, let's get our dependencies installed:

npm install jsonwebtoken bcrypt express
npm install --save-dev @types/jsonwebtoken @types/bcrypt
Enter fullscreen mode Exit fullscreen mode

User Registration with Password Hashing

Here's how to implement user registration with bcrypt password hashing:

const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const { User } = require("../models/index");

class AuthController {
  async register(req, res) {
    try {
      const { name, email, password, role = "staff" } = req.body;

      // Validate input
      if (!name || !email || !password) {
        return res.status(400).json({
          success: false,
          message: "Name, email, and password are required",
        });
      }

      // Check if user already exists
      const existingUser = await User.findOne({ where: { email } });
      if (existingUser) {
        return res.status(409).json({
          success: false,
          message: "User with this email already exists",
        });
      }

      // Hash password with bcrypt (10 rounds)
      const hashedPassword = await bcrypt.hash(password, 10);

      // Create user
      const user = await User.create({
        name,
        email,
        password: hashedPassword,
        role,
        status: "active",
      });

      // Generate JWT token
      const token = jwt.sign(
        { id: user.id, email: user.email, role: user.role },
        process.env.JWT_SECRET || "your-secret-key-change-this",
        { expiresIn: "7d" }
      );

      return res.status(201).json({
        success: true,
        message: "User registered successfully",
        data: {
          id: user.id,
          name: user.name,
          email: user.email,
          role: user.role,
          token,
        },
      });
    } catch (error) {
      console.error("Error registering user:", error);
      return res.status(500).json({
        success: false,
        message: "Error registering user",
        error: error.message,
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

User Login

Login implementation with password verification:

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

    // Validate input
    if (!email || !password) {
      return res.status(400).json({
        success: false,
        message: "Email and password are required",
      });
    }

    // Find user
    const user = await User.findOne({ 
      where: { 
        email: email.toLowerCase().trim() 
      } 
    });

    // Always return the same error message to prevent user enumeration
    if (!user) {
      return res.status(401).json({
        success: false,
        message: "Invalid email or password",
      });
    }

    // Verify password
    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      return res.status(401).json({
        success: false,
        message: "Invalid email or password",
      });
    }

    // Generate JWT token
    const token = jwt.sign(
      { id: user.id, email: user.email, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: "7d" }
    );

    return res.json({
      success: true,
      message: "Login successful",
      data: {
        id: user.id,
        name: user.name,
        email: user.email,
        role: user.role,
        token,
      },
    });
  } catch (error) {
    console.error("Error logging in:", error);
    return res.status(500).json({
      success: false,
      message: "Error logging in",
      error: error.message,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Protected Route Middleware

Create middleware to protect routes:

const jwt = require("jsonwebtoken");

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN

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

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({
        success: false,
        message: "Invalid or expired token",
      });
    }

    req.user = user;
    next();
  });
};

module.exports = { authenticateToken };
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Always hash passwords with bcrypt
  2. Use strong JWT secrets
  3. Set appropriate token expiration
  4. Validate tokens on protected routes
  5. Handle errors consistently
  6. Prevent user enumeration

📖 Read the Complete Guide

This is just a brief overview! The complete guide on my blog includes:

  • Token Refresh - Implementing refresh tokens
  • Role-Based Access - Protecting routes by role
  • Password Reset - Secure password reset flow
  • Security Best Practices - Production security patterns
  • Error Handling - Comprehensive error management
  • Real-world examples from production applications

👉 Read the full article with all code examples here


What's your experience with JWT authentication? Share your tips in the comments! 🚀

For more backend guides, check out my blog covering Express.js, Prisma ORM, Better Auth, and more.

Top comments (0)