/nextjs-jwt-auth-boilerplate

NextJS + ChakraUI + Prisma + JWT-based Authentication + Two-Factor Authentication + Sentry

Primary LanguageTypeScript

NextJS JWT authentication boilerplate

Table of Contents

1. Introduction

This project was developed to show an example of JWT token-based authentication in a Web environment for a research project at the University of Applied Sciences of Southern Switzerland. You can find the related paper here

2. Working demo

Based on this boilerplate, I developed a PWA based on the Chinook database that allows an Employee to view a list of his or her customers.

Demo project link

3. Description

This NextJS 13 boilerplate comes with a fully functional two-factor authentication system based on JWT tokens.

Main features:

  • Sentry error tracking
  • Fully-typed with TypeScript
  • Login with email and password (hashed with bcrypt)
  • Role-based access control (by default: User, Admin)
  • Automatic JWT access token refresh
  • Two-factor authentication via email
  • Front-end useAuth hook to easily manage the user session
  • User session persistence via cookies and local storage
  • New flexible back-end middleware management system
  • Protected routes and pages

4. Tech Stack

5. Getting Started

5.1. Prerequisites

  • Node.js v14.17.0 or higher
  • Yarn v1.22.10 or higher
  • PostgreSQL v13.3 or higher

5.2. Configuration

5.2.1. Install required packages

yarn install

5.2.2. (Optional) Create a new PostgreSQL container with Docker

docker run --name nextjs-jwt-auth -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres

5.2.3. Copy the .env.example file to .env and fill in the required environment variables

cp .env.example .env.local 

5.2.4. Push database schema and seed data to the database

yarn prisma db push && yarn prisma db seed

5.2.5. Start the development server a

yarn dev

6. Authentication flow

The authentication flow is the following:

Authentication flow

The chart is self-explanatory, but to better understand the flow, we can see the following steps:

  1. The user sends a request to the /api/login endpoint, submitting the E-Mail address and password of the user in the request body. The server then validates the user credentials and, if valid, generates a JWT access token and a JWT refresh token. The access token is used to access the protected resources, while the refresh token is used to obtain a new access token when the current access token expires. The access token is sent in the response body, while the refresh token is sent in the response cookies. After that, the server sends an E-Mail to the user with a token used as OTP (one-time password) for the two-factor authentication.

  2. The user cannot access the protected resources until the two-factor authentication in completed.

  3. The user sends a GET request to /api/two-factor passing the two-factor authentication code sent via E-Mail in the request query with name token. The server validates the token and, if valid, the is authenticated and can access the protected resources using the access token generated in step 1. This is a sample two-factor authentication URL:

https://my-awesome-app.com/two-factor?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJsdWNhNjQ2OUBnbWFpbC5jb20iLCJuYW1lIjoiSmFuZSIsInN1cm5hbWUiOiJXaGl0ZSIsInJvbGUiOiJBRE1JTiIsImlhdCI6MTY2OTU0Mjk3MiwiZXhwIjoxNjY5NTQzODcyfQ.swKDoKXq72NOzOmRj781_X1EiH2pw2F-BEJiMXkE8xI
  1. The user can now access the protected resources using the access token generated in step 1. The access token is stored inside a cookie named "token".

  2. When the access token expires, the user can obtain a new access token by sending a POST request to the /api/refresh endpoint with a payload containing the refresh token.

{
  "refreshToken": "my-secret-refresh-token"
}

The server then validates the refresh token and, if valid, generates a new access token and updates the user's access token cookie value using Set-Cookie header. This process is done automatically inside the NextJS application by the useAuth hook.

6.1. The useAuth hook

The useAuth hook is a React hook that can be used to easily manage the user session. It is used to authenticate users, to get the user session information, to refresh the access token, to logout users, and to check if the user is authenticated.

The useAuth hook is defined in the providers/auth/AuthProvider.tsx file and is used in the pages/_app.tsx file to wrap the entire application. This is the list of the features provided by the useAuth hook:

  • currentUser: The current user session information, such as the user ID, E-Mail address, name, surname and role
  • accessToken: The JWT access token used to access the protected resources
  • refreshToken: The JWT refresh token used to obtain a new access token when the current access token expires
  • isAuthenticated: A boolean value that indicates if the user is authenticated
  • login(username: string, password: string): A function that can be used to authenticate users
  • logOut(): A function that can be used to logout the user
  • refreshSession(): A function that can be used to refresh the user session

6.2. Route protection

To protect access to the protected resources, have been used two different approaches:

  • Middleware (/middleware.ts) that check if the user has set the access token in the cookies and, if not, redirects the user to the login page

  • Server-side rendering (SSR) function that checks the user's access token validity and, if not valid, redirects the user to the login page

6.3. JWT tokens

The JWT access token and the JWT refresh token have the following payload:

{
  "sub": <user id>,
  "email": <user email>,
  "name": <user first name>,
  "surname": <user last name>,
  "role": <user role: ADMIN or USER>,
  "iat": <issued at timestamp>,
  "exp": <expire at timestamp>,
  "iss": "${APP_URL}", // ENVIRONMENT VARIABLE
}

The JWT access token expires after 15 minutes, while the JWT refresh token expires after 30 days. Both tokens are signed using different secret keys to increase security.

Both tokens shares the same payload structure to permit the server to do extra checks on the token validity. If the user saved in the database is not the same user that is present in the token payload, the token is not valid. This has been done to prevent the user from using a token with old / invalid user information.

7. Screenshots and short demo

Demo video using two accounts at the same time:

demo.mp4

Dashboard page: Dashboard page

Login page:

Login page

Two-factor authentication pages:

Two-Factor authentication landing page: Two-Factor landing page

Two-Factor authentication success page: Two-Factor landing page

Two-Factor authentication error page: Two-Factor landing page