Implementing Safe User Login in Node.js Using JWT and Mongoose

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.

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 provided userId.

  • 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 and refreshToken.

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

Did you find this article valuable?

Support Akash Satpute by becoming a sponsor. Any amount is appreciated!