Complete JWT (Json Web Token) Authentication Tutorial For Your Upcoming Full-Stack Project

Navraj SinghNavraj Singh
6 min read

Integrating JWT Authentication in Your Full Stack Project😙

So, you want to include JWT (JSON WEB TOKEN) Authentication in your next project for seamless and secure authentication? Here is the sweet and straight-to-the-point tutorial for you...

Pre-Requisites

  1. Your project setup.

  2. Your backend folder and its modules(routes, controllers etc.) set up and ready.

Step 1: Setting Up Your Project

Before diving into the code, ensure you have your project structure set up. This typically includes your backend server, routes, controllers, and a database. Make sure to install necessary packages like jsonwebtoken, bcryptjs, and express.

Step 2: Authentication Controller

In your authentication controller, you'll handle the user sign-in. Here, you'll validate the user's credentials, generate a JWT token, and send it back as a cookie for future authenticated requests. Here is the complete code.. dont worry below every line is explained in detail..

const signin = async (req, res, next) => {
  const { email, password } = req.body;
  try {
    const validUser = await userModel.findOne({ email });
    if (!validUser) return next(errorHandler(404, "User Email Not Found"));

    const validPassword = bcryptjs.compareSync(password, validUser.password);
    if (!validPassword) return next(errorHandler(401, "Wrong Credentials"));

    const token = jwt.sign({ id: validUser._id }, process.env.JWT_SECRET);

    // _doc means it is raw javascript object which is mongoDB document with no additional mongoose methods and data, bcuz mongoose attaches additonal information and methods to retrieved documents from mongoDB.
    const { password: hashedPassword, ...rest } = validUser._doc;
    const expiryDate = new Date(Date.now() + 3600000); // 1 hour

    // httpOnly:true option makes the cookie accessible only on the server side
    res
      .cookie("access_token", token, { httpOnly: true, expires: expiryDate })
      .status(200)
      .json({ rest });
  } catch (err) {
    next(err);
  }
};

Step By Step Explanation

  1. Here is the skeleton of the controller function, set this up..
const signin = async (req, res, next) => {

  try {

  } catch (err) {

  }
};
  1. Here this line of code just extract and de-structure the name and password the user will give us from "Request.Body"...
const signin = async (req, res, next) => {
  const { email, password } = req.body;
  try {

  } catch (err) {
  }
};
  1. Here these new 4 lines of code in try block are checking two things..

    1. Checking if the email the user gave us, exists in the database of MongoDB using Mongoose. IF the email do not exist the "IF" statement will return a response that "404, User Email is Not Found". The "errorHandler" function in code line "next(errorHandler(404, "User Email Not Found"));" is my custom errorHandler.... errorHandler function code available below...

    2. As of now if te user email exists in our database we move forward to 3rd line of code which is doing the password check.

      I am using a very popular library called bcryptjs to hash and encrypt the password before storing them into the database. so i used the de-structured password from req.body and give it to bcryptjs.compareSync() function/method to check if the password is correct by comparing database hashed password & password that the user gave is in req.body...

      if the password is correct then fine, but if not then do same thing,

      which is to make a "IF" statement to return a response that says (401, "Wrong Credentials")

const signin = async (req, res, next) => {
  const { email, password } = req.body;
  try {
    const validUser = await userModel.findOne({ email });
    if (!validUser) return next(errorHandler(404, "User Email Not Found"));

    const validPassword = bcryptjs.compareSync(password, validUser.password);
    if (!validPassword) return next(errorHandler(401, "Wrong Credentials"));
  } catch (err) {
  }
};
// This function comes from another module which exist 
// in a another folder called "utils" in my backend folder...
// Look you dont need to do this, you can return a simple res.json()
// response its completely fine for you to do that... 
const errorHandler = (statusCode, message) => {
  const error = new Error();
  error.statusCode = statusCode;
  error.message = message;
  return error;
};

module.exports = { errorHandler };
  1. Here is the moment where we use JWT Authentication.. you will be surprised it is only a single line of code..

    1.  const token = jwt.sign({ id: validUser._id }, process.env.JWT_SECRET);
      

      here jwt.sign() is used to make a "Token" it takes two parameters/arguments,

      jwt.sign( 1. A unique string(it must be unique every single time), 2. A key(just a random string of any text) ).

      for example,

      1. A unique string (1st argument) can be MongoDB's objectID which is automatically generated by MongoDB for every single document it contains, its always unique.

      2. A key (2nd argument) can be any random string, i my case i just randomly pressed keys on keyboard to make a 12-22 character long string. "asfasdfasdrowr3rjwer" like this. As "Key" is very important, it needs to be stored very securely, so i made a ".env" file and stored it there and then imported it using process.env.JWT_TOKEN

Now we have a token generated..

  1.  const { password: hashedPassword, ...rest } = validUser._doc;
     const expiryDate = new Date(Date.now() + 3600000); // 1 hour
    
     // httpOnly:true option makes the 
     // cookie accessible only on the server side.
     res
        .cookie("access_token", token, { httpOnly: true, expires: expiryDate })
        .status(200)
        .json({ rest });
    

    Here i just extracted and removed the password from the MongoDB document that i extracted using de-structured "email" from req.body

    Then i made a expiry date for my cookie.

    Then i just sent a response with a COOKIE, STATUS, JSON

    Which you can understand from code.

  2.  } catch (err) {
         next(err)
       }
     };
    

    here I just return a response if an error is "caught" in the catch block..

    that's it, that's all the code..

Step 3: Understanding the Code In Short

  • The code checks if the user exists in the database based on the provided email.

  • If the user doesn't exist, it returns a "User Email Not Found" error.

  • It then validates the password using bcrypt.js.

  • If the password doesn't match, it returns a "Wrong Credentials" error.

  • If everything checks out, it generates a JWT token using the user's ID and a secret key.

  • It then sends this token as a cookie named access_token with an expiry date of 1 hour.

Step 4: Enhancements and Best Practices

  • Security: Always store sensitive information securely, like your JWT secret. Keep it in environment variables.

  • Token Expiry: Consider shorter token expiry times for enhanced security.

  • HTTPS: Ensure your site is served over HTTPS to protect the token during transmission.

  • Error Handling: Implement consistent and clear error handling for a better user experience.

  • Validation: Validate user inputs thoroughly to prevent malicious attacks.

Step 5: Testing

  • Test your authentication flow rigorously to ensure it works as expected.

  • Verify edge cases, such as incorrect passwords or non-existent emails, to handle errors gracefully.

Step 6: Continuous Improvement

  • Keep exploring new authentication methods and security practices.

  • Stay updated with the latest developments in JWT and security to enhance your project's safety.

Feel free to customize this tutorial according to your project's needs and complexities.

My Github Link Here...

My Linked-In Link Here...

My Twitter(X) Link Here...

0
Subscribe to my newsletter

Read articles from Navraj Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Navraj Singh
Navraj Singh

Navraj Singh is a DevOps Enthusiast with knowledge of DevOps and Cloud Native tools. He is technical blogger and devops community builder. He has background of Backend Development in NodeJS ecosystem.