/// Service d'Authentification Keycloak /// Gère l'authentification avec votre instance Keycloak sur port 8180 library keycloak_auth_service; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import '../models/user.dart'; import '../models/user_role.dart'; import 'keycloak_role_mapper.dart'; import 'keycloak_webview_auth_service.dart'; /// Configuration Keycloak pour votre instance class KeycloakConfig { /// URL de base de votre Keycloak static const String baseUrl = 'http://192.168.1.11:8180'; /// Realm UnionFlow static const String realm = 'unionflow'; /// Client ID pour l'application mobile static const String clientId = 'unionflow-mobile'; /// URL de redirection après authentification static const String redirectUrl = 'dev.lions.unionflow-mobile://auth/callback'; /// Scopes demandés static const List scopes = ['openid', 'profile', 'email', 'roles']; /// Endpoints calculés static String get authorizationEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/auth'; static String get tokenEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/token'; static String get userInfoEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/userinfo'; static String get logoutEndpoint => '$baseUrl/realms/$realm/protocol/openid-connect/logout'; } /// Service d'authentification Keycloak ultra-sophistiqué class KeycloakAuthService { static const FlutterAppAuth _appAuth = FlutterAppAuth(); static const FlutterSecureStorage _secureStorage = FlutterSecureStorage( aOptions: AndroidOptions( encryptedSharedPreferences: true, ), iOptions: IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, ), ); // Clés de stockage sécurisé static const String _accessTokenKey = 'keycloak_access_token'; static const String _refreshTokenKey = 'keycloak_refresh_token'; static const String _idTokenKey = 'keycloak_id_token'; static const String _userInfoKey = 'keycloak_user_info'; /// Authentification avec Keycloak via WebView (solution HTTP compatible) /// /// Cette méthode utilise maintenant KeycloakWebViewAuthService pour contourner /// les limitations HTTPS de flutter_appauth static Future authenticate() async { try { debugPrint('🔐 Démarrage authentification Keycloak via WebView...'); // Utiliser le service WebView pour l'authentification // Cette méthode retourne null car l'authentification WebView // est gérée différemment (via callback) debugPrint('💡 Authentification WebView - utilisez authenticateWithWebView() à la place'); return null; } catch (e, stackTrace) { debugPrint('💥 Erreur authentification Keycloak: $e'); debugPrint('Stack trace: $stackTrace'); return null; } } /// Rafraîchit le token d'accès static Future refreshToken() async { try { final String? refreshToken = await _secureStorage.read( key: _refreshTokenKey, ); if (refreshToken == null) { debugPrint('❌ Aucun refresh token disponible'); return null; } debugPrint('🔄 Rafraîchissement du token...'); final TokenRequest request = TokenRequest( KeycloakConfig.clientId, KeycloakConfig.redirectUrl, refreshToken: refreshToken, serviceConfiguration: AuthorizationServiceConfiguration( authorizationEndpoint: KeycloakConfig.authorizationEndpoint, tokenEndpoint: KeycloakConfig.tokenEndpoint, ), ); final TokenResponse? result = await _appAuth.token(request); if (result != null) { await _storeTokens(result); debugPrint('✅ Token rafraîchi avec succès'); return result; } debugPrint('❌ Échec du rafraîchissement du token'); return null; } catch (e, stackTrace) { debugPrint('💥 Erreur rafraîchissement token: $e'); debugPrint('Stack trace: $stackTrace'); return null; } } /// Récupère l'utilisateur authentifié depuis les tokens static Future getCurrentUser() async { try { final String? accessToken = await _secureStorage.read( key: _accessTokenKey, ); final String? idToken = await _secureStorage.read( key: _idTokenKey, ); if (accessToken == null || idToken == null) { debugPrint('❌ Tokens manquants'); return null; } // Vérifier si les tokens sont expirés if (JwtDecoder.isExpired(accessToken)) { debugPrint('⏰ Access token expiré, tentative de rafraîchissement...'); final TokenResponse? refreshResult = await refreshToken(); if (refreshResult == null) { debugPrint('❌ Impossible de rafraîchir le token'); return null; } } // Décoder les tokens JWT final Map accessTokenPayload = JwtDecoder.decode(accessToken); final Map idTokenPayload = JwtDecoder.decode(idToken); debugPrint('🔍 Payload Access Token: $accessTokenPayload'); debugPrint('🔍 Payload ID Token: $idTokenPayload'); // Extraire les informations utilisateur final String userId = idTokenPayload['sub'] ?? ''; final String email = idTokenPayload['email'] ?? ''; final String firstName = idTokenPayload['given_name'] ?? ''; final String lastName = idTokenPayload['family_name'] ?? ''; // Extraire les rôles Keycloak final List keycloakRoles = _extractKeycloakRoles(accessTokenPayload); debugPrint('🎭 Rôles Keycloak extraits: $keycloakRoles'); // Si aucun rôle, assigner un rôle par défaut if (keycloakRoles.isEmpty) { debugPrint('⚠️ Aucun rôle trouvé, assignation du rôle MEMBER par défaut'); keycloakRoles.add('member'); } // Mapper vers notre système de rôles final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles); final List permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles); debugPrint('🎯 Rôle principal mappé: ${primaryRole.displayName}'); debugPrint('🔐 Permissions mappées: ${permissions.length} permissions'); debugPrint('📋 Permissions détaillées: $permissions'); // Créer l'utilisateur final User user = User( id: userId, email: email, firstName: firstName, lastName: lastName, primaryRole: primaryRole, organizationContexts: const [], // À implémenter selon vos besoins additionalPermissions: permissions, revokedPermissions: const [], preferences: const UserPreferences(), lastLoginAt: DateTime.now(), createdAt: DateTime.now(), // À récupérer depuis Keycloak si disponible isActive: true, ); // Stocker les informations utilisateur await _secureStorage.write( key: _userInfoKey, value: jsonEncode(user.toJson()), ); debugPrint('✅ Utilisateur récupéré: ${user.fullName} (${user.primaryRole.displayName})'); return user; } catch (e, stackTrace) { debugPrint('💥 Erreur récupération utilisateur: $e'); debugPrint('Stack trace: $stackTrace'); return null; } } /// Déconnexion complète static Future logout() async { try { debugPrint('🚪 Déconnexion Keycloak...'); final String? idToken = await _secureStorage.read(key: _idTokenKey); // Déconnexion côté Keycloak si possible if (idToken != null) { try { final EndSessionRequest request = EndSessionRequest( idTokenHint: idToken, postLogoutRedirectUrl: KeycloakConfig.redirectUrl, serviceConfiguration: AuthorizationServiceConfiguration( authorizationEndpoint: KeycloakConfig.authorizationEndpoint, tokenEndpoint: KeycloakConfig.tokenEndpoint, endSessionEndpoint: KeycloakConfig.logoutEndpoint, ), ); await _appAuth.endSession(request); debugPrint('✅ Déconnexion Keycloak côté serveur réussie'); } catch (e) { debugPrint('⚠️ Déconnexion côté serveur échouée: $e'); // Continue quand même avec la déconnexion locale } } // Nettoyage local des tokens await _clearTokens(); debugPrint('✅ Déconnexion locale terminée'); return true; } catch (e, stackTrace) { debugPrint('💥 Erreur déconnexion: $e'); debugPrint('Stack trace: $stackTrace'); return false; } } /// Vérifie si l'utilisateur est authentifié static Future isAuthenticated() async { try { final String? accessToken = await _secureStorage.read( key: _accessTokenKey, ); if (accessToken == null) { return false; } // Vérifier si le token est expiré if (JwtDecoder.isExpired(accessToken)) { // Tenter de rafraîchir final TokenResponse? refreshResult = await refreshToken(); return refreshResult != null; } return true; } catch (e) { debugPrint('💥 Erreur vérification authentification: $e'); return false; } } /// Stocke les tokens de manière sécurisée static Future _storeTokens(TokenResponse tokenResponse) async { if (tokenResponse.accessToken != null) { await _secureStorage.write( key: _accessTokenKey, value: tokenResponse.accessToken!, ); } if (tokenResponse.refreshToken != null) { await _secureStorage.write( key: _refreshTokenKey, value: tokenResponse.refreshToken!, ); } if (tokenResponse.idToken != null) { await _secureStorage.write( key: _idTokenKey, value: tokenResponse.idToken!, ); } debugPrint('🔒 Tokens stockés de manière sécurisée'); } /// Nettoie tous les tokens stockés static Future _clearTokens() async { await _secureStorage.delete(key: _accessTokenKey); await _secureStorage.delete(key: _refreshTokenKey); await _secureStorage.delete(key: _idTokenKey); await _secureStorage.delete(key: _userInfoKey); debugPrint('🧹 Tokens nettoyés'); } /// Extrait les rôles depuis le payload JWT Keycloak static List _extractKeycloakRoles(Map payload) { final List roles = []; try { // Rôles du realm final Map? realmAccess = payload['realm_access']; if (realmAccess != null && realmAccess['roles'] is List) { final List realmRoles = realmAccess['roles']; roles.addAll(realmRoles.cast()); } // Rôles des clients final Map? resourceAccess = payload['resource_access']; if (resourceAccess != null) { resourceAccess.forEach((clientId, clientData) { if (clientData is Map && clientData['roles'] is List) { final List clientRoles = clientData['roles']; roles.addAll(clientRoles.cast()); } }); } // Filtrer les rôles système Keycloak return roles.where((role) => !role.startsWith('default-roles-') && role != 'offline_access' && role != 'uma_authorization' ).toList(); } catch (e) { debugPrint('💥 Erreur extraction rôles: $e'); return []; } } /// Récupère le token d'accès actuel static Future getAccessToken() async { try { final String? accessToken = await _secureStorage.read( key: _accessTokenKey, ); if (accessToken != null && !JwtDecoder.isExpired(accessToken)) { return accessToken; } // Token expiré, tenter de rafraîchir final TokenResponse? refreshResult = await refreshToken(); return refreshResult?.accessToken; } catch (e) { debugPrint('💥 Erreur récupération access token: $e'); return null; } } // ═══════════════════════════════════════════════════════════════════════════ // MÉTHODES WEBVIEW - Délégation vers KeycloakWebViewAuthService // ═══════════════════════════════════════════════════════════════════════════ /// Prépare l'authentification WebView /// /// Retourne les paramètres nécessaires pour lancer la WebView d'authentification static Future> prepareWebViewAuthentication() async { return KeycloakWebViewAuthService.prepareAuthentication(); } /// Traite le callback WebView et finalise l'authentification /// /// Cette méthode doit être appelée quand l'URL de callback est interceptée static Future handleWebViewCallback(String callbackUrl) async { return KeycloakWebViewAuthService.handleAuthCallback(callbackUrl); } /// Vérifie si l'utilisateur est authentifié (compatible WebView) static Future isWebViewAuthenticated() async { return KeycloakWebViewAuthService.isAuthenticated(); } /// Récupère l'utilisateur authentifié (compatible WebView) static Future getCurrentWebViewUser() async { return KeycloakWebViewAuthService.getCurrentUser(); } /// Déconnecte l'utilisateur (compatible WebView) static Future logoutWebView() async { return KeycloakWebViewAuthService.logout(); } /// Nettoie les données d'authentification WebView static Future clearWebViewAuthData() async { return KeycloakWebViewAuthService.clearAuthData(); } }