3.4.1: JWT App

Learning Objectives

  1. Comprehend why developers may want to implement their own version of authentication as opposed to using auth0 or other third party systems.

  2. Understand the implementation of JWT within a React and ExpressJs Application context.

Introduction

When developing your own authentication system there are a few approaches that one could take. JSON Web Token or JWT is one such approach. To authenticate a user a clients application must send a JWT in the authorisation headers of the HTTP request to the Applications backend server. The backend Application's API will validate the request and token using middleware functions. JWT's allow developers to define a compact and self-contained way to securely transmit information between Frontend and Backend of the application. We use JWT tokens to authorise users within our application, when the user signs up and logs in they will be granted a JWT token which is used within the application to verify the user within the application. When a user is signed out, the token can be invalidated to ensure the security of application.

Look into the plethora of JWT implementations here.

Auth0 or Custom implementation?

Leveraging a tool like Auth0 has many benefits first and forth most is Customisation as well as control, when building your own system you have full control over the features that are implemented. This level of customisation allows developers to build unique features like integration with an existing user database, granular access control or specific security implementations. When you have existing infrastructure such as a user database creating your own system means that you can easily integrate with them. An additional benefit of creating your own authentication system is that you have direct control over the security measures and protocols that are put into place. This gives you complete control over user data and privacy. That being said using a third-party authentication service like Auth0 does offer several advantages, you will reduce your development time, there are pre-built integrations offered by Auth0 and it contains robust security features. At the end of the day the decision to leverage a third-party authentication system or build your own depends on the projects requirements, how long the development cycle is and how much customisation and control you need.

Backend Implementation

When implementing JWT Authentication on an ExpressJs backend it is important to leverage certain tools such as bcrypt, to help you hash user passwords, jsonwebtoken, which provides the means to generate, verify and validate JWTs. When used in conjunction with a database system one could develop their own version of JWT authentication and have complete control over the backend authenticating and authorisation logic.

The ExpressJS backend application will need to handle HTTP requests such that users can sign-in, sign-up and sign-out of the application, additional requests could include forgotten passwords and even alter emails. In addition to this, we should set up token expiration and refresh mechanisms to prevent unauthorised access as well as enhance security.

Note that the backend system is connected to your Frontend Application, this is done through HTTP requests that are shared between the communicating applications.

Here is an example of how you can sign a JWT within your backend, this token must be sent to the Frontend in order for user verification. Note that this example uses a .env to store its secrets, as we are using ExpressJS we prefix the environmental variables with process.env.

Token Generation
const jwt = require("jsonwebtoken");

const newAuthToken = (payload, refreshToken = false) => {
  const secretKey = refreshToken
    ? process.env.REFRESH_JWT_SECRET
    : process.env.JWT_SECRET;

  const expiresIn = refreshToken
    ? process.env.REFRESH_JWT_EXPIRES_IN
    : process.env.JWT_EXPIRES_IN;

  return jwt.sign(payload, secretKey, { expiresIn });
};

module.exports = newAuthToken;

Here is an example of a JWT middleware for an ExpressJs backend, it can be imported and used within the middleware chain to verify a users JWT token within their request. This is a vital implementation to ensure that only logged in users can alter and interact with the database. Notice that in the example above we are signing the JWT, while in the code below we are verifying it using methods from the jsonwebtoken package we just use the package for generating and verification of JWT's, not storage of our user data/ JWT information.

Auth Middleware
const jwt = require("jsonwebtoken");

const jwtAuth = (req, res, next) => {
  try {
    const accessToken = req.headers.authorization.split(" ")[1];
    
    if (!accessToken) {
      return res.status(401).json({
        error: true,
        msg: "Error: missing or invalid access token.",
      });
    }

    const user = jwt.verify(accessToken, process.env.JWT_SECRET);
    req.user = user;
    next();
  } catch (error) {
    return res.status(403).json({
      error: true,
      msg: "Error: unauthorised access, invalid token.",
    });
  }
};

module.exports = jwtAuth;

When signing up a user developers need to perform a number of tasks. First storing the user information within the database, provided that user doesn't exist, then leverage the jsonwebtoken package to create tokens that can be associated into the database with the current user. Then finally sending the user and token back as the response to this HTTP request. Notice how there are error handlers set up that respond or are triggerred by different issues.

Example Signup Func
signUp = async (req, res) => {
  const { firstName, lastName, email, password, profilePicture } = req.body;

  if (!firstName || !lastName || !email || !password || !profilePicture) {
    return res.status(400).json({
      error: true,
      msg: "Error: Please fill in all required fields and try again.",
    });
  }

  try {
    // check if user exists
    const existingUser = await this.model.findOne({
      where: { email: email },
    });

    if (existingUser) {
      return res.status(400).json({
        error: true,
        msg: "Error: An account with this email address already exists.",
      });
    }

    // step 1: hash password
    const hashPassword = await bcrypt.hash(password, saltRounds);

    // step 2: create new user
    const newUser = await this.model.create({
      firstName: firstName,
      lastName: lastName,
      email: email,
      password: hashPassword,
      profilePicture: profilePicture,
    });

    // step 3: create a payload for jwt
    const payload = {
      id: newUser.id,
      email: newUser.email,
    };

    // step 4: create tokens
    const accessToken = generateAuthToken(payload);
    const refreshToken = generateAuthToken(payload, true);
    const verificationToken = generateEmailToken();
    // step 5: update user info to include refresh token
    await this.model.update(
      {
        refreshToken: refreshToken,
        verificationToken: verificationToken,
      },
      { where: { id: newUser.id } }
    );

    const user = await this.model.findByPk(newUser.id);

    return res.status(200).json({
      success: true,
      data: { user, accessToken },
      msg: "Success: Your account has been created successfully.",
    });
  } catch (error) {
    return res.status(400).json({
      error: true,
      msg: "Error: Oops! We stumbled upon an issue while setting up your account. Please refresh the page and try again.",
    });
  }
};

Frontend Implementation

To implement JWT Authentication within a React Frontend context it is necessary to integrate packages such as axios into your application to make the HTTP requests to faciliate user sign-in, sign-up and sign-out. Moreover its vital to create the forms to capture user authentication data such as user emails and passwords. After this is completed, we need to consider where we could store our token, it might be possible within a secure cookie or even browser localstorage.

Upon successful login, if users want to access JWT pages then the Frontend application will need to verify that the current user has a signed and valid JWT, if the token is present then the user will gain access to the Frontend components protected by authenticating logic. If users are making requests to alter information in the ExpressJs backend then the users requests must include the JWT token for the Backend API to validate, if the token is authenticated successfully then the user will gain access to protected routes and alterations could be made. Otherwise an error message might be the response resulting in an error handler being fired off in the frontend.

The code below is an example of how we might send the signup request from a React Frontend that is powered by Vitejs, notice how are are importing the environmental variables using import.meta.env.VITE_SOME_ prefix.

const sendSignupInformation = async () => {
  ...
  const response = await axios.post(`${import.meta.env.VITE_SOME_DB_API}/auth/signup`,
        {
            firstName: firstName,
            lastName: lastName,
            email: email,
            password: password,
            profilePicture: url,
        }
    );
   ...
  }

Once the the React Application receives a successful response it is possible to store the received access token in a secure cookie or the browsers local storage. Use this access token to validate that a user has been logged in and setup the required statful logic to denote that a user has logged in a boolean state can suffice. To setup protected components on the Frontend using React Router follow the Private Routing section.

After a user has logged in, every subsequent request that is made to a protected backend route needs to contain their most recent access token to validate that they are actually logged in. In this example the developers store the access token into local storage.

Update Profile Request
const handleUpdateProfile = async (inputValue) => {
    // Get the users access token from local storage
    const accessToken = localStorage.getItem("accessToken");
    try {
    // Generate an axios request to the backend
      const response = await axios.post(
        `${import.meta.env.VITE_SOME_DB_API}/user/profile`,
        {
          profilePicture: inputValue.url,
          firstName: inputValue.firstName,
          lastName: inputeValue.lastName
        },
        // place the access token within the Authorization header, as a Bearer token
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );
      ... // Reset form logic
    } catch (error) {
      //Notify the user if there is an error using a toast message
      toast.error(`${error.response.data.msg}`);
    }
  };

Additional features for consideration

There are various additional features that could be implemented when setting up a custom version of authentication. One of the most important features that can be implemented on-top of basic authentication is an email verification system. If you are interested in setting up a password recovery system or invitation emails you could consider using nodemailer, to power your Express application to send emails to your users. The email verification system can power your application to support forgotten passwords for your users, that respects your authentication flow and is all controlled by your backend.

Last updated