feat: improve session management by handling session errors and enhancing token refresh logic
This commit is contained in:
parent
b467ae704c
commit
b1f59249c0
@ -1,8 +1,6 @@
|
|||||||
import { authOptions } from "@/authOptions";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession, signOut } from "next-auth/react";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
moment.locale("fr");
|
moment.locale("fr");
|
||||||
|
|
||||||
@ -36,8 +34,11 @@ axiosInstance.interceptors.request.use(async (config) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) {
|
if (!session || session.error) {
|
||||||
redirect(authOptions.pages!.signIn!);
|
console.log("No session found, redirecting to login page");
|
||||||
|
await signOut({ callbackUrl: "/auth/login" });
|
||||||
|
// cancel the request
|
||||||
|
return new Promise(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedAccessToken = session.accessToken;
|
cachedAccessToken = session.accessToken;
|
||||||
|
25
src/app/types/next-auth.d.ts
vendored
25
src/app/types/next-auth.d.ts
vendored
@ -25,26 +25,39 @@ declare module "next-auth" {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessTokenExpires: Moment;
|
accessTokenExpires: Moment;
|
||||||
error?: Error;
|
error?: string;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
expires_at: number;
|
provider: string;
|
||||||
|
type: string;
|
||||||
|
providerAccountId: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
expires_at: number;
|
||||||
|
refresh_expires_in: number;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
|
token_type: string;
|
||||||
|
id_token: string;
|
||||||
|
session_state: string;
|
||||||
|
scope: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "next-auth/jwt" {
|
declare module "next-auth/jwt" {
|
||||||
interface JWT {
|
interface JWT {
|
||||||
|
// Default properties
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
picture: string;
|
||||||
|
sub: string;
|
||||||
|
|
||||||
|
// Custom properties
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessTokenExpires: Moment;
|
accessTokenExpires: Moment;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
error?: Error;
|
refreshTokenExpires: Moment;
|
||||||
|
error?: string;
|
||||||
user: User | AdapterUser;
|
user: User | AdapterUser;
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
jti: string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,34 +42,44 @@ export const authOptions: AuthOptions = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, account, user }) {
|
async jwt({ token, account, user: profile }) {
|
||||||
if (account && user) {
|
// Initial sign in
|
||||||
|
if (account && profile) {
|
||||||
token.accessToken = account.access_token;
|
token.accessToken = account.access_token;
|
||||||
token.accessTokenExpires = moment(
|
|
||||||
account.expires_at * 1000,
|
|
||||||
).subtract(5, "s");
|
|
||||||
token.refreshToken = account.refresh_token;
|
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(
|
const accessTokenDecode = jsonwebtoken.decode(
|
||||||
account.access_token,
|
account.access_token,
|
||||||
) as JWTDecoded;
|
) as JWTDecoded;
|
||||||
|
|
||||||
token.user = {
|
token.user = {
|
||||||
...user,
|
...profile,
|
||||||
roles: accessTokenDecode.realm_access.roles,
|
roles: accessTokenDecode.realm_access.roles,
|
||||||
};
|
};
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
// Return previous token if the access token has not expired yet
|
||||||
moment().isBefore(
|
if (moment().isBefore(moment(token.accessTokenExpires)))
|
||||||
moment(token.accessTokenExpires).subtract(5, "s"),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return token;
|
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);
|
return refreshAccessToken(token);
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
@ -89,25 +99,23 @@ export const authOptions: AuthOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let isRefreshing = false;
|
|
||||||
let refreshPromise: Promise<JWT> | null = null;
|
|
||||||
|
|
||||||
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
||||||
// If another request is already refreshing the token, wait for it to finish
|
|
||||||
if (isRefreshing) {
|
|
||||||
return refreshPromise!;
|
|
||||||
}
|
|
||||||
|
|
||||||
isRefreshing = true;
|
|
||||||
refreshPromise = (async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<{
|
const response = await axios.post<{
|
||||||
access_token: string;
|
access_token: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
|
id_token: string;
|
||||||
|
"not-before-policy": number;
|
||||||
|
refresh_expires_in: number;
|
||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
|
scope: string;
|
||||||
|
session_state: string;
|
||||||
|
token_type: string;
|
||||||
|
|
||||||
|
error?: string;
|
||||||
error_description?: string;
|
error_description?: string;
|
||||||
}>(
|
}>(
|
||||||
process.env.OAUTH_TOKEN_URL!,
|
process.env.OAUTH_TOKEN_URL,
|
||||||
{
|
{
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
refresh_token: token.refreshToken,
|
refresh_token: token.refreshToken,
|
||||||
@ -131,11 +139,10 @@ const refreshAccessToken = async (token: JWT): Promise<JWT> => {
|
|||||||
.subtract(5, "s"),
|
.subtract(5, "s"),
|
||||||
refreshToken: response.data.refresh_token,
|
refreshToken: response.data.refresh_token,
|
||||||
};
|
};
|
||||||
} finally {
|
} catch {
|
||||||
isRefreshing = false;
|
return {
|
||||||
refreshPromise = null;
|
...token,
|
||||||
}
|
error: "RefreshAccessTokenError",
|
||||||
})();
|
};
|
||||||
|
}
|
||||||
return refreshPromise;
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user