0

I have a project made with NextJS 13.4, NextAuth, Axios and NestJS as the API server and authenticator.

I created hooks that will create Axios Intercpetors to handle the access and the refresh token logic. The hooks are working fine and I am able to send authorized requests to the API server using the access token and get a new token if the old one expired by using the refresh token. The tokens are stored inside the Session of the NextAuth and they get updated each time the Refresh Token is retrieved from the API server.

So far, I created the sign in and sign up functions without any issues and the Axios Interceptors and hooks are working fine.

Now, I started running into a BIG problem once I wanted to load the user profile once a component is loaded. I placed the axios call inside useEffect() so I will populate the userProfile state and then use it the component. When the page first load, the component is mounted fine and I can see the user data. But when I click Refresh in the browser or hit F5, I get an axios error of 401.

After some investigation, I found out that the hook is getting undefined session from NextAuth useSession. And since axios is getting the tokens from the session, it is getting rejected from the API since it is passing undefined in the headers.

When I moved the axios call outside the useEffect() and tried to call it by pressing a button on the component, it works fine without an issue.

Something is happening when I palce the axios call inside the useEffect().

PLEASE help as my whole project is now stuck for days and I am not able to move forward. I searched the web daily for a solution but I was unable to.

Here are the different parts of the code that might help you help me.

The axios instance creation file:

import axios, { AxiosError } from "axios"

const BASE_URL = "http://localhost:8000" //API

export default axios.create({
  baseURL: BASE_URL,
  headers: { "Content-Type": "application/json" },
})

export const axiosAuth = axios.create({
  baseURL: BASE_URL,
  headers: { "Content-Type": "application/json" },
})

The useAxiosAuth hook file which will intercept the request and response to manage the tokens attachements:

"use client"

import { useEffect } from "react"
import { axiosAuth } from "lib/axios"
import { useSession } from "next-auth/react"

import { useRefreshToken } from "./useRefreshToken"

const useAxiosAuth = () => {
  const { data: session, status } = useSession()
  const refreshToken = useRefreshToken()

  useEffect(() => {
    const requestIntercept = axiosAuth.interceptors.request.use(
      (config) => {
        if (!config.headers["Authorization"]) {
          config.headers[
            "Authorization"
          ] = `Bearer ${session?.user?.accessToken}`
        }
        if (!config.headers["Token-Id"]) {
          config.headers["Token-Id"] = `${session?.user?.tokenId}`
        }
        return config
      },
      (error) => Promise.reject(error)
    )

    const responseIntercept = axiosAuth.interceptors.response.use(
      (response) => response,
      async (error) => {
        const prevRequest = error?.config
        if (error?.response?.status === 401 && !prevRequest?.sent) {
          prevRequest._retry = true

          await refreshToken()

          prevRequest.headers[
            "Authorization"
          ] = `Bearer ${session?.user.accessToken}`

          return axiosAuth(prevRequest)
        }
        return Promise.reject(error)
      }
    )

    return () => {
      axiosAuth.interceptors.request.eject(requestIntercept)
      axiosAuth.interceptors.response.eject(responseIntercept)
    }
  }, [session, refreshToken, status])

  return axiosAuth
}

export default useAxiosAuth

The useRefreshToken hook (used in the above file) that will get a new tokens using the previous Refresh Token and update the NextAuth session on the server and client:

"use client"

import axios from "lib/axios"
import { signIn, useSession } from "next-auth/react"

export const useRefreshToken = () => {
  const { data: session, update, status } = useSession()
  const refreshToken = async () => {
    const res = await axios.post(
      "/auth/refresh",
      {},
      {
        headers: {
          Authorization: `Bearer ${session?.user.refreshToken}`,
          "Token-Id": session?.user.tokenId,
        },
      }
    )

    if (session) {
      //update server session
      await update({
        ...session,
        user: {
          ...session.user,
          accessToken: res?.data.accessToken,
          refreshToken: res?.data.refreshToken,
          tokenId: res?.data.tokenId,
          accessTokenExpires: res?.data.accessTokenExpires,
        },
      })
      //update client session at the same time
      session.user.accessToken = res?.data.accessToken
      session.user.refreshToken = res?.data.refreshToken
      session.user.tokenId = res?.data.tokenId
      session.user.accessTokenExpires = res?.data.accessTokenExpires
    } else {
      signIn()
    }
  }
  return refreshToken
}

And finally, here is the implementation that is causing this strange issue:

"use client"

import React, { useEffect, useState } from "react"
import { useSession } from "next-auth/react"
import useAxiosAuth from "@/lib/hooks/useAxiosAuth"
import UserProfile from "@/types/user-profile"

export default function Profile() {
  const [userProfile, setUserProfile] = useState<UserProfile>()
  const axios = useAxiosAuth()

  useEffect(() => {
    ;(async () => {
      await axios.get<UserProfile>("/profile").then((res) => {
        setUserProfile(res.data)
      })
    })()
    return () => {}
  }, [axios])
  
  return (
    <>
.....
      </>
)
}

Here is the error I am getting from axios and you can see that the tokens are undefined

enter image description here

Please help and someone please guide me on what is wrong and how can I fix this.

Yousi
  • 811
  • 3
  • 12
  • 26
  • have a look [here](https://stackoverflow.com/a/75428838/13488990) maybe it helps, this is how the docs suggests checking the user status – Ahmed Sbai Aug 27 '23 at 22:46

1 Answers1

0

It looks like the issue is related to the timing of how useEffect and useSession interact. When the page first loads, useSession might not be fully initialized, leading to undefined session values, hence the 401 error.

  1. Initialize State: You can use the status from useSession to check if the session is still loading. Run the Axios call only when the session is fully loaded.

    const { data: session, status } = useSession();
    useEffect(() => {
      if (status === 'authenticated') {
        // Your Axios code here
      }
    }, [status]);
    
  2. Error Handling: Add a conditional check before making the Axios call to ensure that the session data exists. If not, you can redirect the user to the login page or do something appropriate.

    if(session && session.user) {
      // Axios call
    }
    
  3. Button Alternative: Since you mentioned it works when triggered by a button, you might consider having a "Retry" button that shows up only when the Axios request fails due to authentication. This is more of a workaround, but it could be useful while you're debugging.

Try these suggestions and you should be able to ensure that the Axios calls are made only when the session data is available.

jimmykurian
  • 301
  • 2
  • 12
  • 1
    You're a total GENIUS! The status check (point 1) worked like a charm, seriously! It's crazy how such a simple thing can be so effective. You're a lifesaver, no joke! Big thanks! – Yousi Aug 27 '23 at 04:22