Hamza Bargaz.

How to Configure Custom Auth Token Refresh for Next.js 14 Server Components

How to Configure Custom Auth Token Refresh for Next.js 14 Server Components

in modern web development, Security in the processing of authentication is critical to current web development. This is made simple with Next.js, which includes an integrated authentication system that interfaces with server-side components.

In this article, we'll look at how to set up custom authentication for Next.js 14 server components and ensure secure data fetching.

Custom Authentication Overview

Next.js offers a built-in authentication mechanism, but there are times when custom solutions are necessary. such as integrating with third-party authentication providers or implementing complex workflows for authentication.

Exploring the Code

Let's look at a sample code snippet that demonstrates how to configure custom data fetching for server-side in a Next.js application:

First, let's create a send function using the native Fetch API to retrieve the necessary data:

type FetcherProps {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  path: string;
  data?: any;
  headers?: object;
  configs?: object;
}

// Define API base URL
const host = process.env.API_URL;

// Function for sending HTTP requests
export default function send(option: FetcherProps) {
  const { method, path, data, headers, configs } = option;
  return fetch(`${host}${path}`, {
    method,
    headers,
    credentials: 'include',
    body: JSON.stringify(data),
    ...configs,
  });
}

Next, we need to create a reusable function server-request.ts that can be used for all API data calls in server-side components to handle the server fetch request:

// Import necessary modules and types
import { cookies } from 'next/headers';
import send from './send.ts'
import { actionRefreshToken } from '@/actions'

type FetcherProps {
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
  path: string;
  data?: any;
  headers?: object;
  configs?: object;
}

// Define a function for making server requests
export default async function serverRequest({
  method,
  path,
  headers,
  data,
  configs,
}: FetcherProps) {
  // Retrieve authentication token from cookies
  const cookieStore = cookies();
  const token = cookieStore.get('token')?.value;

  // Send HTTP request with authentication token
  const res = await send({
    method,
    path,
    headers: {
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...headers,
    },
    data,
    configs,
  });

  // Handle response
  let status = res.status;
  let body = await res.json();
  let resHeaders = res.headers;

  // Handle error responses
  if (!res.ok) {
	  //We add a cond to handle the invalid token error from BE 
    if (status === 401 && body?.detail === 'Invalid token.') {
      // Refresh token if invalid and return token value or error 
      const refresh = await actionRefreshToken();
      
      if (refresh?.error) {
        throw { data: 'session ended', status: 401 };
      }
   // if no error we recall the function of serverRequest with new refresh token
      return await serverRequest({
        method,
        path,
        {
          Authorization: `Bearer ${refresh.token}`,
          ...headers,
        },
        data,
        configs
      });
    }
    // return any other error in this object format
    throw { data: body, status, headers: resHeaders };
  }

  // Return response
  return { data: body, status, headers: resHeaders };
}

for handling refresh token function we have two ways either we refresh it with server actions or with router handler for this example were using server actions actionRefreshToken.ts :


'use server';

import { cookies } from 'next/headers';
import send from '@lib/send';

export async function actionRefreshToken() {
  try {
    const body = await send({
      method: 'GET',
      path: '/refresh-token',
    })
    const { token } = await body.json();
    // Set the new token in the cookie
    cookies().set({
      name: 'token',
      value: token,
      httpOnly: true,
      maxAge: 1000 * 60 * 60 * 24, // minutes to milliseconds
      path: '/',
    });
    return { token };
  } catch (error: any) {
    // If the token is invalid, delete it from the cookie
    cookies().delete('token');
    return { error: error.message}
  }
}

Usage Example

Now, let's see how we can use the getUser function in a Next.js app/page.tsx:

// Import the function
import serverRequest from "@lib/server-request"

// Function to fetch user data
const getUser = async () => {
  try {
    const response = await serverRequest({
	    method:'GET', 
	    path: `/user/me`, 
	    configs: {
	      cache: 'force-cache',
	      next: { tags: ['user'] },
	    }
    });
    const user = await response.data;
    return user;
  } catch (error: any) {
    if (error.status === 401 && error.data === 'session ended') {
    // we redirect the user to login page if the session ended
      redirect('/login');
    }
    return error.data;
  }
};

// Next.js page component
export default async function Page() {
  // Fetch user data
  const user = await getUser();

  // Use user data in the page component
  // ......

  return (
    // ......
  );
}

And that's it! I hope that this article has helped you understand how to configure custom authentication in Next.js applications.

If you enjoyed it and found this post helpful, please share it with your network so that others can benefit as well. If you require any additional information, please do not hesitate to contact me. I am pleased to assist.

← Go back