---
url: 'https://www.corbado.com/blog/nodejs-express-mongodb-jwt-authentication-roles'
title: 'Node.js Express JWT Authentication with MongoDB & Roles'
description: 'This tutorial teaches you how to build a secure Node.js and MongoDB backend with JWT Authentication and Authorization.'
lang: 'en'
author: 'Lukas'
date: '2025-01-15T15:12:50.744Z'
lastModified: '2026-03-27T07:01:08.142Z'
keywords: 'MongoDB authentication, MongoDB auth'
category: 'Engineering'
---

# Node.js Express JWT Authentication with MongoDB & Roles

## 1. Introduction

In this tutorial, we will build a secure backend application using **Node.js** and
**MongoDB** that supports user authentication (registration and login) and authorization
with **JSON Web Tokens (JWT)**. By the end of this guide, you will understand how to:

1. Implement a secure flow for user signup and login using JWT.
2. Structure a [Node.js](https://www.corbado.com/blog/nodejs-passkeys) Express application with modern
   middlewares for CORS, authentication, and authorization.
3. Configure Express routes to integrate JWT seamlessly.
4. Define Mongoose models for managing users and roles.
5. Interact with a MongoDB database using Mongoose.

This comprehensive guide ensures you have all the necessary information to implement the
application independently, following the latest best practices and leveraging modern
technologies.

## 2. Token-Based Authentication

![token based authentication](https://www.corbado.com/website-assets/token_based_authentication_35a99552df.png)

### 2.1 Understanding JWT

**JSON Web Tokens (JWT)** are a compact and self-contained way for securely transmitting
information between parties as a JSON object. Unlike session-based authentication, where
session data is stored on the server, JWTs are stored on the client side (e.g., in Local
Storage for browsers) and eliminate the need for additional backend sessions or
authentication modules for different clients.

A JWT consists of three parts:

- **Header**: Contains the type of token and the signing algorithm.
- **Payload**: Contains the claims or the data you want to transmit.
- **Signature**: Ensures the token hasn't been altered.

The structure follows the `header.payload.signature` format. Clients typically attach JWTs
in the `Authorization` header with the `Bearer` prefix:

```http
Authorization: Bearer <token>
```

### 2.2 Advantages of Token-Based Authentication

- **Scalability**: No need to store session data on the server.
- **Flexibility**: Suitable for Single Page Applications (SPAs), mobile apps, and other
  clients.
- **Security**: Signed tokens ensure data integrity and authenticity.

## 3. Project Structure

See the sign-up, login and access resource sequence charts:

![jwt auth sign up sign in](https://www.corbado.com/website-assets/jwt_auth_sign_up_sign_in_2fd1e6fe49.png)

There's also the necessity to refresh tokens if the old one is expired:

![refresh token](https://www.corbado.com/website-assets/refresh_token_1b1fd5f3d9.png)

Here's the directory structure for our [Node.js](https://www.corbado.com/blog/nodejs-passkeys), Express, and
MongoDB application:

```
node-js-jwt-auth-mongodb/
├── app/
│   ├── config/
│   │   ├── auth.config.js
│   │   └── db.config.js
│   ├── controllers/
│   │   ├── auth.controller.js
│   │   └── user.controller.js
│   ├── middlewares/
│   │   ├── authJwt.js
│   │   └── verifySignUp.js
│   ├── models/
│   │   ├── index.js
│   │   ├── role.model.js
│   │   └── user.model.js
│   └── routes/
│       ├── auth.routes.js
│       └── user.routes.js
├── package.json
└── server.js
```

Here is the overview of the [Node.js](https://www.corbado.com/blog/nodejs-passkeys) Express app:

![nodejs express mongodb architecture authentication authorization](https://www.corbado.com/website-assets/nodejs_express_mongodb_architecture_authentication_authorization_b4b4da61b1.png)

## 4. Implementation Steps

### 4.1 Create Node.js Application

First, create a new directory for your project and initialize a Node.js application:

```bash
mkdir node-js-jwt-auth-mongodb
cd node-js-jwt-auth-mongodb
npm init -y
```

Update your `package.json` to use ESModules by adding `"type": "module"`:

Install the required dependencies:

```bash
npm install express cors bcryptjs jsonwebtoken mongoose
```

### 4.2 Setup Express Web Server

Create a `server.js` file in the root directory:

```javascript
// server.js
import express from "express";
import cors from "cors";
import db from "./app/models/index.js";
import authRoutes from "./app/routes/auth.routes.js";
import userRoutes from "./app/routes/user.routes.js";

const app = express();

// Middleware configuration
const corsOptions = {
    origin: "http://localhost:8081",
};

app.use(cors(corsOptions));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Simple route for testing
app.get("/", (req, res) => {
    res.json({ message: "Welcome to the Node.js JWT Authentication application." });
});

// Routes
app.use("/api/auth", authRoutes);
app.use("/api/test", userRoutes);

// Set port and start server
const PORT = process.env.PORT || 8080;

// Connect to MongoDB and start the server
db.mongoose
    .connect(`mongodb://${db.config.HOST}:${db.config.PORT}/${db.config.DB}`)
    .then(() => {
        console.log("Successfully connected to MongoDB.");
        // Initialize roles in the database
        initial();
        app.listen(PORT, () => {
            console.log(`Server is running on port ${PORT}.`);
        });
    })
    .catch((err) => {
        console.error("Connection error:", err);
        process.exit();
    });

// Initial function to populate roles
function initial() {
    db.Role.estimatedDocumentCount()
        .then((count) => {
            if (count === 0) {
                return Promise.all([
                    new db.Role({ name: "user" }).save(),
                    new db.Role({ name: "admin" }).save(),
                    new db.Role({ name: "moderator" }).save(),
                ]);
            }
        })
        .then((roles) => {
            if (roles) {
                console.log(
                    "Added 'user', 'admin', and 'moderator' to roles collection.",
                );
            }
        })
        .catch((err) => {
            console.error("Error initializing roles:", err);
        });
}
```

### 4.3 Configure MongoDB Database

Create a configuration file for MongoDB connection parameters:

```javascript
// app/config/db.config.js
export default {
    HOST: "localhost",
    PORT: 27017,
    DB: "node_js_jwt_auth_db",
};
```

### 4.4 Define Mongoose Models

#### User Model

```javascript
// app/models/user.model.js
import mongoose from "mongoose";

const userSchema = new mongoose.Schema(
    {
        username: {
            type: String,
            required: true,
            unique: true,
            trim: true,
        },
        email: {
            type: String,
            required: true,
            unique: true,
            trim: true,
            lowercase: true,
        },
        password: {
            type: String,
            required: true,
            minlength: 6,
        },
        roles: [
            {
                type: mongoose.Schema.Types.ObjectId,
                ref: "Role",
            },
        ],
    },
    { timestamps: true },
);

const User = mongoose.model("User", userSchema);
export default User;
```

#### Role Model

```javascript
// app/models/role.model.js
import mongoose from "mongoose";

const roleSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true,
    },
});

const Role = mongoose.model("Role", roleSchema);
export default Role;
```

#### Index Model

```javascript
// app/models/index.js
import mongoose from "mongoose";
import dbConfig from "../config/db.config.js";

import User from "./user.model.js";
import Role from "./role.model.js";

const db = {};

db.mongoose = mongoose;
db.User = User;
db.Role = Role;

db.ROLES = ["user", "admin", "moderator"];
db.config = dbConfig;

export default db;
```

### 4.5 Initialize Mongoose

Mongoose is initialized in the `server.js` file, where we connect to the MongoDB database
and initialize roles if they don't exist.

### 4.6 Configure Authentication Keys

Create a configuration file for authentication-related settings:

```javascript
// app/config/auth.config.js
export default {
    secret: "your-secret-key", // Replace with your own secret key
};
```

**Security Note:** Ensure that the secret key is stored securely, preferably using
environment variables or a secrets manager in a production environment.

### 4.7 Create Middleware Functions

#### Verify Sign-Up Middleware

This middleware checks for duplicate usernames or emails and verifies the existence of
roles.

```javascript
// app/middlewares/verifySignUp.js
import db from "../models/index.js";

const ROLES = db.ROLES;
const User = db.User;

const checkDuplicateUsernameOrEmail = async (req, res, next) => {
    try {
        // Check if username exists
        const userByUsername = await User.findOne({ username: req.body.username });
        if (userByUsername) {
            return res
                .status(400)
                .json({ message: "Failed! Username is already in use!" });
        }

        // Check if email exists
        const userByEmail = await User.findOne({ email: req.body.email });
        if (userByEmail) {
            return res.status(400).json({ message: "Failed! Email is already in use!" });
        }

        next();
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
};

const checkRolesExisted = (req, res, next) => {
    if (req.body.roles) {
        const invalidRoles = req.body.roles.filter((role) => !ROLES.includes(role));
        if (invalidRoles.length > 0) {
            return res.status(400).json({
                message: `Failed! Roles [${invalidRoles.join(", ")}] do not exist!`,
            });
        }
    }
    next();
};

const verifySignUp = {
    checkDuplicateUsernameOrEmail,
    checkRolesExisted,
};

export default verifySignUp;
```

#### JWT Authentication Middleware

This middleware verifies the presence and validity of JWTs and checks user roles for
authorization.

```javascript
// app/middlewares/authJwt.js
import jwt from "jsonwebtoken";
import config from "../config/auth.config.js";
import db from "../models/index.js";

const User = db.User;
const Role = db.Role;

const verifyToken = async (req, res, next) => {
    let token = req.headers["x-access-token"] || req.headers["authorization"];

    if (!token) {
        return res.status(403).json({ message: "No token provided!" });
    }

    // Remove 'Bearer ' prefix if present
    if (token.startsWith("Bearer ")) {
        token = token.slice(7, token.length);
    }

    try {
        const decoded = jwt.verify(token, config.secret);
        req.userId = decoded.id;

        // Fetch user details
        const user = await User.findById(req.userId);
        if (!user) {
            return res.status(404).json({ message: "User not found!" });
        }

        req.user = user;
        next();
    } catch (err) {
        return res.status(401).json({ message: "Unauthorized!" });
    }
};

const isAdmin = async (req, res, next) => {
    try {
        const user = req.user;
        const roles = await Role.find({ _id: { $in: user.roles } });

        const hasAdminRole = roles.some((role) => role.name === "admin");

        if (!hasAdminRole) {
            return res.status(403).json({ message: "Require Admin Role!" });
        }

        next();
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
};

const isModerator = async (req, res, next) => {
    try {
        const user = req.user;
        const roles = await Role.find({ _id: { $in: user.roles } });

        const hasModeratorRole = roles.some((role) => role.name === "moderator");

        if (!hasModeratorRole) {
            return res.status(403).json({ message: "Require Moderator Role!" });
        }

        next();
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
};

const authJwt = {
    verifyToken,
    isAdmin,
    isModerator,
};

export default authJwt;
```

#### Index Middleware

Combining all middleware functions for easier import.

```javascript
// app/middlewares/index.js
import authJwt from "./authJwt.js";
import verifySignUp from "./verifySignUp.js";

export { authJwt, verifySignUp };
```

### 4.8 Create Controllers

#### Authentication Controller

Handles user registration (`signup`) and login (`signin`).

```javascript
// app/controllers/auth.controller.js
import config from "../config/auth.config.js";
import db from "../models/index.js";
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";

const User = db.User;
const Role = db.Role;

export const signup = async (req, res) => {
    try {
        // Create a new user
        const user = new User({
            username: req.body.username,
            email: req.body.email,
            password: bcrypt.hashSync(req.body.password, 8),
        });

        const role = await Role.findOne({ name: "user" });
        user.roles = [role._id];

        // Save user to the database
        await user.save();
        res.status(201).json({ message: "User was registered successfully!" });
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
};

export const signin = async (req, res) => {
    try {
        // Find user by username
        const user = await User.findOne({ username: req.body.username }).populate(
            "roles",
            "-__v",
        );

        if (!user) {
            return res.status(404).json({ message: "User Not found." });
        }

        // Validate password
        const passwordIsValid = bcrypt.compareSync(req.body.password, user.password);
        if (!passwordIsValid) {
            return res.status(401).json({
                accessToken: null,
                message: "Invalid Password!",
            });
        }

        // Generate JWT
        const token = jwt.sign({ id: user.id }, config.secret, {
            algorithm: "HS256",
            expiresIn: 86400, // 24 hours
        });

        // Extract user roles
        const authorities = user.roles.map((role) => `ROLE_${role.name.toUpperCase()}`);

        res.status(200).json({
            id: user._id,
            username: user.username,
            email: user.email,
            roles: authorities,
            accessToken: token,
        });
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
};
```

#### User Controller

Handles access to protected resources based on user roles.

```javascript
// app/controllers/user.controller.js
export const allAccess = (req, res) => {
    res.status(200).send("Public Content.");
};

export const userBoard = (req, res) => {
    res.status(200).send("User Content.");
};

export const moderatorBoard = (req, res) => {
    res.status(200).send("Moderator Content.");
};

export const adminBoard = (req, res) => {
    res.status(200).send("Admin Content.");
};
```

### 4.9 Define Routes

#### Authentication Routes

Define routes for user signup and signin.

```javascript
// app/routes/auth.routes.js
import express from "express";
import { signup, signin } from "../controllers/auth.controller.js";
import { verifySignUp } from "../middlewares/index.js";

const router = express.Router();

// Signup route
router.post(
    "/signup",
    [verifySignUp.checkDuplicateUsernameOrEmail, verifySignUp.checkRolesExisted],
    signup,
);

// Signin route
router.post("/signin", signin);

export default router;
```

#### User Routes

Define routes for accessing protected resources based on user roles.

```javascript
// app/routes/user.routes.js
import express from "express";
import {
    adminBoard,
    allAccess,
    moderatorBoard,
    userBoard,
} from "../controllers/user.controller.js";
import { authJwt } from "../middlewares/index.js";

const router = express.Router();

// Public route
router.get("/all", allAccess);

// User route (any authenticated user)
router.get("/user", [authJwt.verifyToken], userBoard);

// Moderator route
router.get("/mod", [authJwt.verifyToken, authJwt.isModerator], moderatorBoard);

// Admin route
router.get("/admin", [authJwt.verifyToken, authJwt.isAdmin], adminBoard);

export default router;
```

## 5. Running and Testing the Application

### 5.1 Start the Server

Run the application using the start script defined in `package.json`:

```bash
npm start
```

You should see console logs indicating that the server is running and connected to
MongoDB:

```
Successfully connected to MongoDB.
Added 'user', 'admin', and 'moderator' to roles collection.
Server is running on port 8080.
```

### 5.2 Create a local MongoDB instance

In case you don't have a MongoDB instance to use for testing available, you can create a
temporary one using the following docker command:

```bash
docker run --rm -p 27017:27017 mongo
```

### 5.2 Test the API Endpoints

Use tools like **Postman** or **cURL** to interact with the API.

#### 5.2.1 Register a New User

**Endpoint:** `POST /api/auth/signup`

**Body:**

```json
{
    "username": "john_doe",
    "email": "john@example.com",
    "password": "password123"
}
```

**Response:**

```json
{
    "message": "User was registered successfully!"
}
```

_Note:_ All users are assigned the `user` role by default. More privileged roles should
not be obtained through registration, but directly through database access.

#### 5.2.2 Login

**Endpoint:** `POST /api/auth/signin`

**Body:**

```json
{
    "username": "john_doe",
    "password": "password123"
}
```

**Response:**

```json
{
    "id": "user_id",
    "username": "john_doe",
    "email": "john@example.com",
    "roles": ["ROLE_USER"],
    "accessToken": "jwt_token"
}
```

#### 5.2.3 Access Public Resource

**Endpoint:** `GET /api/test/all`

**Response:**

```
Public Content.
```

#### 5.2.4 Access Protected Resource (User)

**Endpoint:** `GET /api/test/user`

**Headers:**

```
Authorization: Bearer <jwt_token>
```

**Response:**

```
User Content.
```

#### 5.2.5 Access Admin Resource

**Endpoint:** `GET /api/test/admin`

**Headers:**

```
Authorization: Bearer <jwt_token>
```

**Response:**

```json
{
    "message": "Require Admin Role!"
}
```

_Note:_ Only users with the `admin` role can access this endpoint.

#### 5.2.6 Access Moderator Resource

**Endpoint:** `GET /api/test/mod`

**Headers:**

```
Authorization: Bearer <jwt_token>
```

**Response:**

```json
{
    "message": "Require Moderator Role!"
}
```

_Note:_ Only users with the `admin` role can access this endpoint.

### 5.3 Handling Errors

- **Invalid Credentials:** Returns a 401 status with an error message.
- **Unauthorized Access:** Returns a 403 status if the user lacks the necessary role.
- **Missing Token:** Returns a 403 status indicating no token was provided.
- **Expired or Invalid Token:** Returns a 401 status indicating unauthorized access.

## 6. Security Considerations

Building a secure authentication and authorization system involves several critical
factors:

### 6.1 Protecting the Secret Key

Ensure that your JWT secret key (`auth.config.js`) is kept secure. In production
environments, use environment variables or a secure secrets manager to store sensitive
information.

### 6.2 Password Security

- **Hashing:** Passwords are hashed using **bcryptjs** before storage to prevent
  plain-text passwords from being exposed.
- **Salting:** Bcrypt automatically handles salting, adding an extra layer of security
  against rainbow table attacks.

### 6.3 Token Security

- **Expiration:** JWTs have an expiration time (`expiresIn: 86400` seconds) to limit the
  window of opportunity for token misuse.
- **Validation:** Tokens are validated on each request to protected endpoints to ensure
  authenticity and integrity.

### 6.4 Role-Based Access Control (RBAC)

Implement RBAC to restrict access to resources based on user roles. This ensures that
users can only access what they are permitted to.

### 6.5 Error Handling

Provide meaningful error messages without exposing sensitive information. Always handle
errors gracefully to prevent potential leaks.

### 6.6 HTTPS

Always use HTTPS in production to encrypt data in transit, protecting tokens and sensitive
information from interception.

### 6.7 Rate Limiting and Throttling

Implement rate limiting to protect against brute-force attacks and denial-of-service (DoS)
attempts.

## 7. Conclusion

Congratulations! You've successfully built a secure backend application using **Node.js**
and **MongoDB** with robust user authentication and authorization using **JWT**. This
setup ensures that your application can handle user registration, login, and role-based
access control efficiently and securely.

### Next Steps

- **Implement Refresh Tokens:** Enhance security by implementing refresh tokens to allow
  users to obtain new access tokens without re-authenticating.
- **Build a Frontend Client:** Develop a frontend application using frameworks like
  [React](https://www.corbado.com/blog/react-passkeys), [Angular](https://www.corbado.com/blog/angular-passkeys), or
  [Vue.js](https://www.corbado.com/blog/vuejs-passkeys) to interact with your backend API.
- **Expand Role Hierarchies:** Create more granular roles and permissions to better suit
  your application's needs.
- **Deploy to Production:** Move your application to a production environment, ensuring
  all security best practices are followed.

Happy coding!
