Generate JSON Web Token (JWT) for user authorization

This exercise walks through creating JSON Web Tokens for facilitating user authorization.

Objective

Create a system that generates JWT (JSON Web Token) for user authorization.

Description

In this issue, create a system that will generate JWT for user authorization. This will help to authorize that the user that sends the request to the server is the same user that actually logged in during the authentication process.

Acceptance criteria

  • Route handler /login should generate AccessToken and RefreshToken
  • The expiry period for the AccessToken should be 15seconds
  • Route handler /token should take the RefreshToken and create new tokens to keep the user authorized in the system.
  • Route handler /logout should delete the refresh tokens.

Solution

Introduction

JWT is for authorization- making sure that the user that sends the request to the server is the same user that actually logged in during the authentication process.

Make POST request with our email and password and we send that along to the server but instead of storing information on the server inside the session server, the server creates a JSON web token (JWT) and it encodes and serializes that and signs it with its own secret key so the server knows that if you tamper with it then it is invalid. Then it takes the JWT and sends it back to the browser. Nothing actually is stored on the server. The browser can then choose to store that however, it wants. The client then sends the request to the server and they make sure to include the JWT so that it knows what the user is authenticating with. The server signs the JWT with its own secret key and verifies that the JWT has not been changed since the time it had signed it. If JWT was not tampered the server deserializes the web token and knows that it is the correct user. And if the user is authorized to use the resource for which it made the request, the server will send the response back to the client.

The main difference between the session-based authorization and JWT is that the user information is stored on the server, so the server has to do the lookup to find the user based on the session ID.

JWT with node and express Install dependencies:

npm i express jsonwebtoken

Also, install a package dotenv that will allow storing the secret tokens

Create a file .env to store the secret tokens.

Create a file called auth.js and start by requiring express library and in order to set up an express server we need to get the app variable that is going to come from Express, so we can just call the express function and do app.listen(3000) and add the port on which we want to listen (port 3000 in our example).

const express = require("express");
const app = express();

app.listen(3000);

Now we have our application running on port 3000.

Ideally, we should use a database to store user information. But to keep it simple let’s create an array of users, which we will be using to authenticate them.

For every user, there will be the role – admin or member attached to their user object. Also, remember to hash the password if you are in a production environment:

const users = [
 {
   username: 'JohnDoe',
   password: 'smBHVpYgndW6G7AX'
 },
 {
   username: 'JimHudson123',
   password: 'wqEyKmsq47KCDr86'
 },
 {
   username: 'RyanTaylor',
   password: 'Q9g6WDHws6LuvUK8'
 }
];

The next thing we need to do is actually create a JSON web token and to do that we require the jsonwebtoken library that we had installed.

const jwt = require("jsonwebtoken");

Also since we are going to be passing JSON to this app we need to make sure our server can handle it.

app.use(express.json);

This lets our application use JSON from the body that gets passed up to it inside of the request.

Secret tokens are required to sign the JWT Add them to the .env files

ACCESS_TOKEN_SECRET=f3583d22973c927e58c22d9e43ab77260da5161a3c59d3759892a308e34eb26066f094553fb74b137e14ee4378ab488a8b54f116fe51eb90b912fcec6e2093d9

You should never share this secret, otherwise, a bad actor could use it to forge JWT tokens to gain unauthorized access to your service. The more complex this access token is, the more secure your application will be. So try to use a complex random string for this token:

Now we can create a request handler to handle the user login request. For that create a login route.

app.post("/login", (req, res) => {
  const { username, password } = req.body;

  const user = users.find((user) => {
    return user.username === username && user.password === password;
  });

  if (user) {
    const accessToken = generateAccessToken(user);
    res.json({ accessToken: accessToken });
  } else {
    res.json({ message: "login failed" });
  }
});

function generateAccessToken(user) {
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
}

In this handler, we have searched for a user that matches the username and the password in the request body. Then we have generated an access token with a JSON object with the username and the role of the user.

Our authentication service is ready. Let’s boot it up by running:

node auth.js

You can use POSTMAN to test the POST request. Send a POST request to http://localhost:4000 with Content-Type: application/json and content in the body:

{
  "username": "Jim",
  "password": "password123member"
}

You should get the accessToken as the response.

Access token

Refresh Tokens

At this point, our application handles both authentication and authorization, although there’s a major flaw with the design – the JWT token never expires.

If this token is stolen, then they will have access to the account forever and the actual user won’t be able to revoke access.

To remove this possibility, let’s update our login request handler to make the token expire after a specific period. We can do this by passing the expiresIn property as an option to sign the JWT.

function generateAccessToken(user) {
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15s" });
}

When we expire a token, we should also have a strategy to generate a new one, in the event of an expiration. To do that, we’ll create a separate JWT token, called a refresh token, which can be used to generate a new one.

First we will have to add another secret token to the .env file.

REFRESH_TOKEN_SECRET=f8b264d5ffb1c43fd286ce5d8a4b25d74f3468666362f62ba11c816681c7f09e91273e51d58f9691a3d5cf3b8c3edfd31c1ec3e2fe78d9c39ae645ca6bfb6188

Also, create an empty array in the auth.js file to store the refresh tokens

let refreshTokens = [];

Update the login route so that when a user logs in, instead of generating a single token, generate both refresh and authentication tokens:

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const user = users.find((user) => {
    return user.username === username && user.password === password;
  });

  if (user) {
    const accessToken = generateAccessToken(user);
    const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET);
    refreshTokens.push(refreshToken);
    res.json({ accessToken: accessToken, refreshToken: refreshToken });
  } else {
    res.json({ message: "login failed" });
  }
});

function generateAccessToken(user) {
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15s" });
}

Now, if you make the same request in postman you will get access and refresh tokens.

Refresh Tokens At this point, our application handles both authentication and authorization, although there's a major flaw with the design - the JWT token never expires. If this token is stolen, then they will have access to the account forever and the actual user won't be able to revoke access. To remove this possibility, let's update our login request handler to make the token expire after a specific period. We can do this by passing the expiresIn property as an option to sign the JWT. function generateAccessToken(user) { return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15s" }); } When we expire a token, we should also have a strategy to generate a new one, in the event of an expiration. To do that, we'll create a separate JWT token, called a refresh token, which can be used to generate a new one. First we will have to add another secret token to the .env file. REFRESH_TOKEN_SECRET=f8b264d5ffb1c43fd286ce5d8a4b25d74f3468666362f62ba11c816681c7f09e91273e51d58f9691a3d5cf3b8c3edfd31c1ec3e2fe78d9c39ae645ca6bfb6188 Also create an empty array in the auth.js file to store the refresh tokens let refreshTokens = []; Update the login route so that when a user logs in, instead of generating a single token, generate both refresh and authentication tokens: app.post("/login", (req, res) => { const { username, password } = req.body; const user = users.find((user) => { return user.username === username && user.password === password; }); if (user) { const accessToken = generateAccessToken(user); const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET); refreshTokens.push(refreshToken); res.json({ accessToken: accessToken, refreshToken: refreshToken }); } else { res.json({ message: "login failed" }); } }); function generateAccessToken(user) { return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "15s" }); } Now, if you make the same request in postman you will get access and refresh tokens.

Now, if there was a service for example /posts that required authorization of the user, it would work only for the first 15 seconds, after that it would expire and show a forbidden message.

We will create a new route for creating a new token based on the refresh token. This route checks if there was a refresh token created and passed to this route. If it succeeds it creates a new token and the user can stay authorized to use the service.

app.post("/token", (req, res) => {
  const { refreshToken } = req.body;

  if (refreshToken === null) {
    return res.sendStatus(401);
  }
  if (!refreshTokens.includes(refreshToken)) {
    return res.sendStatus(403);
  }
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.sendStatus(403);
    }
    const accessToken = generateAccessToken({ name: user.username });
    res.json({ accessToken: accessToken });
  });
});

Test by passing Refresh token

Test by passing Refresh token

But there is a problem with this too. If the refresh token is stolen from the user, someone can use it to generate as many new tokens as they’d like.

To avoid this, let’s implement a simple logout function. This will simply delete the refreshTokens from the array that we had initialized. In the production environment, this will be from a database.

app.delete("/logout", (req, res) => {
  const { refreshToken } = req.body;
  refreshTokens = refreshTokens.filter((token) => token !== refreshToken);
  res.json({ message: "successful logout" });
});

Testing logout - JWT token