A Beginners guide to User Authentication and Authorization with JSON WEB TOKENS versus Sessions in NodeJs

A Beginners guide to User Authentication and Authorization with JSON WEB TOKENS versus Sessions in NodeJs

ยท

24 min read

In a web server, authentication (https://g.co/kgs/Yy9RH2) is used to verify a user in the client-side trying to access the database or the site. The server can achieve this in various ways but in most cases, the server uses the user's information like the username and password while authorization (https://g.co/kgs/7ryDCC) is a process by which a server determines if the client has permission to access or use a resource on a site. Authorization is usually done using sessions ( https://g.co/kgs/vZViVo ) or tokens.

User Authentication with sessions

Cookies and Sessions are used to store information. Cookies are only stored on the client-side machine, while sessions get stored on the client as well as a server. When a User logs in, the server creates a session for the user and stores it in the memory. The server then creates an id called session id which is stored in a cookie on the browser (https://en.wikipedia.org/wiki/HTTP_cookie). When the User makes a request to the server, the request is sent along with the cookie. The server then checks the session id and compares it with the session information saved in its memory. After the verification is completed by the server, the server responds with the data to the User.

Now let's see how User Authentication works with NodeJs!

Let's get started with the following steps:

  • Create a folder in your system.
  • Open the folder in your terminal.
  • Run npm init -y command to initialize your NodeJs application in that directory.
  • Run npm install express in the same directory to install the express framework for your app.

user_authentication terminal start.png

  • Then open the folder in your code editor.
  • Create a file and save it as server.js this file will contain the script to run your server.
  • In the server.js file, include the express module with the require() function and call the function
const express = require('express');
const app = express();
  • Next, set your app to listen on a port 3000
app.listen(3000);
  • Install a package called nodemon as a dev dependency for your app that allows your server to restart automatically when changes are made to your app, with this command npm install --save-dev nodemon
  • Next, in your package.json file, go to the scripts section and add the devStart key and nodemon with the server.js file as value that tells your server to start your app with nodemon
"devStart": "nodemon server.js"
  • Next, start your server by running npm run devStart in your terminal.

nodeauth 1.png

Your server starts running but returns nothing. Now let's create routes and start making requests to the server.

Your app requires that users login with their username and password

  • In the server.js file, create an array called users which represents your database that contains the credentials of your users as an object of username and password. This array of objects would contain the credentials of the users.
    const users = [
      {
          username: "Jordan",
          password: "jordan123"
      },
      {
          username: "kate",
          password: "kate1234"
      }
    ];
    
  • Next, create a route to this users JSON object using a GET request that responds with the users JSON object. To enable the use of JSON objects in your app, add the express.json() middleware with app.use to your server.js file.
app.use(express.json());

app.get('/users', (req, res) => {
    res.status(200).send(users);
});
  • To test this request, we will be using postman which can be downloaded from (https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop//%40).
  • Your server is still running, so you do not have to restart your server because you have nodemon installed.
  • Go to postman and on the drop down to the left, choose GET and set your url to http://localhost/3000/users and click on the Send button to send the request. You should get a response of the users object.

nodeauth postman.png

The passwords of the users are in plain text in the database which means anyone that gains access to your database can have access to all the passwords of your users and be able to do anything with it. To avoid this we use hashing which can be achieved by a library called bcrypt.

HASHING PASSWORDS WITH BCRYPT

There is a process of hiding the passwords for security reasons from been seen by anyone who gains access to your database and it is called Hashing. Hashing a password is the process of converting a password with a one-way algorithm and a hash function into a string that is difficult to be reversed. Bycrpt is a library designed for hashing passwords by combining a salt with the hashed password. A salt is random data that is used as an additional input to a one-way function that hashes data.

So in your app, install the bcrypt library with npm install bcrypt. After it's done installing, include it in your server.js file with the require() function

const bcrypt = require('bcrypt');

Using bcrypt requires two steps:

  1. Create a salt
  2. Then use the salt along with the password to create a hashed password.

To implement this, we need to create a route. So create a POST request to the users route.

app.post('/users', (req, res) => {}

Delete the objects in the users array in servers.js file and set it to an empty array. When you use the POST request, you will create a user that will be added to the empty array(the database).

So in your route handler for the POST request to the '/users route, create a user

app.post('/users', (req, res) => {
    const user = { name: req.body.name, password: req.body.password };
    users.push[user];
    res.status(200).send(users);
});

While your server is still running, go to postman and make this post request to this route http://localhost:3000/users, set the type of request to JSON(application/json) and in the body of the request include the JSON object of the name and password of the user. postman1.pngThe body of the request.

postman 2.png The body of the response.

You can see that the password is not hashed. To hash this password, you generate a salt using bcrypt in the route handler for the post request with bcrypt.genSalt() the genSalt() function accepts a parameter called rounds with the default of 10, which means the time it will take for the hashed password to be created. In your route handler use an async function because bcrypt is asynchronous and a try catch block and within the try block generate the salt and the hashed password. Replace the password with the hashed password and then return an error in the catch block if anything goes wrong.

app.post('/users', async(req, res) => {
    try {
        const salt = bcrypt.genSalt();
        const hashedPassword = bcrypt.hash(req.body.password, salt);
        console.log(salt);
        console.log(hashedPassword);
        const user = { name: req.body.name, password: req.body.password };
        users.push(user);
        res.status(200).send(users);
    } catch {
        res.status(500).send();
    }
});

Now create a post request on postman again and see that your response for password is now hashed! postman3.png Try making a GET request on postman and it will respond with the user's username and password will be hashed.

Now that you have successfully been able to hash the password of a user and get the information of the user. The next thing to do is to create the login route that will check/compare the user's information with that which was created and you can achieve this by using the bcrypt.compare() method.

In the login route, the first thing you will do is get the user then check if the user exists, if the user does not exist, throw an error. Use the try catch block as you did in the previous route '/users' for the POST request. In the try block, use the bcrypt.compare() methods that accepts two parameters; the first parameter is the password of the body of the request and the second parameter is the hashed password and compares them to see if they match, if they match, respond with success and if they do not match, the catch block will handle the error and respond with a message 'user not allowed'.

app.post('/users/login', async(req, res) => {
    const user = users.find(user => user.name = req.body.name);
    if(user == null) {
        return res.status(404).send("User does not exist");
    }
    try {
        if(await bcrypt.compare(req.body.password, user.password)) {
            res.send("Success");
        }
    } catch {
        res.status(500).send("user not allowed");
    }
});

To test this code on postman,

  • create a user with the POST request to http://localhost:3000/users
  • next, login the user with the POST request to http://localhost:3000/users/login After logging the user it responds with success because the password the user used to log in matched with the hashed password that was created when the user was created. postman4.png Now try changing the password of the user in the body of the login request, to something else and repeat the two steps above. It will respond with 'User does not exist' as specified in your catch block.

postman5.png

THE COMPARISON BETWEEN SESSIONS AND JSON WEB TOKEN FOR AUTHORIZATION

comparison_btw_JWT_and_sessions.jpg The above diagram is an illustration of authorization with session versus JSON WEB TOKENS(JWT) which shows the basic processes of how authorization works with sessions and JWT.

The image on the left represents authorization with sessions. First, the user creates an account and logs in with a username and password on the browser via the POST request. The server authenticates the requests like you just did above and will create a session for the user and store it in the memory. The server then creates an id called session id which is stored in a cookie on the browser (https://en.wikipedia.org/wiki/HTTP_cookie). When the User makes a request to the server, the request is sent along with the cookie. The server then checks the session id and compares it with the session information saved in its memory. After the verification is completed by the server, the server responds with the data to the User.

The image on the right represents authorization with JSON WEB TOKENS, the user creates an account and logs in with the username and password via the POST request to the server. After authentication is successful, the server creates a token called the JSON WEB TOKEN, this token is signed or serialized with a secret key by the server and is sent to the client. When the user makes a request to any page on the site, the request is sent alongside the JWT to the server on the Authorization header. Then the JWT signature gets the user information from the JWT and verifies it, then sends a response to the client.

The main difference between using Sessions for authorization and using JWT is that the session authorization stores the users information on the server while the JWT authorization stores the users information in the token on the client. The advantage of using JWT for authorization is the users information is stored in the token and therefore, in large scale websites, users do not have to log in again to access resources on the part of the site that uses another server. The secret key can be used for all the parts of the sites that use different servers unlike sessions.

THE DESCRIPTION OF THE STRUCTURE OF JWT

JWT has a structure consisting of three parts; the header, the payload and the verify signature. Below is a diagram which can be found on jwt.io that shows these three parts of JWT jwtnew.png To the left is the encoded token which is sent to and from the user and to the right are the three parts, the header consists of the algorithm for verifying the signature and the type of the token which is JWT.

The second part, the payload is the data of the user and it comprises of three parts; the reserved, the private and the public. The reserved part of the payload is the predefined and optional but recommended part that consists of information of the user like the exp(expiration date of the token), isa(issued at/manufacturing date of the token) and sub(subject) etc. The public part can be defined at will.

The third part, the verify signature is the part that takes the header and payload and base64URL encodes each of them together with a secret key and hash them with an alogrithm defined in the header.

THE IMPLEMENTATION OF JWT FOR AUTHORIZATION IN NODEJS

You have completed the authentication of the user above for your app, the next thing is the authorization of the user with JSON WEB TOKEN to enable or disable access to users logged in after authentication.

You will be creating the token In your server.js file, in the post request to the /users/login route.

app.post('/users/login', async(req, res) => {
    const user = users.find(user => user.name = req.body.name);
    if(user == null) {
        return res.status(404).send("User does not exist");
    }
    try {
        if(await bcrypt.compare(req.body.password, user.password)) {
            //start implementing JWT here
            res.send("Success");
        };
    } catch {
        res.status(500).send("user not allowed");
    }
});

To implement JWT in your app, the first thing you need to do is install the jsonwebtokens and dotenv libraries to be able to generate tokens and secret keys that will be used to serialize your user object.

STEPS FOR IMPLEMENTING JWT IN YOUR APP

  1. Install jsonwebtoken library that creates your tokens with npm install jsonwebtoken and include in your server.js file
    const jwt = require('jsonwebtoken');
    
  2. Install dotenvmodule that loads environment variables from a .env file into process.env with npm install dotenv and include in your app at the top in your server.js file
    require('dotenv').config();
    
  3. Create a .env file that will contain the environment variables and the secret key.

  4. Get the name from the body of the request in the /users/login route

    const name = req.body.name;
    
  5. Create the token withjwt.sign() which takes two parameters; the user object and the environment variable in theprocess.env that contains the secret key.

    const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
    

    The ACCESS_TOKEN_SECRET is the environment variable that will store the secret key. This variable is set in the .env file with a nodejs module called crypto. The crypto module in nodejs uses an algorithm that performs data encryption and decryption.

  6. In your terminal, while your server is running on one tab, open another tab and type node followed by this statement

    require('crypto').randomBytes(64).toString('hex')
    

    Here, you are using the require() function to get the crypto module and .randomBytes(64).toString('hex) means that the crypto module should generate a key of 64 hexadecimal strings as the secret token. Your output should look like this token key.png

  7. Copy this secret token and in your .env file, set the ACCESS_TOKEN_SECRET environment variable to the secret token.

    ACCESS_TOKEN_SECRET = 9f922829a8022703ed1f296752c1f99ffbc55baa3ed49570620dd9e1f339fbb0016f154f4832664e9f92446546dc0827e5ef4ba83d68589add414def7a5fa08c
    

    Then back in your server.js file, return the accessToken as a JSON obect in your server

    res.json({ accessToken: accessToken });
    
  8. Go to postman and create a user using the /users route with a POST request hashpost.png After making this request, this should respond with this below hashpost1.png Now login the user with the /users/login route with a POST request loginauth1.png and it should respond with your access token access1.png Summary: When the user logs in and is authenticated, the user sends the JWT in the Authorization Header.

  9. Verify the token that was sent by creating a middleware that will

    • get the token from the authorization header in req.headers['authorization']
    • check if the token exists and if not return an error of status 401 meaning the token doesn't exist, but if the token exists move unto the next step
    • verify the token with jwt.verify() that takes three parameters; the token, environment variable that contains the secret key and a callback function. The callback function has two parameters; the error and the user.
    • check if there is an error and return a status of 403 which means access to the requested resource is forbidden for some reason.
    • set the user on the request to the user
    • lastly call the next() function that allows you to use the middleware in your GET request to the /products route.

      function authenticateToken(req, res, next) {
      const authHeader = req.headers['authorization'];
      if (authHeader == null) {
         return res.sendStatus(401);
      }
      
      jwt.verify(authHeader, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
         if (err) {
             return res.sendStatus(403);
         }
         req.user = user;
      
         next();
      });
      };
      
  10. For instance, you have a products resource on your app that only users that are logged in and authenticated and verified can access that resource. So create and array of objects called products that will contain the username of the users logged in and their orders in an object.
    const products = [
    {
        name: "Debbie",
        orders: "Food"
    },
    {
        name: "Mike",
        orders: "Laptop"
    }
    ];
    
    Next, create a route that will handle GET requests for the products and include the middleware that verifies your token.
    app.get('/products', authenticateToken, (req, res) => {
    const {name} = req.user;
    const productUser = products.filter(product => products.name === name);
    if(!productUser) {
        return res.send("User has no product");    
    }
    res.json(productUser);
    });
    
    Here, you included the middleware and with that in your GET request to the products, you can now get the req.user that was defined in the middleware and store it in {name} using object destructuring. The next line uses the filter() function in JavaScript to find the user of the product that has the same name used to login. Then uses an if condition to check if that user exists and if the user doesn't match, it returns an error message "User has no product. If the user exists, it responds with the user as a JSON object.

To test this code, copy the token returned by the user and on postman create a GET request to this route http://localhost:3000/products and in the Headers - key, set it to Authorization and in the value, paste the token and click on Send. It should return this below in the body of the response last.png

Conclusion

Now you have successfully completed authentication for a user with bcrypt and authorization with JSON WEB TOKENS with dotenv and the crypto library for your app.