feat: improve session management by handling session errors and enhancing token refresh logic

This commit is contained in:
Rémi 2025-01-06 21:48:31 +01:00
parent b467ae704c
commit b1f59249c0
3 changed files with 91 additions and 70 deletions

View File

@ -1,8 +1,6 @@
import { authOptions } from "@/authOptions";
import axios from "axios";
import moment, { Moment } from "moment";
import { getSession } from "next-auth/react";
import { redirect } from "next/navigation";
import { getSession, signOut } from "next-auth/react";
moment.locale("fr");
@ -36,8 +34,11 @@ axiosInstance.interceptors.request.use(async (config) => {
try {
const session = await getSession();
if (!session) {
redirect(authOptions.pages!.signIn!);
if (!session || session.error) {
console.log("No session found, redirecting to login page");
await signOut({ callbackUrl: "/auth/login" });
// cancel the request
return new Promise(() => {});
}
cachedAccessToken = session.accessToken;

View File

@ -25,26 +25,39 @@ declare module "next-auth" {
accessToken: string;
refreshToken: string;
accessTokenExpires: Moment;
error?: Error;
error?: string;
user: User;
}
interface Account {
expires_at: number;
provider: string;
type: string;
providerAccountId: string;
access_token: string;
expires_at: number;
refresh_expires_in: number;
refresh_token: string;
token_type: string;
id_token: string;
session_state: string;
scope: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
// Default properties
name: string;
email: string;
picture: string;
sub: string;
// Custom properties
accessToken: string;
accessTokenExpires: Moment;
refreshToken: string;
error?: Error;
refreshTokenExpires: Moment;
error?: string;
user: User | AdapterUser;
iat: number;
exp: number;
jti: string;
}
}

View File

@ -42,34 +42,44 @@ export const authOptions: AuthOptions = {
},
],
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
async jwt({ token, account, user: profile }) {
// Initial sign in
if (account && profile) {
token.accessToken = account.access_token;
token.accessTokenExpires = moment(
account.expires_at * 1000,
).subtract(5, "s");
token.refreshToken = account.refresh_token;
token.accessTokenExpires = moment.unix(account.expires_at);
token.refreshTokenExpires = moment().add(
account.refresh_expires_in,
"seconds",
);
const accessTokenDecode = jsonwebtoken.decode(
account.access_token,
) as JWTDecoded;
token.user = {
...user,
...profile,
roles: accessTokenDecode.realm_access.roles,
};
return token;
}
if (
moment().isBefore(
moment(token.accessTokenExpires).subtract(5, "s"),
)
) {
// Return previous token if the access token has not expired yet
if (moment().isBefore(moment(token.accessTokenExpires)))
return token;
}
if (
token.refreshTokenExpires &&
moment().isAfter(token.refreshTokenExpires)
)
return {
...token,
error: "Refresh token has expired",
};
// Access token has expired, try to refresh it
return refreshAccessToken(token);
},
async session({ session, token }) {
@ -89,53 +99,50 @@ export const authOptions: AuthOptions = {
},
};
let isRefreshing = false;
let refreshPromise: Promise<JWT> | null = null;
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
// If another request is already refreshing the token, wait for it to finish
if (isRefreshing) {
return refreshPromise!;
try {
const response = await axios.post<{
access_token: string;
expires_in: number;
id_token: string;
"not-before-policy": number;
refresh_expires_in: number;
refresh_token: string;
scope: string;
session_state: string;
token_type: string;
error?: string;
error_description?: string;
}>(
process.env.OAUTH_TOKEN_URL,
{
grant_type: "refresh_token",
refresh_token: token.refreshToken,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
if (response.status !== 200) throw response.data;
return {
...token,
accessToken: response.data.access_token,
accessTokenExpires: moment()
.add(response.data.expires_in, "seconds")
.subtract(5, "s"),
refreshToken: response.data.refresh_token,
};
} catch {
return {
...token,
error: "RefreshAccessTokenError",
};
}
isRefreshing = true;
refreshPromise = (async () => {
try {
const response = await axios.post<{
access_token: string;
expires_in: number;
refresh_token: string;
error_description?: string;
}>(
process.env.OAUTH_TOKEN_URL!,
{
grant_type: "refresh_token",
refresh_token: token.refreshToken,
client_id: process.env.OAUTH_CLIENT_ID,
client_secret: process.env.OAUTH_CLIENT_SECRET,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
},
);
if (response.status !== 200) throw response.data;
return {
...token,
accessToken: response.data.access_token,
accessTokenExpires: moment()
.add(response.data.expires_in, "seconds")
.subtract(5, "s"),
refreshToken: response.data.refresh_token,
};
} finally {
isRefreshing = false;
refreshPromise = null;
}
})();
return refreshPromise;
};