NextJS JWT authentication boilerplate
Table of Contents
- 1. Introduction
- 2. Working demo
- 3. Description
- 4. Tech Stack
- 5. Getting Started
- 6. Authentication flow
- 7. Screenshots and short demo
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.
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
- NextJS v13, based on ReactJS v17
- TypeScript v4.8
- Chakra UI
- React Hook Form
- SWR (stale-while-revalidate)
- Prisma v4.6
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
.env.example
file to .env
and fill in the required environment variables
5.2.3. Copy the 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:
The chart is self-explanatory, but to better understand the flow, we can see the following steps:
-
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. -
The user cannot access the protected resources until the two-factor authentication in completed.
-
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 nametoken
. 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
-
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".
-
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.
useAuth
hook
6.1. The 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 roleaccessToken
: The JWT access token used to access the protected resourcesrefreshToken
: The JWT refresh token used to obtain a new access token when the current access token expiresisAuthenticated
: A boolean value that indicates if the user is authenticatedlogin(username: string, password: string)
: A function that can be used to authenticate userslogOut()
: A function that can be used to logout the userrefreshSession()
: 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
Login page:
Two-factor authentication pages:
Two-Factor authentication landing page: