How to Build Secure and Scalable Authentication System with Node.js and MongoDB
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:
In the code above, we define two routes: /forgot-password
and /reset-password
to handle the password reset functionality.
- 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 thecrypto
module and store it in the user'sresetToken
field along with an expiration time. Finally, we send a success response indicating that the reset token has been sent. - The
/reset-password
route is used to reset the user's password using the reset token. We extract theresetToken
andnewPassword
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 usingbcrypt
. We then update the user's password and reset token fields and save the changes to the database. - 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:
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/