How to Build Secure and Scalable Authentication System with Node.js and MongoDB

Sandeep Singh (Full Stack Dev.)
11 min readJun 1, 2023

Introduction

In the world of web development, security is of utmost importance, especially when dealing with user data and sensitive information. Creating a robust authentication and authorization system is essential to protect user accounts and control access to different parts of an application. In this article, we will explore how to build an authentication and authorization system using Node.js, Express.js, and MongoDB.

Understanding Authentication and Authorization

Before we dive into the implementation details, let's clarify the concepts of authentication and authorization. Authentication is the process of verifying the identity of a user, ensuring they are who they claim to be. Authorization, on the other hand, focuses on granting or denying access to specific resources or functionalities based on the user's role and permissions.

Setting Up the Development Environment

To get started, we need to set up our development environment. Ensure that you have Node.js and MongoDB installed on your machine. You can download and install them from their official websites if you haven't already.

Installing Required Dependencies

Once the development environment is ready, we can proceed to install the required dependencies. We will use npm (Node Package Manager) to manage our project dependencies. Open your terminal and navigate to the project directory. Run the following command to initialize a new Node.js project:

npm init -y

This command will create a package.json file in your project directory. Next, we need to install the necessary packages. Run the following commands to install Express.js, Mongoose, and other required dependencies:

npm install express mongoose bcrypt jsonwebtoken

Creating the Express Application

With the dependencies installed, we can now create our Express application. Create a new file called app.js and open it in your preferred code editor. Let's begin by importing the required modules and setting up the basic configuration:

const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.json());

// Start the server
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Configuring MongoDB

To store user information and session data, we will be using MongoDB. Create a new file called db.js and add the following code to connect to the MongoDB database.

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/auth_demo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Connected to MongoDB'))
.catch((err) => console.error('Failed to connect to MongoDB', err));

User Registration

Now that our application and database are set up, let's move on to user registration. We need to create a user model and define the necessary routes to handle user registration requests.

const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
createdAt: { type: Date, default: Date.now },
});

const User = mongoose.model('User', userSchema);

module.exports = User;

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

In the code above, we define a userSchema using the mongoose.Schema function. The schema defines the fields and their respective types for the User model. In this example, we have fields such as name, email, password, role, and createdAt.

The name field is of type String and is required. The email field is also of type String, is required, and has a unique constraint to ensure each user has a unique email address. The password field is of type String and is required to store the user's encrypted password. The role field is of type String and has two possible values: 'user' and 'admin', with the default value set to 'user'. Lastly, the createdAt field is of type Date and has a default value of the current date and time when a new user is created.

We then create a User model using mongoose.model(), passing in the model name ('User') and the defined userSchema. Finally, we export the User model to be used in other parts of the application.

This code serves as a basic example, and you can modify and expand it based on your specific requirements for the User model in your application.

Add the following code to your app.js file:

const User = require('./models/user');
// Register a new user
app.post('/register', async (req, res) => {
try {
const { email, password } = req.body;

// Check if the email is already registered
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}

// Create a new user
const newUser = new User({ email, password });
await newUser.save();

res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
console.error('Error registering user:', error);
res.status(500).json({ error: 'An error occurred while registering the user' });
}
});

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

In the code above, we define a route /register using the HTTP POST method to handle user registration requests. We extract the email and password from the request body. Next, we check if the email is already registered by querying the database using the User model. If an existing user with the same email is found, we return an error response.

If the email is not already registered, we create a new User instance and save it to the database using the save() method. Finally, we send a success response indicating that the user has been registered.

User Login

Once users are registered, they should be able to log in to access protected resources. Let’s implement the user login functionality by adding the following code to app.js:

// User login
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
    // Check if the user exists
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Validate the password
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Generate a JWT token
const token = jwt.sign({ userId: user._id }, 'secretKey');

res.status(200).json({ token });
} catch (error) {
console.error('Error logging in:', error);
res.status(500).json({ error: 'An error occurred while logging in' });
}
});

In the code above, we define a route /login using the HTTP POST method to handle user login requests. We extract the email and password from the request body. First, we check if a user with the provided email exists in the database. If not, we return an error response.

If the user exists, we validate the password by calling the comparePassword() method of the User model. This method compares the provided password with the hashed password stored in the database. If the passwords match, we generate a JSON Web Token (JWT) using the sign() method from the jsonwebtoken package. The token contains the user's unique identifier (userId).

Finally, we send a success response with the generated token, which the client can use to authenticate subsequent requests.

Implementing Authentication Middleware

To secure our routes and ensure that only authenticated users can access protected resources, we need to implement authentication middleware. This middleware will verify the JWT token included in the request headers. Add the following code to app.js:

// Authentication middleware
const authenticateUser = (req, res, next) => {
try {
const token = req.headers.authorization.split(' ')[1];

// Verify the token
const decodedToken = jwt.verify(token, 'secretKey');

// Attach the user ID to the request object
req.userId = decodedToken.userId;

next();
} catch (error) {
console.error('Error authenticating user:', error);
res.status(401).json({ error: 'Unauthorized' });
}
};
// Apply authentication middleware to protected routes
app.get('/protected', authenticateUser, (req, res) => {
// Handle protected route logic here
});

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

In the code above, we define the authenticateUser middleware function that will be applied to protected routes. Within the middleware, we extract the token from the request headers and verify it using the verify() method from the jsonwebtoken package. If the token is valid, we decode it to obtain the userId and attach it to the request object for future use.

Next, we apply the authenticateUser middleware to the /protected route using the app.get() method. This means that only authenticated users with a valid token will be able to access this route. You can replace the comment // Handle protected route logic here with your own logic for the protected route.

User Roles and Permissions

In many applications, different users have different roles and permissions. Let’s explore how we can implement user roles and permissions using MongoDB.

First, we need to modify our User model to include a role field. Open the user.js file and add the following code:

const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
});

In the code above, we add a new field role to the user schema. This field is of type String and has two possible values: 'user' and 'admin'. The default value is set to 'user'.

Next, we can implement authorization logic based on user roles and permissions. For example, if only admin users should be able to access certain routes, we can define a middleware that checks the user’s role before granting access. Add the following code to app.js:

// Authorization middleware
const authorizeUser = (requiredRole) => (req, res, next) => {
try {
const user = await User.findById(req.userId);

if (user.role !== requiredRole) {
return res.status(403).json({ error: 'Forbidden' });
}

next();
} catch (error) {
console.error('Error authorizing user:', error);
res.status(500).json({ error: 'An error occurred while authorizing the user' });
}
};
// Apply authorization middleware to restricted routes
app.get('/admin', authenticateUser, authorizeUser('admin'), (req, res) => {
// Handle admin-only route logic here
});

In the code above, we define the authorizeUser middleware function, which takes a requiredRole parameter. This function returns another middleware function that will be applied to restricted routes. Within the middleware, we fetch the user from the database using the User.findById() method and check if their role matches the requiredRole. If not, we return a 403 Forbidden error response.

Finally, we apply the `authenticateUser` middleware followed by the `authorizeUser` middleware to the `/admin` route using the `app.get()` method. This means that only authenticated users with an ‘admin’ role will be able to access this route. You can replace the comment `// Handle admin-only route logic here` with your own logic for the admin-only route.

Restricting Access to Routes In addition to role-based authorization, you may also want to restrict access to certain routes based on specific conditions. For example, allowing only logged-in users to access a particular route. Let’s see how we can implement route restrictions.

// Restrict access to a route 
app.get('/restricted', authenticateUser, (req, res) => { // Handle restricted route logic here });

In the code above, we apply the authenticateUser middleware to the /restricted route using the app.get() method. This means that only authenticated users with a valid token will be able to access this route. You can replace the comment // Handle restricted route logic here with your own logic for the restricted route.

Password Encryption and Hashing

To ensure the security of user passwords, it is crucial to encrypt and hash them before storing them in the database. We can use the bcrypt library to achieve this. Let's update our user registration logic to include password encryption and hashing.

const bcrypt = require('bcrypt');
// Register a new user
app.post('/register', async (req, res) => {
try {
const { email, password } = req.body;

// Check if the email is already registered
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}

// Encrypt and hash the password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

// Create a new user
const newUser = new User({ email, password: hashedPassword });
await newUser.save();

res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
console.error('Error registering user:', error);
res.status(500).json({ error: 'An error occurred while registering the user' });
}
});

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

In the code above, we import the bcrypt library and use the genSalt() method to generate a salt, which is a random string used in the hashing process. We then use the hash() method to hash the password with the generated salt. The resulting hashed password is stored in the database instead of the plain text password.

Resetting Forgotten Passwords

It’s common for users to forget their passwords and need a way to reset them. Let’s implement a password reset functionality using a reset token.

// Generate a password reset token
app.post('/forgot-password', async (req, res) => {
try {
const { email } = req.body;

// Check if the user exists
const user = await User.findOne({ email });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}

// Generate a reset token
const resetToken = crypto.randomBytes(20).toString('hex');
user.resetToken = resetToken;
user.resetTokenExpiration = Date.now() + 3600000; // Token expires in 1 hour
await user.save();
res.status(200).json({ message: 'Password reset token sent' });
} catch (error) {
console.error('Error generating reset token:', error);
res.status(500).json({ error: 'An error occurred while generating the reset token' });
}
});

// Reset password using the reset token
app.post('/reset-password', async (req, res) => {
try {
const { resetToken, newPassword } = req.body;

// Find the user with the provided reset token
const user = await User.findOne({
resetToken,
resetTokenExpiration: { $gt: Date.now() },
});
if (!user) {
return res.status(401).json({ error: 'Invalid or expired reset token' });
}

// Encrypt and hash the new password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(newPassword, salt);

// Update the user's password and reset token fields
user.password = hashedPassword;
user.resetToken = undefined;
user.resetTokenExpiration = undefined;
await user.save();

res.status(200).json({ message: 'Password reset successful' });
} catch (error) {
console.error('Error resetting password:', error);
res.status(500).json({ error: 'An error occurred while resetting the password' });
}
});

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Deploy this Nodejs server: If you want to deploy this server , I think the best, high speed and cheaper is Hostinger’s VPS Hosting. My majority of the Nodejs servers are deployed there and its is quite easier to deploy there…
Currently Hostinger also giving 3 Months🤩 Extra for free, I think the best time to buy this , if you are looking for deploying🧑‍💻 your Nodejs server:

https://www.hostinger.com/vps-hosting

In the code above, we define two routes: /forgot-password and /reset-password to handle the password reset functionality.

  1. The /forgot-password route is responsible for generating a password reset token and sending it to the user's email. We first check if the user exists in the database. If the user is found, we generate a random reset token using the crypto module and store it in the user's resetToken field along with an expiration time. Finally, we send a success response indicating that the reset token has been sent.
  2. The /reset-password route is used to reset the user's password using the reset token. We extract the resetToken and newPassword from the request body. We search for a user with the provided reset token and check if the token is valid and has not expired. If the user is found, we encrypt and hash the new password using bcrypt. We then update the user's password and reset token fields and save the changes to the database.
  3. These steps ensure that the user’s password can be securely reset using a reset token.

Conclusion

In this article, we have covered the process of creating an authentication and authorization system using Node.js, Express.js, and MongoDB. We implemented user registration, user login, authentication middleware, user roles and permissions, password encryption and hashing, as well as password reset functionality.

By following the outlined steps and understanding the provided code examples, you can build a robust authentication and authorization system for your Node.js applications. Remember to consider security best practices, handle errors effectively, and test your system thoroughly.

If you have any further questions or need assistance, feel free to refer to the FAQs section below or reach out for support.

Deploy this Nodejs server: If you want to deploy this server , I think the best, high speed and cheaper is Hostinger’s VPS Hosting. My majority of the Nodejs servers are deployed there and its is quite easier to deploy there…
Currently Hostinger also giving 3 Months🤩 Extra for free, I think the best time to buy this , if you are looking for deploying🧑‍💻 your Nodejs server:

https://www.hostinger.com/vps-hosting

Click here to get full source code🌟👨‍💻: https://github.com/sandy088/secure-and-scalable-authent-and-authoriz-System

Let’s Connect👋 on Linkedin: https://www.linkedin.com/in/sandeep-singh55/

--

--

Sandeep Singh (Full Stack Dev.)

Fullstack Developer | MERN & Flutter | Passionate about Open Source | Engaged in Contributing & Collaborating for a Better Tech Community. 🚀