Sign in
Log inSign up
JWT Authentication In 4 Easy Steps

JWT Authentication In 4 Easy Steps

JWT Authentication is the most popular way to set up user authentication in your app, In this blog, we'll learn how to integrate JWT in just 4 steps.

ayan shukla's photo
ayan shukla
·Jul 20, 2021·

9 min read

Let's face it, security on the front-end is similar to hanging curtains on the front door and not expecting anyone to break in. Security on the client’s side can be deobfuscated, modified and tampered by a user. If you genuinely want to protect your user's personal data and credentials, you need to secure your routes on the server.

JSON Web Token

So what is JWT ? According to the official documentation

JSON Web Token (JWT) is a JSON encoded representation of a claim(s) that can be transferred between two parties. The claim is digitally signed by the issuer of the token, and the party receiving this token can later use this digital signature to prove the ownership of the claim.

In this blog, I will be showing four easy steps to secure private routes with the help of JWT Authentication. Let's go !!

1. Setting Up a Node JS server

First step would be to spin up a NodeJS server using express JS.

Let's create signup and login routes for the user and hook them up with their respective callback functions. Both the routes will be making a POST request coming from the client side.

//index.js
const express = require('express');
const {loginUser, signupUser} = require('./auth');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello Express app!')
});

app.post('/signup', signupUser);
app.post('/login', loginUser);

app.listen(3000, () => {
  console.log('server started');
});

Now that we have set up the routes for user signup and login, we can move on to figuring out logic for each action.

2. Registering the User

User sign up on the client’s side is a simple process. The user will be asked to enter their Username, Email & a Password. Upon the click of the sign up button, a POST request is made to the server to their respective route and then further actions are performed.

In this example, the user will be making a POST request to the /signup route that we have created on our server in the first step. The incoming request contains a payload consisting of :

  • Username
  • Email
  • Password

This payload will be coming as a request body and can be accessed through req.body in the function that we will be creating for user signup which will be called every time a request to /signup route is made.

Hashing password with BcryptJS

Before we move ahead and create a userSignup function, let us take a moment to understand what password hashing is.

Saving passwords in plain text on the Database would not be a wise thing to do as it has chances of getting exposed to any employee or attackers who may have malicious intent and can misuse the user's sensitive data.

Passwords are normally hashed and then stored in the Database. One such package we can use to hash and salt passwords is Bcrypt js.

bcrypt.hash(password, 10, async (err, hash) => {
        if (err) {
          res.status(500).json({ success: false, error: err });
        } else {
          const userCreds = new User({
            name,
            email,
            password: hash
          });
          const user = await userCreds.save();

bcrypt.hash takes the plain text password that we receive in the post request along with email and username while signing up for a user, it takes that password and puts it through 10 rounds (You can put any number) of Salting.

Salting basically adds another level of protection by adding a random bunch of strings to our hashed password on random places of the password string, so that It becomes harder for attackers to decrypt the password.

Note - Hashing is a one way process, meaning, once the password is hashed It cannot be reversed.

The parameters in the callback function of bcrypt.hash contains an error and the hashed password, depending upon the success the function will return either the hashed password or an error. When the hashed password is returned, it is ready to be stored in the database along with other information of the user such as name and email.

Creating userSignup function

We will write this function in the /auth.js file that we have created on our server.

//auth.js
exports.signupUser = async (req, res) => {
    const { username, email, password } = req.body;
  try {
      bcrypt.hash(password, 10, async (err, hash) => {
        if (err) {
          res.status(500).json({ success: false, error: err });
        } else {
          const userCreds = new User({
            name,
            email,
            password: hash
          });
          const user = await userCreds.save();
          res.status(201).json({
            success: true,
            message: "Acount Created Successfully"
          });
        }
      });

  } catch (error) {
    res.json({ success: false, message: "some error occured" });
  }
}

Starting off by de-structuring the items we have received in the request body from the client side. Then we will be hashing the password we received from the user with bcrypt.js. The hashed password is stored in the User document in the database along with name and email.

Upon success, the user is sent the response with a success message that they have successfully registered as a user.

3. Signing JWT upon Login

This is the juicy part of the entire process, In the login function, we will be doing two most important things :

  • Comparing password through bcrypt
  • Signing JWT and sending it as a response

Login of a user process is very similar to the signing up process on the client side, user will make a POST request to the /login route along with the email and password that they have signed up with in the second step. The request will be handled on our server.

The existence of the email is normally checked first, if it is not present in the database, the user is sent an error saying email doesn't exist, please sign up.

Let's write loginUser function.

  exports.loginUser = async (req, res) => {
  const { email, password } = req.body;
  try {
    const foundUser = await User.findOne({ email: email });
    bcrypt.compare(password, foundUser.password, (err, result) => {
      if (err) {
        return res.status(401).json({
          success: false,
          message: "Authentication Failed"
        });
      }
      if (result) {
        const token = jwt.sign(
          { userId: foundUser._id },
          process.env.JWT_KEY,
          { expiresIn: "24h" }
        );

        const user = {
          token,
          name: foundUser.name
        };

        return res
          .status(200)
          .json({ success: true, message: "Auth Successful", user });
      }

      res
        .status(401)
        .json({ success: false, message: "Password is Incorrect !" });
    });
  } catch (error) {
    res.status(401).json({ success: false, message: "Authentication Failed" });
  }
};

As mentioned above, the first step will be to compare the received plain text password with the hashed password that exists on our database. User email from req.body will be used to index users from the database.

Once the user is found, we have the access to the found user's name, email and password in the foundUser object

While the process of hashing is done on sign up of a user, password comparison is done at the time of login in of the user.

Bcrypt.compare is a method that we will be using to compare the hashed password that we get from foundUser object, with the user entered plain text password that comes in the request body.

Bcrypt compare takes three parameters

  • Plain text password from req.body
  • Hashed password from user found in the database
  • Callback function that returns error or result

When the user entered password is successfully matched with the hashed password stored in the database, the result argument returns true which is a greenlight to generate and sign a JWT with user info and finally send it back to the client in the response.

Generating the Token

jwt.sign is a function used to generate a token, it takes the payload, secret key and options as its arguments. For the payload we will be providing the userID of the found user from the database.

One important thing to remember while generating the token is to never put private data in the payload, something like userID or an email is fine, but passwords or sensitive data should not go in the payload, because any user can see what's inside the payload when the token is generated at jwt.io

const secret = "mysecretkey";
const token = jwt.sign({ userId: foundUser._id }, secret, {expiresIn: "24h"});

Here we are generating a token with our secret key and options have an expiration time until which token is valid.

The generated token will then be sent back to the user as a response right after successful login, and with that token the user will be able to make API requests to the secured routes on the server. The token will be attached to every incoming request in the request header from the client side, and hence, it will be verified on each request to the server that requires user data.

We will be looking at how the token is verified in the next step.

Verification of the Token

In the above step, we learned the process of signing a token. Now we will look at how a signed token payload is verified on the server.

  • When a signed token is sent with the API request from the app, the token sits inside the authorization header of the request.
  • the signed token is then extracted on the server and verified using jwt.verify
const secret = 'mysecretkey'
const verifyToken = (req, res) => {
  try {
    const token = req.headers.authorization;
    const decoded = jwt.verify(token, process.env.JWT_KEY);
    req.user = decoded;
  } catch (err) {
    res.status(401).json({ success: false, message: "Unauthorized Access" });
  }
};
  • The verification process is simple, the method we are using is Verify using a symmetric key : jwt.verify(token, secret) This method takes two arguments- Token that we received in the request, and Secret that we have provided while signing the token. The condition is that the token should have been signed with the same secret key (which is stored on the server) as it is being verified with, only then it will decode the signed token payload and upon successful verification, jwt.verify removes the headers and signature of the signed Token and outputs the original JWT payload, which is : userID: "1234" in this example.
  • We can put this userID in the request object so that It can be accessed on any route we need it in.

If the signed token payload is verified then we can proceed further with the request, else throw an error if the token is not verified and authentication has failed.

Conclusion

With JWT authentication we essentially verify on each API request to the private route, whether the token is correct or not OR the user is authenticated or not.

Few examples of protected routes are "cart", "wishlist", "orders", "playlist" etc. These routes are only supposed to be shown to the user when they are logged in to the app OR they are authenticated.

With this, we have successfully integrated JWT authentication with our app 🎉

I hope this blog helped you to integrate JWT into your NodeJS REST API. If you have any queries, please contact me at twitter.com/AyanShukla4

Hassle-free blogging platform that developers and teams love.
  • Docs by Hashnode
    New
  • Blogs
  • AI Markdown Editor
  • GraphQL APIs
  • Open source Starter-kit

© Hashnode 2024 — LinearBytes Inc.

Privacy PolicyTermsCode of Conduct