Implementing Safe User Login in Node.js Using JWT and Mongoose
Step-by-Step Guide to User Login Authentication in Node.js
In the era of web applications, user authentication stands as a cornerstone of security and user experience. Today, we will explore a comprehensive implementation of a login function in a Node.js application, leveraging Mongoose for MongoDB interactions, JWT for token management, and Express.js for handling HTTP requests. By the end of this post, you'll have a clear understanding of how to securely authenticate users and manage their sessions using access and refresh tokens.
The Login Function: Implementing user authentication in a Node.js app using Mongoose, JWT, and Express.js for secure session management.
Here is the complete code for our login function, designed to authenticate users, generate access and refresh tokens, and securely send these tokens to the client via HTTP cookies.
javascriptCopy codeconst loginUser = asyncHandler(async (req, res) => {
// Extracting email, username, and password from the request body
const { email, username, password } = req.body;
console.log(email);
// Validate that either username or email is provided
if (!username && !email) {
throw new ApiError(400, "Username or email is required");
}
// Find the user by username or email
const user = await User.findOne({
$or: [{ username }, { email }]
});
// Check if the user exists
if (!user) {
throw new ApiError(404, "User does not exist");
}
// Validate the provided password
const isPasswordValid = await user.isPasswordCorrect(password);
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials");
}
// Generate access and refresh tokens
const { accessToken, refreshToken } = await generateAccessAndRefereshTokens(user._id);
// Retrieve the user without sensitive fields
const loggedInUser = await User.findById(user._id).select("-password -refreshToken");
// Cookie options for secure transmission
const options = {
httpOnly: true,
secure: true
};
// Send the tokens and user data as response
return res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", refreshToken, options)
.json(
new ApiResponse(
200,
{
user: loggedInUser,
accessToken,
refreshToken
},
"User logged In Successfully"
)
);
});
Breaking Down the Code
Let's dive deeper into each section of this function to understand how it works.
1. Extracting Request Data
javascriptCopy codeconst { email, username, password } = req.body;
console.log(email);
We start by extracting email
, username
, and password
from the request body. These values are provided by the client during the login attempt.
2. Validating Input
javascriptCopy codeif (!username && !email) {
throw new ApiError(400, "Username or email is required");
}
We ensure that either a username or an email is provided. If neither is present, we throw a 400 Bad Request
error.
3. Finding the User
javascriptCopy codeconst user = await User.findOne({
$or: [{ username }, { email }]
});
We search for the user in the database using either the provided username or email. This allows for flexible login methods.
4. Checking User Existence
javascriptCopy codeif (!user) {
throw new ApiError(404, "User does not exist");
}
If no user is found, we throw a 404 Not Found
error.
5. Validating the Password
javascriptCopy codeconst isPasswordValid = await user.isPasswordCorrect(password);
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials");
}
We validate the provided password using a method defined on the user model. If the password is incorrect, we throw a 401 Unauthorized
error.
6. Generating Tokens
javascriptCopy codeconst { accessToken, refreshToken } = await generateAccessAndRefereshTokens(user._id);
We generate an access token and a refresh token using a helper function. These tokens will be used to manage the user's session.
7. Retrieving the User Without Sensitive Fields
javascriptCopy codeconst loggedInUser = await User.findById(user._id).select("-password -refreshToken");
We retrieve the user document again, excluding the password
and refreshToken
fields to avoid sending sensitive information back to the client.
8. Setting Cookie Options
javascriptCopy codeconst options = {
httpOnly: true,
secure: true
};
We define cookie options to enhance security. httpOnly
ensures cookies are inaccessible via JavaScript, and secure
ensures they are only sent over HTTPS.
9. Sending the Response
javascriptCopy codereturn res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", refreshToken, options)
.json(
new ApiResponse(
200,
{
user: loggedInUser,
accessToken,
refreshToken
},
"User logged In Successfully"
)
);
Finally, we send a 200 OK
response, setting the access and refresh tokens as HTTP cookies and including the logged-in user's data (without sensitive fields) in the JSON response.
This function, generateAccessAndRefereshTokens
, is responsible for generating access and refresh tokens for a user. Let's go through the code line by line to understand how it works.
javascriptCopy codeconst generateAccessAndRefereshTokens = async (userId) => {
try {
console.log(`Generating tokens for userId: ${userId}`);
The function takes a
userId
as its parameter.It starts by logging a message indicating that token generation has started for the given
userId
.
javascriptCopy code const user = await User.findById(userId);
if (!user) {
throw new ApiError(404, "User not found");
}
It uses
User.findById(userId)
to fetch the user from the database using the provideduserId
.If the user is not found, it throws a
404
error with the message "User not found".
javascriptCopy code const accessToken = user.generateAccessToken();
const refreshToken = user.generaterefreshToken();
If the user is found, it generates an access token by calling the
generateAccessToken
method on the user object.It also generates a refresh token by calling the
generaterefreshToken
method on the user object.
javascriptCopy code user.refreshToken = refreshToken;
await user.save({ validateBeforeSave: false });
The newly generated refresh token is then assigned to the
refreshToken
field of the user object.The user object is saved back to the database with the updated refresh token. The
validateBeforeSave: false
option skips any validation checks that might be defined in the Mongoose schema, which can be useful if you're only updating a single field and are confident in the document's validity otherwise.
javascriptCopy code console.log(`Tokens generated successfully for userId: ${userId}`);
return { accessToken, refreshToken };
} catch (error) {
console.error(`Error generating tokens for userId: ${userId}`, error);
throw new ApiError(500, "Something went wrong while generating refresh and access token");
}
};
If everything goes smoothly, it logs a success message indicating that tokens were generated successfully for the user.
It then returns an object containing the
accessToken
andrefreshToken
.If any error occurs during this process, it catches the error, logs an error message, and throws a
500
error with the message "Something went wrong while generating refresh and access token".
Conclusion
In this blog post, we have walked through the implementation of a secure and robust login function in a Node.js application. By following these steps, you can ensure that your application provides a seamless and secure authentication experience for your users. From validating user credentials to securely managing session tokens, this approach covers all critical aspects of user authentication.