feat: Securisation du Backend

This commit is contained in:
Luc SORIGNET
2026-02-27 10:45:36 +01:00
parent 2fef6d61a4
commit fa843097ba
55 changed files with 2898 additions and 910 deletions

View File

@ -1,6 +1,5 @@
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { getJWT, refreshJWT } from '@/app/actions/authAction';
import jwt_decode from 'jsonwebtoken';
import logger from '@/utils/logger';
@ -13,19 +12,32 @@ const options = {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials, req) => {
authorize: async (credentials) => {
// URL calculée ici (pas au niveau module) pour garantir que NEXT_PUBLIC_API_URL est chargé
const loginUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/login`;
try {
const data = {
email: credentials.email,
password: credentials.password,
};
const res = await fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
Connection: 'close',
},
body: JSON.stringify({
email: credentials.email,
password: credentials.password,
}),
});
const user = await getJWT(data);
if (user) {
return user;
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body?.errorMessage || 'Identifiants invalides');
}
const user = await res.json();
return user || null;
} catch (error) {
logger.error('Authorize error:', error.message);
throw new Error(error.message || 'Invalid credentials');
}
},
@ -33,8 +45,10 @@ const options = {
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 jours
updateAge: 24 * 60 * 60, // 24 heures
maxAge: 60 * 60, // 1 Hour
// 0 = réécrire le cookie à chaque fois que le token change (indispensable avec
// un access token Django de 15 min, sinon le cookie expiré reste en place)
updateAge: 0,
},
cookies: {
sessionToken: {
@ -64,25 +78,61 @@ const options = {
return token;
}
// Token expiré, essayer de le rafraîchir
// Token Django expiré (lifetime = 15 min), essayer de le rafraîchir
logger.info('JWT: access token expiré, tentative de refresh');
if (!token.refresh) {
logger.error('JWT: refresh token absent dans la session');
return { ...token, error: 'RefreshTokenError' };
}
const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/refreshJWT`;
if (!process.env.NEXT_PUBLIC_API_URL) {
logger.error('JWT: NEXT_PUBLIC_API_URL non défini, refresh impossible');
return { ...token, error: 'RefreshTokenError' };
}
try {
const response = await refreshJWT({ refresh: token.refresh });
if (response && response?.token) {
return {
...token,
token: response.token,
refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
};
} else {
throw new Error('Failed to refresh token');
const res = await fetch(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Connection: close évite le SocketError undici lié au keep-alive vers Daphne
Connection: 'close',
},
body: JSON.stringify({ refresh: token.refresh }),
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
logger.error('JWT: refresh échoué', { status: res.status, body });
throw new Error(`Refresh HTTP ${res.status}`);
}
const response = await res.json();
if (!response?.token) {
logger.error('JWT: réponse refresh sans token', { response });
throw new Error('Réponse refresh invalide');
}
logger.info('JWT: refresh réussi');
return {
...token,
token: response.token,
refresh: response.refresh,
tokenExpires: jwt_decode.decode(response.token).exp * 1000,
error: undefined,
};
} catch (error) {
logger.error('Refresh token failed:', error);
return token;
logger.error('JWT: refresh token failed', { message: error.message });
return { ...token, error: 'RefreshTokenError' };
}
},
async session({ session, token }) {
if (token?.error === 'RefreshTokenError') {
session.error = 'RefreshTokenError';
return session;
}
if (token && token?.token) {
const { user_id, email, roles, roleIndexLoginDefault } =
jwt_decode.decode(token.token);