Authentification fonctionnelle via security.lions.dev
This commit is contained in:
@@ -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 = {
|
||||
|
||||
119
contexts/KeycloakContext.tsx
Normal file
119
contexts/KeycloakContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user