Once you have a REST API, the next logical step is to secure it. You need a way to know who is making requests and control what they can access. This process is called **authentication**. In this guide, we'll implement a robust, modern authentication system for a Node.js Express API using **JSON Web Tokens (JWT)**.

We'll cover two of the most critical security practices: securely storing user passwords by **hashing** them with `bcrypt`, and creating a stateless authentication system with JWT. By the end, you'll be able to build:

  • A user registration endpoint that securely saves user credentials.
  • A user login endpoint that returns a JWT upon success.
  • Protected routes that can only be accessed by authenticated users.

Prerequisites

This tutorial builds on the concepts from our Node.js REST API tutorial. You should be comfortable with creating a basic Express server and setting up routes. You will also need Postman to test the authentication flow.

Step 1: Installing Dependencies and Setup

Let's start by installing the necessary libraries for authentication and security.

npm install jsonwebtoken bcryptjs dotenv
  • jsonwebtoken: To create and verify JSON Web Tokens.
  • bcryptjs: To hash passwords before storing them. Never store plain-text passwords!
  • dotenv: To manage environment variables, like our JWT secret key.

Next, create a file named .env in the root of your project. This file will hold our secret key.

# .env file
JWT_SECRET=your_super_secret_and_long_random_string
It is critical that your JWT_SECRET is a long, complex, and random string that nobody can guess. Do NOT commit this file to Git.

To load these environment variables into our application, add the following line at the very top of your `index.js` file:

require('dotenv').config();

Step 2: User Registration Endpoint

First, we need a way for users to sign up. This endpoint will take an email and password, hash the password, and store the new user. For this guide, we'll continue using an in-memory array to store users.

In `index.js`, add a users array and the `/register` endpoint:

const bcrypt = require('bcryptjs');

// In-memory user store (in a real app, use a database)
const users = [];

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

    // Check if user already exists
    const existingUser = users.find(user => user.email === email);
    if (existingUser) {
      return res.status(400).send('User with this email already exists.');
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10); // 10 is the salt round

    const newUser = {
      id: users.length + 1,
      email,
      password: hashedPassword
    };

    users.push(newUser);
    console.log(users); // For debugging
    res.status(201).send('User registered successfully.');

  } catch (error) {
    res.status(500).send(error.message);
  }
});

Step 3: User Login Endpoint and JWT Generation

When a user logs in, we need to verify their credentials and, if they are correct, issue them a JWT. This token will act as their "passport" for accessing protected parts of our API.

const jwt = require('jsonwebtoken');

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

    // Find the user by email
    const user = users.find(user => user.email === email);
    if (!user) {
      return res.status(400).send('Invalid credentials.');
    }

    // Check if the password is correct
    const isPasswordCorrect = await bcrypt.compare(password, user.password);
    if (!isPasswordCorrect) {
      return res.status(400).send('Invalid credentials.');
    }

    // Generate a JWT
    const token = jwt.sign(
      { userId: user.id, email: user.email }, // Payload
      process.env.JWT_SECRET,                  // Secret Key
      { expiresIn: '1h' }                       // Options (e.g., token expiration)
    );

    res.json({ token });

  } catch (error) {
    res.status(500).send(error.message);
  }
});
The JWT payload contains information about the user that we can trust because it's digitally signed by our server. We send this token back to the client.

Step 4: Creating Authentication Middleware

Middleware is a function that runs before our route handlers. We'll create a middleware to check for a valid JWT on incoming requests to protected routes. This is the gatekeeper of our API.

Create a function `authenticateToken.js` or add this directly in `index.js`:

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

  if (token == null) {
    return res.sendStatus(401); // Unauthorized
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.sendStatus(403); // Forbidden (token is no longer valid)
    }
    req.user = user;
    next(); // Proceed to the next middleware or route handler
  });
}

Step 5: Protecting a Route

Now we can use our `authenticateToken` middleware to protect any route we want. Let's create a `/profile` endpoint that only a logged-in user can access.

// A protected route
app.get('/profile', authenticateToken, (req, res) => {
  // req.user is available here because our middleware added it
  res.json({
    message: `Welcome to your profile, ${req.user.email}!`,
    userData: req.user
  });
});

Notice how we simply pass `authenticateToken` as the second argument to `app.get()`. Express will automatically run it before executing our main route logic.

Step 6: Testing the Full Flow with Postman

  1. **Register:** Send a `POST` request to `http://localhost:3000/register` with an email and password in the JSON body. You should get a `201 Created`.
  2. **Attempt Unauthorized Access:** Send a `GET` request to `http://localhost:3000/profile`. You should get a `401 Unauthorized` error because you haven't provided a token.
  3. **Login:** Send a `POST` request to `http://localhost:3000/login` with the same credentials. You will receive a JSON object with a `token`. Copy this token string.
  4. **Authorized Access:** Create a new `GET` request to `http://localhost:3000/profile`. Go to the "Authorization" tab, select "Bearer Token" from the dropdown, and paste your token into the field. Send the request. You should now get a `200 OK` with your profile data!

Conclusion

You have now implemented a complete and secure authentication system in a Node.js API. You've learned how to protect user passwords with `bcrypt`, issue stateless credentials with JWT, and protect routes using Express middleware. This is a fundamental and highly sought-after skill for any backend developer. You can now confidently build secure APIs that properly manage user access.