Australian flagJoin us at the FIDO seminar in Melbourne – Feb 7, 2025!
nodejs express mongodb jwt authentication rolesEngineering

Node.js Express JWT Authentication with MongoDB & Roles

This tutorial teaches you how to build a secure Node.js and MongoDB backend with JWT Authentication and Authorization.

Blog-Post-Author

Lukas

Created: January 15, 2025

Updated: January 17, 2025


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 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#

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:

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#

Here's the directory structure for our Node.js, 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
Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

4. Implementation Steps#

4.1 Create Node.js Application#

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

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:

npm install express cors bcryptjs jsonwebtoken mongoose

4.2 Setup Express Web Server#

Create a server.js file in the root directory:

// 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:

// app/config/db.config.js export default { HOST: 'localhost', PORT: 27017, DB: 'node_js_jwt_auth_db', };
Demo Icon

Want to try passkeys yourself? Check our Passkeys Demo.

Try Passkeys

4.4 Define Mongoose Models#

User Model#

// 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#

// 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#

// 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;
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

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:

// 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.

// 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.

// 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.

// app/middlewares/index.js import authJwt from './authJwt.js'; import verifySignUp from './verifySignUp.js'; export { authJwt, verifySignUp };
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

4.8 Create Controllers#

Authentication Controller#

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

// 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.

// 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.

// 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.

// 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;
Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

5. Running and Testing the Application#

5.1 Start the Server#

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

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:

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:

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

Response:

{ "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:

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

Response:

{ "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:

{ "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:

{ "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.
StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

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, Angular, or Vue.js 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!

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free