NextJS14 signOut when refresh token expired

I’d like to signout my user when a refresh token is expired, I tried to setup it by this way :

/auth.config.ts

import CredentialProvider from 'next-auth/providers/credentials';
import { NextAuthConfig } from 'next-auth';
import { jwtDecode } from "jwt-decode"
import { cookies } from 'next/headers';
import setCookieParser from "set-cookie-parser"


async function refreshAccessToken(token:any) {
  try {
    console.log(cookies().get("jwt_secret")?.value)
    const response = await fetch(process.env.API_URL + "/rf-tk", { 
      method: "GET", 
      credentials: 'include',
       headers: {
           'Accept': 'application/json',
           'Content-Type': 'application/json',
           ... cookies().get("jwt_secret")?.value && { Cookie : cookies().get("jwt_secret")?.value}
      }

    });
    const dataJson = await response.json()
    if (!response.ok) {
      throw dataJson;
    }
    const decoded:any= jwtDecode(dataJson.token)
    return {
      ...token,
      accessToken: dataJson.token,
      accessTokenExpires: decoded.exp * 1000,
      // refreshToken: data.refresh_token ?? token.refreshToken, // Fall back to old refresh token
    };
  } catch (error) {
    console.log('problemasss !!!')
    return {
      ...token,
      error: "RefreshAccessTokenError",
    };
  }
}

const authConfig = {
  providers: [
    CredentialProvider({
      credentials: {
        email: { type: 'email' },
        password: { type: 'password' },
      },
      async authorize(credentials, req) {
        const rep = await fetch(
          process.env.API_URL + '/api/account/login',
          { credentials: 'include', method: "POST", body: JSON.stringify({
            email: credentials?.email,
            password: credentials?.password,
          }), headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          } });
          const parsedCookie:any = setCookieParser(rep.headers.getSetCookie())
          cookies().set('jwt_secret', parsedCookie[0].value, {                 
            httpOnly: true,
            secure: false, 
            sameSite: 'lax',
            maxAge: parsedCookie[0].maxAge
            })
          const dataJson = await rep.json()
        if (dataJson?.accessToken) {
          return {
            ...dataJson
          };
        }

        return null;
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user } : any) {
      if (token && user) {
        const decoded:any = jwtDecode(user?.accessToken)
        token = { ...user, accessTokenExpires: decoded.exp * 1000 }
        return {
          accessTokenExpires: decoded.exp * 1000,
          ...user,
        };
      }
      if (Date.now() < token.accessTokenExpires) {
        return token;
      }
      
      return await refreshAccessToken(token);
    },
    async session({ session, token } : any ) {
      session.user.id = token.user.user_uuid;
      session.user.username = token.user.username;
      session.user.email = token.user.email;
      session.user.role = token.user.role
      session.accessToken = token.accessToken;
      session.error = token.error
      return session;
    },
  },
  session: {
    strategy: 'jwt', // Use JWT for session management
  },
  pages: {
    signIn: '/', // Custom sign-in page
  },
} satisfies NextAuthConfig;

export default authConfig;

and I put a useEffect() in a component that is present in all my pages :

/components/layout/header.tsx

"use client"
import ThemeToggle from '@/components/layout/ThemeToggle/theme-toggle';
import { cn } from '@/lib/utils';
import { MobileSidebar } from './mobile-sidebar';
import { BalanceShow } from '../balance/balance-show';
import { DepositDialog } from '../deposit-dialog/deposit-dialog';
import { signOut, useSession } from 'next-auth/react';
import { useEffect } from 'react';

export default function Header() {
  const { data: session } : any = useSession()
  useEffect(() => {
    if(session?.error === "RefreshAccessTokenError") {
      console.log('RefreshAccessTokenError DETECTED')
      signOut()
    }
  }, [session?.error])
  return (
<header className="sticky inset-x-0 top-0 w-full">
  <nav className="flex items-center justify-evenly px-4 py-2">
    <div className={cn('block lg:!hidden')}>
      <MobileSidebar />
    </div>
    <div className={cn('block m-auto flex inline-flex')}>
      <BalanceShow />
      <DepositDialog />
    </div>
    <div className="">
      <ThemeToggle />
    </div>
  </nav>
</header>

  );
}

Actually is partially working, but when the refresh token is expired, my refresh-token route is being called multiple times, and sometime I’m not getting logged out, do you have a better implementation for it ? I’m struggling to make it work

I tried to check the NextAuth documentation, but it’s outdated, I’m using :
“next”: “14.1”,
“next-auth”: “^5.0.0-beta.18”

  1. Modify the refreshAccessToken function:
    In this function, ensure that if the refresh token fails, you return an error indicating the token has expired.
async function refreshAccessToken(token: any) {
  if (isRefreshing) {
    // If a refresh is already in progress, return the promise for the existing refresh request
    return refreshPromise;
  }

  isRefreshing = true;
  refreshPromise = (async () => {
    try {
      const response = await fetch(process.env.API_URL + "/rf-tk", {
        method: "GET",
        credentials: 'include',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          ...(cookies().get("jwt_secret")?.value && { Cookie: cookies().get("jwt_secret")?.value }),
        }
      });

      const dataJson = await response.json();

      if (!response.ok) {
        if (response.status === 401) {
          return {
            ...token,
            error: "RefreshTokenExpired",  // Custom error for token expiry
          };
        }
        throw dataJson;
      }

      const decoded: any = jwtDecode(dataJson.token);
      return {
        ...token,
        accessToken: dataJson.token,
        accessTokenExpires: decoded.exp * 1000,
      };
    } catch (error) {
      return {
        ...token,
        error: "RefreshAccessTokenError",
      };
    } finally {
      isRefreshing = false;
      refreshPromise = null;
    }
  })();

  return refreshPromise;
}

  1. Update the jwt callback:
    In this callback, check if the refresh token has expired. If it has, propagate this error to the session, so the client can detect it.
callbacks: {
  async jwt({ token, user }: any) {
    // On initial sign in, attach the user and token
    if (user) {
      const decoded: any = jwtDecode(user.accessToken);
      return {
        ...user,
        accessTokenExpires: decoded.exp * 1000,
      };
    }

    // If the access token is still valid, return the token
    if (Date.now() < token.accessTokenExpires) {
      return token;
    }

    // Try to refresh the access token if it has expired
    const refreshedToken = await refreshAccessToken(token);

    // If refresh fails, handle sign-out on the client-side by setting error
    if (refreshedToken.error === "RefreshTokenExpired") {
      return {
        ...token,
        error: "SessionExpired",
      };
    }

    return refreshedToken;
  },

  async session({ session, token }: any) {
    session.user.id = token.user?.user_uuid;
    session.user.username = token.user?.username;
    session.user.email = token.user?.email;
    session.user.role = token.user?.role;
    session.accessToken = token.accessToken;
    session.error = token.error;

    // Propagate session expiration to the client
    if (token.error === "SessionExpired") {
      session.error = "SessionExpired";  // Client will handle this
    }

    return session;
  },
}

  1. Handle Sign-Out on the Client-Side:
    On the client side, check if the session contains the SessionExpired error and trigger a sign-out accordingly.
import { useSession, signOut } from 'next-auth/react';
import { useEffect } from 'react';

export default function MyComponent() {
  const { data: session, status } = useSession();

  useEffect(() => {
    if (session?.error === "SessionExpired") {
      signOut({ callbackUrl: '/login' });  // Redirect to login page after sign-out
    }
  }, [session]);

  if (status === 'loading') return <p>Loading...</p>;

  return (
    <div>
      {session ? (
        <p>Welcome, {session.user?.username}!</p>
      ) : (
        <p>Please log in.</p>
      )}
    </div>
  );
}


Key Points:

Refresh Token Expiry Detection: In refreshAccessToken, you detect when the refresh token has expired by checking for 401 responses. If this happens, return a specific error (RefreshTokenExpired).

Sign Out Trigger: In the jwt callback, if the refresh token has expired, propagate a SessionExpired error to the session. The client checks for this error and triggers signOut() when detected.

Client-Side Handling: On the client side, use the useSession() hook from next-auth/react to monitor the session. If the session has expired, automatically sign out the user and redirect them to the login page.

This approach should sign out your user when the refresh token has expired.

4

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật