Authentification fonctionnelle via security.lions.dev

This commit is contained in:
DahoudG
2025-11-01 14:16:20 +00:00
parent a5adb84a62
commit 1d68878601
20 changed files with 387 additions and 1067 deletions

View File

@@ -1,7 +1,8 @@
'use client';
import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
import { initKeycloak, keycloakInitOptions, RoleUtils, BTP_ROLES, KEYCLOAK_TIMEOUTS, KEYCLOAK_REDIRECTS } from '@/config/keycloak';
import { useKeycloak } from './KeycloakContext';
import { RoleUtils, BTP_ROLES } from '@/config/keycloak';
import { useRouter } from 'next/navigation';
// Types pour l'authentification
@@ -61,7 +62,10 @@ interface AuthProviderProps {
// Provider d'authentification
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const router = useRouter();
// Utiliser le contexte Keycloak
const { keycloak, authenticated, loading, token, login: keycloakLogin, logout: keycloakLogout, updateToken: keycloakUpdateToken } = useKeycloak();
// État de l'authentification
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
@@ -178,11 +182,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}
}, [fetchUserInfo, extractUserInfo]);
// Fonction de connexion
// Fonction de connexion - utilise Keycloak JS SDK
const login = useCallback(async (): Promise<void> => {
try {
// Redirection directe vers l'API d'authentification
window.location.href = '/api/auth/login';
keycloakLogin();
} catch (error) {
console.error('Erreur lors de la connexion:', error);
setAuthState(prev => ({
@@ -191,21 +194,12 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
isLoading: false,
}));
}
}, []);
}, [keycloakLogin]);
// Fonction de déconnexion
// Fonction de déconnexion - utilise Keycloak JS SDK
const logout = useCallback(async (): Promise<void> => {
try {
// Nettoyer les tokens locaux
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('idToken');
// Supprimer le cookie
document.cookie = 'keycloak-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
// Redirection directe vers l'API de déconnexion
window.location.href = '/api/auth/logout';
keycloakLogout();
} catch (error) {
console.error('Erreur lors de la déconnexion:', error);
// Forcer la déconnexion locale même en cas d'erreur
@@ -219,45 +213,32 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
});
router.push('/');
}
}, [router]);
}, [keycloakLogout, router]);
// Fonction pour rafraîchir l'authentification
// Fonction pour rafraîchir l'authentification - utilise Keycloak SDK
const refreshAuth = useCallback(async (): Promise<void> => {
try {
// Vérifier les tokens stockés
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
await logout();
return;
if (keycloak && authenticated) {
await keycloakUpdateToken();
}
// TODO: Implémenter le rafraîchissement via API si nécessaire
console.log('Rafraîchissement des tokens...');
} catch (error) {
console.error('Erreur lors du rafraîchissement du token:', error);
await logout();
}
}, [logout]);
}, [keycloak, authenticated, keycloakUpdateToken, logout]);
// Fonction pour mettre à jour le token
// Fonction pour mettre à jour le token - utilise Keycloak SDK
const updateToken = useCallback(async (minValidity: number = 30): Promise<boolean> => {
try {
// Vérifier si le token est encore valide
const accessToken = localStorage.getItem('accessToken');
if (!accessToken) {
return false;
if (keycloak && authenticated) {
return await keycloakUpdateToken();
}
// TODO: Vérifier l'expiration du token et rafraîchir si nécessaire
return true;
return false;
} catch (error) {
console.error('Erreur lors de la mise à jour du token:', error);
return false;
}
}, []);
}, [keycloak, authenticated, keycloakUpdateToken]);
// Fonctions utilitaires pour les rôles et permissions
const hasRole = useCallback((role: string): boolean => {
@@ -277,65 +258,39 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
return RoleUtils.isRoleHigher(authState.user.highestRole, role);
}, [authState.user]);
// Initialisation de Keycloak - Désactivée pour utiliser l'authentification manuelle
// Synchroniser l'état avec KeycloakContext
useEffect(() => {
const initializeKeycloak = async () => {
try {
// Vérifier s'il y a des tokens stockés localement
const accessToken = localStorage.getItem('accessToken');
const refreshToken = localStorage.getItem('refreshToken');
const idToken = localStorage.getItem('idToken');
const syncAuthState = async () => {
if (loading) {
setAuthState({
isAuthenticated: false,
isLoading: true,
user: null,
token: null,
refreshToken: null,
error: null,
});
return;
}
if (accessToken) {
// Stocker aussi dans un cookie pour le middleware
document.cookie = `keycloak-token=${accessToken}; path=/; max-age=3600; SameSite=Lax`;
if (authenticated && keycloak && token) {
// Essayer de récupérer les informations utilisateur depuis l'API
let user = await fetchUserInfo(token);
// Récupérer les informations utilisateur depuis l'API
try {
const user = await fetchUserInfo(accessToken);
if (user) {
setAuthState({
isAuthenticated: true,
isLoading: false,
user,
token: accessToken,
refreshToken: refreshToken,
error: null,
});
return;
}
} catch (error) {
console.warn('Impossible de récupérer les informations utilisateur depuis l\'API, utilisation des données par défaut');
}
// Fallback avec des données par défaut si l'API ne répond pas
setAuthState({
isAuthenticated: true,
isLoading: false,
user: {
id: 'dev-user-001',
username: 'admin.btpxpress',
email: 'admin@btpxpress.com',
firstName: 'Jean-Michel',
lastName: 'Martineau',
fullName: 'Jean-Michel Martineau',
roles: [BTP_ROLES.SUPER_ADMIN, BTP_ROLES.ADMIN, BTP_ROLES.DIRECTEUR],
permissions: RoleUtils.getUserPermissions([BTP_ROLES.SUPER_ADMIN]),
highestRole: BTP_ROLES.SUPER_ADMIN,
isAdmin: true,
isManager: true,
isEmployee: false,
isClient: false,
},
token: accessToken,
refreshToken: refreshToken,
error: null,
});
return;
// Si l'API ne répond pas, extraire du token JWT
if (!user) {
user = extractUserInfo(keycloak);
}
// Pas de tokens, rester non authentifié
setAuthState({
isAuthenticated: true,
isLoading: false,
user,
token: token,
refreshToken: keycloak.refreshToken || null,
error: null,
});
} else {
setAuthState({
isAuthenticated: false,
isLoading: false,
@@ -344,33 +299,13 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
refreshToken: null,
error: null,
});
} catch (error) {
console.error('Erreur lors de l\'initialisation de l\'authentification:', error);
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
token: null,
refreshToken: null,
error: 'Erreur lors de l\'initialisation de l\'authentification',
});
}
};
initializeKeycloak();
}, [updateAuthState, refreshAuth, logout]);
syncAuthState();
}, [authenticated, loading, token, keycloak, fetchUserInfo, extractUserInfo]);
// Rafraîchissement automatique du token
useEffect(() => {
if (!authState.isAuthenticated) return;
const interval = setInterval(() => {
updateToken();
}, KEYCLOAK_TIMEOUTS.SESSION_CHECK_INTERVAL * 1000);
return () => clearInterval(interval);
}, [authState.isAuthenticated, updateToken]);
// Le rafraîchissement automatique du token est géré par KeycloakContext
// Valeur du contexte
const contextValue: AuthContextType = {

View File

@@ -0,0 +1,119 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import Keycloak from 'keycloak-js';
import keycloak from '../lib/keycloak';
interface KeycloakContextType {
keycloak: Keycloak | null;
authenticated: boolean;
loading: boolean;
token: string | null;
login: () => void;
logout: () => void;
updateToken: () => Promise<boolean>;
}
const KeycloakContext = createContext<KeycloakContextType | undefined>(undefined);
export const useKeycloak = () => {
const context = useContext(KeycloakContext);
if (!context) {
throw new Error('useKeycloak must be used within a KeycloakProvider');
}
return context;
};
interface KeycloakProviderProps {
children: ReactNode;
}
export const KeycloakProvider: React.FC<KeycloakProviderProps> = ({ children }) => {
const [authenticated, setAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
const initKeycloak = async () => {
try {
console.log('🔐 Initializing Keycloak...');
const authenticated = await keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
checkLoginIframe: false, // Désactivé pour éviter les problèmes CORS
flow: 'standard', // Force authorization_code flow (pas implicit/hybrid)
responseMode: 'query', // Force query string au lieu de fragment
});
console.log(`✅ Keycloak initialized. Authenticated: ${authenticated}`);
setAuthenticated(authenticated);
setToken(keycloak.token || null);
// Rafraîchir le token automatiquement
if (authenticated) {
setInterval(() => {
keycloak.updateToken(70).then((refreshed) => {
if (refreshed) {
console.log('🔄 Token refreshed');
setToken(keycloak.token || null);
}
}).catch(() => {
console.error('❌ Failed to refresh token');
setAuthenticated(false);
});
}, 60000); // Toutes les 60 secondes
}
} catch (error) {
console.error('❌ Keycloak initialization failed:', error);
} finally {
setLoading(false);
}
};
initKeycloak();
}, []);
const login = () => {
keycloak.login({
redirectUri: window.location.origin + '/dashboard',
});
};
const logout = () => {
keycloak.logout({
redirectUri: window.location.origin,
});
};
const updateToken = async (): Promise<boolean> => {
try {
const refreshed = await keycloak.updateToken(30);
if (refreshed) {
setToken(keycloak.token || null);
}
return refreshed;
} catch (error) {
console.error('Failed to update token', error);
return false;
}
};
const value: KeycloakContextType = {
keycloak,
authenticated,
loading,
token,
login,
logout,
updateToken,
};
return (
<KeycloakContext.Provider value={value}>
{children}
</KeycloakContext.Provider>
);
};