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”
- 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;
}
- 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;
},
}
- 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