author-mobile

By Nicholas Rowe,

September 18, 2024

Implementing Refresh Tokens with Next.js and NestJS

Ensuring secure and seamless user authentication is paramount in the ever-evolving web development landscape. A widely adopted approach involves using access tokens and refresh tokens to manage authentication. Let’s walk through implementing refresh tokens in a full-stack application using Next.js for the frontend and NestJS for the backend.

Why Refresh Tokens?

Access tokens typically have a short lifespan for security reasons. When an access token expires, the user must re-authenticate to continue using the application. Refresh tokens solve this problem by allowing the application to obtain a new access token without requiring logging in again. This provides a better user experience while maintaining security.

Setting Up the Backend with NestJS

We’ll start by setting up the backend with NestJS, where we’ll handle the creation and validation of access and refresh tokens.

1. Installing Dependencies

First, set up a new NestJS project:

npx @nestjs/cli new auth-backend
cd auth-backend

Next, install the necessary dependencies:

npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs

2. Creating the Auth Module

Create an auth module to encapsulate the authentication logic:

npx nest g module auth
npx nest g service auth
npx nest g controller auth

3. Implementing JWT Strategy

In auth.service.ts, implement the methods to generate access and refresh tokens:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload, { expiresIn: '15m' }),
      refresh_token: this.jwtService.sign(payload, { expiresIn: '7d' }),
    };
  }

  async refreshToken(refreshToken: string) {
    const payload = this.jwtService.verify(refreshToken);
    return {
      access_token: this.jwtService.sign({ username: payload.username, sub: payload.sub }, { expiresIn: '15m' }),
    };
  }
}

In this code snippet, the login method generates both an access token and a refresh token. In contrast, the refresh token method allows the user to obtain a new access token using the refresh token.

Setting Up the Frontend with Next.js

Next, we’ll set up the frontend using Next.js, which will handle authentication and token management.

1. Install Dependencies

If you don’t have a Next.js project set up yet, create one:

npx create-next-app auth-frontend
cd auth-frontend

Install Axios for making HTTP requests:

npm install axios

2. Implementing Token Handling

Create an Axios instance in your Next.js application to configure API_URL and handle JWT tokens, including access and refreshing tokens. Here’s a basic example using Axios Instance:

import { logout, refreshToken } from "@/api/auth";
import { IAxiosResponse, ServerResponse } from "@/interface/http";
import axios, {
 AxiosError,
 AxiosResponse,
 CreateAxiosDefaults,
 InternalAxiosRequestConfig,
} from "axios";
import { redirect } from "next/navigation";
import { Cookies } from "react-cookie";

const cookie = new Cookies();

const config: CreateAxiosDefaults<any> = {
 // Configuration
 withCredentials: true,
 baseURL: `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1`,
 timeout: 1000 * 60 * 5,
};

const axiosClient = axios.create(config);

export const axiosAuth = axios.create(config);

axiosClient.defaults.headers.common["Accept"] = "application/json";
axiosClient.defaults.headers.common["Content-Type"] = "application/json";
axiosClient.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
axiosClient.defaults.headers.common["X-Requested-Store"] = "default";

let requestTime = 0;
let authToken: string = "";

// Config interceptor here
axiosClient.interceptors.request.use(
 async (config) => {
   const authToken = cookie.get("access_token") || "";
   if (authToken) {
     config.headers.Authorization = `Bearer ${authToken}`;
   }
   return config;
 },
 (error) => {
   return Promise.reject(error);
 }
);

interface ICustomResponse extends AxiosResponse {
 data: ServerResponse;
}

axiosClient.interceptors.response.use(
 (res): ICustomResponse => ({
   ...res,
   data: res.data as ServerResponse,
 }),
 async (error: AxiosError) => {
   try {
     if (error?.response?.status === 401) {
       // if (requestTime >= 6) {
       //   return;
       // }

       const refresh = await refreshToken().catch(async (err: AxiosError) => {
         if (err.response?.status === 400) {
           await logout().then(() => {
             cookie.remove("access_token");
             // window.location.href = "/auth/login";
           });
           // await redirect("/login");
         }
       });
       if (refresh?.access_token) {
         cookie.set("access_token", refresh?.access_token);
       }
       const originalRequest: InternalAxiosRequestConfig<any> =
         error.config as InternalAxiosRequestConfig<any>;
       if (refresh?.access_token) {
         originalRequest.headers[
           "Authorization"
         ] = `Bearer ${refresh?.access_token}`;

         axiosClient.defaults.headers.common[
           "Authorization"
         ] = `Bearer ${refresh?.access_token}`;
       }
       // requestTime += 1;
       return axiosAuth(originalRequest);
     } else {
       if (
         error?.response?.status === 401 &&
         (error?.response?.data as { message: string })?.message !==
           "Please login"
       ) {
         // await logout().then(() => {
         //   cookie.remove("access_token");
         //   window.location.href = "/login";
         // });
       }
     }
   } catch (refreshError) {
     // Handle refresh token error
     console.error("Error refreshing token:", refreshError);
     throw refreshError;
   }
   return Promise.reject(error);
 }
);

export default axiosClient;

Axios Client Configuration

  • The Axios library creates an axiosClient and axiosAuth instance with a base configuration that includes credentials and a base URL for API requests.
  • We configure the axiosClient with default headers, such as Accept, Content-Type, X-Requested-With, and X-Requested-Store.
  • The axiosClient intercepts requests to automatically include an Authorization header with a bearer token (if available) from cookies.

Request Interceptor

  • Before any request is sent, the request interceptor checks if an access_token is present in the cookies.
  • If an access_token is found, it’s added to the request headers under Authorization as a Bearer token.

Response Interceptor

  • The response interceptor handles the server’s responses, specifically focusing on error handling, particularly for 401 Unauthorised responses.
  • When the system detects a 401 status, indicating that the access token is invalid or expired, the interceptor triggers the refresh token workflow.

Refresh Token Workflow

  • Triggering Refresh: If the system encounters a 401 Unauthorized error, it attempts to refresh the token by calling the refreshToken() function.
  • Handling Refresh Errors: If the system fails to refresh the token (e.g., if the refresh token is also invalid or expired), it logs the user out. The system calls the logout() function, removes the access_token cookie, and redirects the user to the login page (this part of the code is commented out, but it’s implied that this action will be taken)
  • Updating Tokens: If the system successfully refreshes the token and obtains a new access_token, it stores the token in the cookies and updates the Authorization header in both the original request and the Axios client’s default headers.
  • Retrying Original Request: The original request that triggered the 401 error is then retried with the new access_token. This is achieved by sending the original request again using axiosAuth(originalRequest).

Refresh Token Workflow

  • If any error occurs during the refresh token process (for example, if the refresh token is invalid or the server is unreachable), it’s caught, logged, and the error is re-thrown.

Saigon Digital Case Study: Implementing Refresh Tokens for Visa Hotel Now and BVIS

At Saigon Digital, we’ve successfully implemented refresh token systems in various client projects, enhancing security while ensuring a seamless user experience. Two notable projects where we applied this approach are Visa Hotel Now and BVIS.

Visa Hotel Now

Visa Hotel Now is a dynamic platform designed to streamline hotel booking processes for travelers. Given the sensitivity of user data and the necessity for a smooth booking experience, implementing refresh tokens was crucial. The refresh token system ensured that users could stay logged in during their entire browsing and booking experience without the need for frequent re-authentication, which could disrupt their flow. The solution also provided a secure mechanism to issue new access tokens without compromising user security.

BVIS (British Vietnamese International School)

For BVIS, a platform used by students, teachers, and administrators, maintaining secure and continuous access to the system was a top priority. The refresh token implementation at BVIS allowed users to remain authenticated for extended periods, supporting continuous access to educational resources and administrative tools. By managing the lifecycle of access tokens effectively, the system minimized security risks while providing a frictionless experience for all users.

These case studies highlight Saigon Digital’s expertise in implementing refresh token systems that balance security and usability. Whether it’s a high-volume hotel booking platform or a comprehensive educational system, our solutions ensure that users enjoy a seamless experience without compromising on security. For businesses looking to enhance their authentication systems, Saigon Digital offers tailored solutions that meet the unique needs of each project.

Conclusion

Implementing refresh tokens in a Next.js and NestJS application enhances security while providing a seamless user experience. By securely managing tokens and refreshing them as needed, your application can maintain high security standards without compromising usability.

Saigon Digital helps you build secure, scalable, and user-friendly applications. For more expert advice and services, connect with us today!

author-avatar
author-avatar

About the Author

Nicholas Rowe

As the CEO and Co-Founder of Saigon Digital, I bring a client-first approach to delivering high-level technical solutions that drive exceptional results to our clients across the world.

I’m interested in...

Give us some info about your project and we’ll be in touch

loading