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 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;
|
||||
|
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user