Page cover
Security isn't a feature - it's a foundation. Implement authentication right from the start, not as an afterthought.

How to Implement JWT Authentication in Node.js

Security Intermediate 50-60 minutes December 5, 2024

Build secure authentication in your Node.js API using JSON Web Tokens (JWT) with refresh token rotation.

Things You'll Need

  • Node.js project with Express
  • MongoDB or another database
  • Basic understanding of authentication concepts
  • Postman or similar API testing tool

Requirements

  • Express server set up
  • Database connection configured
  • User model created

Steps

Follow these 9 steps to complete this guide

1

Install Required Packages

Install jsonwebtoken for creating and verifying JWTs, and bcrypt for password hashing.

bash
npm install jsonwebtoken bcryptjs
npm install -D @types/jsonwebtoken @types/bcryptjs
2

Set Up Environment Variables

Create a .env file with your JWT secrets. Never commit these to version control!

bash
# .env
JWT_SECRET=your_super_secret_key_min_32_characters_long
JWT_REFRESH_SECRET=your_refresh_secret_key_min_32_characters
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

Warnings

  • Use strong, random secrets in production
  • Never commit .env files to Git
  • Rotate secrets regularly in production
3

Create Token Utility Functions

Build helper functions for generating and verifying JWT tokens.

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

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

export const generateAccessToken = (payload: TokenPayload): string => {
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    expiresIn: process.env.JWT_EXPIRES_IN,
  });
};

export const generateRefreshToken = (payload: TokenPayload): string => {
  return jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, {
    expiresIn: process.env.JWT_REFRESH_EXPIRES_IN,
  });
};

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

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

Create Authentication Middleware

Build middleware to protect routes and verify tokens.

typescript
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { verifyAccessToken } from '../utils/jwt';

export interface AuthRequest extends Request {
  user?: {
    userId: string;
    email: string;
  };
}

export const authenticateToken = (
  req: AuthRequest,
  res: Response,
  next: NextFunction
) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

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

  try {
    const payload = verifyAccessToken(token);
    req.user = payload;
    next();
  } catch (error) {
    return res.status(403).json({ message: 'Invalid or expired token' });
  }
};

Tips

  • Return 401 for missing tokens (Unauthorized)
  • Return 403 for invalid tokens (Forbidden)
5

Implement Registration Endpoint

Create an endpoint for user registration with password hashing.

typescript
// routes/auth.ts
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { User } from '../models/User';
import { generateAccessToken, generateRefreshToken } from '../utils/jwt';

const router = Router();

router.post('/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;

    // Check if user exists
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ message: 'User already exists' });
    }

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

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

    // Generate tokens
    const accessToken = generateAccessToken({
      userId: user._id.toString(),
      email: user.email,
    });

    const refreshToken = generateRefreshToken({
      userId: user._id.toString(),
      email: user.email,
    });

    res.status(201).json({
      message: 'User created successfully',
      accessToken,
      refreshToken,
    });
  } catch (error) {
    res.status(500).json({ message: 'Server error' });
  }
});

export default router;

Warnings

  • Always validate input data
  • Use bcrypt salt rounds of at least 10
6

Implement Login Endpoint

Create a login endpoint that verifies credentials and issues tokens.

typescript
// routes/auth.ts (continued)
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // Find user
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }

    // Verify password
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }

    // Generate tokens
    const accessToken = generateAccessToken({
      userId: user._id.toString(),
      email: user.email,
    });

    const refreshToken = generateRefreshToken({
      userId: user._id.toString(),
      email: user.email,
    });

    res.json({
      message: 'Login successful',
      accessToken,
      refreshToken,
    });
  } catch (error) {
    res.status(500).json({ message: 'Server error' });
  }
});

Tips

  • Use the same error message for user not found and wrong password to prevent user enumeration
7

Implement Token Refresh Endpoint

Allow users to get new access tokens using their refresh token.

typescript
// routes/auth.ts (continued)
router.post('/refresh', async (req, res) => {
  try {
    const { refreshToken } = req.body;

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

    // Verify refresh token
    const payload = verifyRefreshToken(refreshToken);

    // Generate new access token
    const newAccessToken = generateAccessToken({
      userId: payload.userId,
      email: payload.email,
    });

    res.json({
      accessToken: newAccessToken,
    });
  } catch (error) {
    res.status(403).json({ message: 'Invalid refresh token' });
  }
});
8

Protect Routes with Middleware

Use the authentication middleware to protect your API routes.

typescript
// routes/protected.ts
import { Router } from 'express';
import { authenticateToken, AuthRequest } from '../middleware/auth';

const router = Router();

router.get('/profile', authenticateToken, async (req: AuthRequest, res) => {
  try {
    const user = await User.findById(req.user!.userId).select('-password');
    res.json({ user });
  } catch (error) {
    res.status(500).json({ message: 'Server error' });
  }
});

export default router;
9

Test Your Authentication

Use Postman or curl to test registration, login, and protected routes.

bash
# Register
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123","name":"Test User"}'

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

# Access protected route
curl http://localhost:3000/profile \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Tips

  • Save the tokens returned from login for testing
  • Test with expired tokens to verify error handling
  • Test the refresh token flow
Tags: Node.js JWT Authentication Security Express

Was this guide helpful?

Check out more step-by-step guides for development, deployment, debugging, and configuration.