Auth: - profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password Multi-org (Phase 3): - OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry - org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role Navigation: - MorePage: navigation conditionnelle par typeOrganisation - Suppression adaptive_navigation (remplacé par main_navigation_layout) Auth AppAuth: - keycloak_webview_auth_service: fixes AppAuth Android - AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet Onboarding: - Nouveaux états: payment_method_page, onboarding_shared_widgets - SouscriptionStatusModel mis à jour StatutValidationSouscription Android: - build.gradle: ProGuard/R8, network_security_config - Gradle wrapper mis à jour
685 lines
23 KiB
Dart
685 lines
23 KiB
Dart
/// Service d'Authentification Keycloak via WebView
|
|
///
|
|
/// Implémentation professionnelle et sécurisée de l'authentification OAuth2/OIDC
|
|
/// avec Keycloak utilisant WebView pour contourner les limitations HTTPS de flutter_appauth.
|
|
///
|
|
/// Fonctionnalités :
|
|
/// - Flow OAuth2 Authorization Code avec PKCE
|
|
/// - Gestion sécurisée des tokens JWT
|
|
/// - Support HTTP/HTTPS
|
|
/// - Gestion complète des erreurs et timeouts
|
|
/// - Validation rigoureuse des paramètres
|
|
/// - Logging détaillé pour le debugging
|
|
library keycloak_webview_auth_service;
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
|
import '../models/user.dart';
|
|
import '../models/user_role.dart';
|
|
import 'keycloak_role_mapper.dart';
|
|
import '../../../../core/config/environment.dart';
|
|
|
|
/// Configuration Keycloak pour l'authentification WebView
|
|
class KeycloakWebViewConfig {
|
|
/// URL de base de l'instance Keycloak (depuis AppConfig)
|
|
static String get baseUrl => AppConfig.keycloakBaseUrl;
|
|
|
|
/// 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 OAuth2 demandés
|
|
static const List<String> scopes = ['openid', 'profile', 'email', 'roles'];
|
|
|
|
/// Timeout pour les requêtes HTTP (en secondes)
|
|
static const int httpTimeoutSeconds = 30;
|
|
|
|
/// Timeout pour l'authentification WebView (en secondes)
|
|
static const int authTimeoutSeconds = 300; // 5 minutes
|
|
|
|
/// 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';
|
|
|
|
static String get jwksEndpoint =>
|
|
'$baseUrl/realms/$realm/protocol/openid-connect/certs';
|
|
}
|
|
|
|
/// Résultat de l'authentification WebView
|
|
class WebViewAuthResult {
|
|
final String accessToken;
|
|
final String idToken;
|
|
final String? refreshToken;
|
|
final int expiresIn;
|
|
final String tokenType;
|
|
final List<String> scopes;
|
|
|
|
const WebViewAuthResult({
|
|
required this.accessToken,
|
|
required this.idToken,
|
|
this.refreshToken,
|
|
required this.expiresIn,
|
|
required this.tokenType,
|
|
required this.scopes,
|
|
});
|
|
|
|
/// Création depuis la réponse token de Keycloak
|
|
factory WebViewAuthResult.fromTokenResponse(Map<String, dynamic> response) {
|
|
return WebViewAuthResult(
|
|
accessToken: response['access_token'] ?? '',
|
|
idToken: response['id_token'] ?? '',
|
|
refreshToken: response['refresh_token'],
|
|
expiresIn: response['expires_in'] ?? 3600,
|
|
tokenType: response['token_type'] ?? 'Bearer',
|
|
scopes: (response['scope'] as String?)?.split(' ') ?? [],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Exceptions spécifiques à l'authentification WebView
|
|
class KeycloakWebViewAuthException implements Exception {
|
|
final String message;
|
|
final String? code;
|
|
final dynamic originalError;
|
|
|
|
const KeycloakWebViewAuthException(
|
|
this.message, {
|
|
this.code,
|
|
this.originalError,
|
|
});
|
|
|
|
@override
|
|
String toString() => 'KeycloakWebViewAuthException: $message${code != null ? ' (Code: $code)' : ''}';
|
|
}
|
|
|
|
/// Service d'authentification Keycloak via WebView
|
|
///
|
|
/// Implémentation complète et sécurisée du flow OAuth2 Authorization Code avec PKCE
|
|
class KeycloakWebViewAuthService {
|
|
// Stockage sécurisé des tokens
|
|
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
|
|
aOptions: AndroidOptions(
|
|
encryptedSharedPreferences: true,
|
|
),
|
|
iOptions: IOSOptions(
|
|
accessibility: KeychainAccessibility.first_unlock_this_device,
|
|
),
|
|
);
|
|
|
|
// Clés de stockage sécurisé — alignées avec KeycloakAuthService pour éviter IC-03
|
|
// KeycloakAuthService lit 'kc_access' / 'kc_refresh' / 'kc_id' ; ApiClient aussi.
|
|
static const String _accessTokenKey = 'kc_access';
|
|
static const String _idTokenKey = 'kc_id';
|
|
static const String _refreshTokenKey = 'kc_refresh';
|
|
static const String _userInfoKey = 'keycloak_webview_user_info';
|
|
static const String _authStateKey = 'keycloak_webview_auth_state';
|
|
|
|
// Client HTTP avec timeout configuré
|
|
static final http.Client _httpClient = http.Client();
|
|
|
|
/// Génère un code verifier PKCE sécurisé
|
|
static String _generateCodeVerifier() {
|
|
const String charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
final Random random = Random.secure();
|
|
return List.generate(128, (i) => charset[random.nextInt(charset.length)]).join();
|
|
}
|
|
|
|
/// Génère le code challenge PKCE à partir du verifier
|
|
static String _generateCodeChallenge(String verifier) {
|
|
final List<int> bytes = utf8.encode(verifier);
|
|
final Digest digest = sha256.convert(bytes);
|
|
return base64Url.encode(digest.bytes).replaceAll('=', '');
|
|
}
|
|
|
|
/// Génère un state sécurisé pour la protection CSRF
|
|
static String _generateState() {
|
|
final Random random = Random.secure();
|
|
final List<int> bytes = List.generate(32, (i) => random.nextInt(256));
|
|
return base64Url.encode(bytes).replaceAll('=', '');
|
|
}
|
|
|
|
/// Construit l'URL d'autorisation Keycloak avec tous les paramètres
|
|
static Future<Map<String, String>> _buildAuthorizationUrl() async {
|
|
final String codeVerifier = _generateCodeVerifier();
|
|
final String codeChallenge = _generateCodeChallenge(codeVerifier);
|
|
final String state = _generateState();
|
|
|
|
// Stocker les paramètres pour la validation ultérieure
|
|
await _secureStorage.write(
|
|
key: _authStateKey,
|
|
value: jsonEncode({
|
|
'code_verifier': codeVerifier,
|
|
'state': state,
|
|
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
|
}),
|
|
);
|
|
|
|
final Map<String, String> params = {
|
|
'response_type': 'code',
|
|
'client_id': KeycloakWebViewConfig.clientId,
|
|
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
|
|
'scope': KeycloakWebViewConfig.scopes.join(' '),
|
|
'state': state,
|
|
'code_challenge': codeChallenge,
|
|
'code_challenge_method': 'S256',
|
|
'kc_locale': 'fr',
|
|
'prompt': 'login',
|
|
};
|
|
|
|
final String queryString = params.entries
|
|
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
|
|
.join('&');
|
|
|
|
final String authUrl = '${KeycloakWebViewConfig.authorizationEndpoint}?$queryString';
|
|
|
|
debugPrint('🔐 URL d\'autorisation générée: $authUrl');
|
|
|
|
return {
|
|
'url': authUrl,
|
|
'state': state,
|
|
'code_verifier': codeVerifier,
|
|
};
|
|
}
|
|
|
|
/// Valide la réponse de redirection et extrait le code d'autorisation
|
|
static Future<String> _validateCallbackAndExtractCode(
|
|
String callbackUrl,
|
|
String expectedState,
|
|
) async {
|
|
debugPrint('🔍 Validation du callback: $callbackUrl');
|
|
|
|
final Uri uri = Uri.parse(callbackUrl);
|
|
|
|
// Vérifier que c'est bien notre URL de redirection
|
|
if (!callbackUrl.startsWith(KeycloakWebViewConfig.redirectUrl)) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'URL de callback invalide',
|
|
code: 'INVALID_CALLBACK_URL',
|
|
);
|
|
}
|
|
|
|
// Vérifier la présence d'erreurs
|
|
final String? error = uri.queryParameters['error'];
|
|
if (error != null) {
|
|
final String? errorDescription = uri.queryParameters['error_description'];
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur d\'authentification: ${errorDescription ?? error}',
|
|
code: error,
|
|
);
|
|
}
|
|
|
|
// Valider le state pour la protection CSRF
|
|
final String? receivedState = uri.queryParameters['state'];
|
|
if (receivedState != expectedState) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'State invalide - possible attaque CSRF',
|
|
code: 'INVALID_STATE',
|
|
);
|
|
}
|
|
|
|
// Extraire le code d'autorisation
|
|
final String? code = uri.queryParameters['code'];
|
|
if (code == null || code.isEmpty) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'Code d\'autorisation manquant',
|
|
code: 'MISSING_AUTH_CODE',
|
|
);
|
|
}
|
|
|
|
debugPrint('✅ Code d\'autorisation extrait avec succès');
|
|
return code;
|
|
}
|
|
|
|
/// Échange le code d'autorisation contre des tokens
|
|
static Future<WebViewAuthResult> _exchangeCodeForTokens(
|
|
String authCode,
|
|
String codeVerifier,
|
|
) async {
|
|
debugPrint('🔄 Échange du code d\'autorisation contre les tokens...');
|
|
|
|
try {
|
|
final Map<String, String> body = {
|
|
'grant_type': 'authorization_code',
|
|
'client_id': KeycloakWebViewConfig.clientId,
|
|
'code': authCode,
|
|
'redirect_uri': KeycloakWebViewConfig.redirectUrl,
|
|
'code_verifier': codeVerifier,
|
|
};
|
|
|
|
final http.Response response = await _httpClient
|
|
.post(
|
|
Uri.parse(KeycloakWebViewConfig.tokenEndpoint),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Accept': 'application/json',
|
|
},
|
|
body: body,
|
|
)
|
|
.timeout(const Duration(seconds: KeycloakWebViewConfig.httpTimeoutSeconds));
|
|
|
|
debugPrint('📡 Réponse token endpoint: ${response.statusCode}');
|
|
|
|
if (response.statusCode != 200) {
|
|
final String errorBody = response.body;
|
|
debugPrint('❌ Erreur échange tokens: $errorBody');
|
|
|
|
Map<String, dynamic>? errorJson;
|
|
try {
|
|
errorJson = jsonDecode(errorBody);
|
|
} catch (e) {
|
|
// Ignore JSON parsing errors
|
|
}
|
|
|
|
final String errorMessage = errorJson?['error_description'] ??
|
|
errorJson?['error'] ??
|
|
'Erreur HTTP ${response.statusCode}';
|
|
|
|
throw KeycloakWebViewAuthException(
|
|
'Échec de l\'échange de tokens: $errorMessage',
|
|
code: errorJson?['error'],
|
|
);
|
|
}
|
|
|
|
final Map<String, dynamic> tokenResponse = jsonDecode(response.body);
|
|
|
|
// Valider la présence des tokens requis
|
|
if (!tokenResponse.containsKey('access_token') ||
|
|
!tokenResponse.containsKey('id_token')) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'Tokens manquants dans la réponse',
|
|
code: 'MISSING_TOKENS',
|
|
);
|
|
}
|
|
|
|
debugPrint('✅ Tokens reçus avec succès');
|
|
return WebViewAuthResult.fromTokenResponse(tokenResponse);
|
|
|
|
} on TimeoutException {
|
|
throw const KeycloakWebViewAuthException(
|
|
'Timeout lors de l\'échange des tokens',
|
|
code: 'TIMEOUT',
|
|
);
|
|
} catch (e) {
|
|
if (e is KeycloakWebViewAuthException) rethrow;
|
|
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors de l\'échange des tokens: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Stocke les tokens de manière sécurisée
|
|
static Future<void> _storeTokens(WebViewAuthResult authResult) async {
|
|
debugPrint('💾 Stockage sécurisé des tokens...');
|
|
|
|
try {
|
|
await Future.wait([
|
|
_secureStorage.write(key: _accessTokenKey, value: authResult.accessToken),
|
|
_secureStorage.write(key: _idTokenKey, value: authResult.idToken),
|
|
if (authResult.refreshToken != null)
|
|
_secureStorage.write(key: _refreshTokenKey, value: authResult.refreshToken!),
|
|
]);
|
|
|
|
debugPrint('✅ Tokens stockés avec succès');
|
|
} catch (e) {
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors du stockage des tokens: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Valide et parse un token JWT
|
|
///
|
|
/// Stratégie de validation JWT :
|
|
/// - Côté mobile : vérification de l'expiration et de l'issuer uniquement.
|
|
/// La signature JWT n'est PAS vérifiée côté mobile car :
|
|
/// 1. Le token est envoyé au backend dans chaque requête API (header Authorization)
|
|
/// 2. Le backend Quarkus valide la signature via JWKS (quarkus-oidc)
|
|
/// 3. Toutes les communications passent par HTTPS en production
|
|
/// 4. Le token est stocké dans FlutterSecureStorage (EncryptedSharedPreferences / Keychain)
|
|
/// - Si une validation de signature locale est requise, implémenter via le
|
|
/// endpoint JWKS : `KeycloakWebViewConfig.jwksEndpoint`
|
|
static Map<String, dynamic> _parseAndValidateJWT(String token, String tokenType) {
|
|
try {
|
|
// Vérifier l'expiration
|
|
if (JwtDecoder.isExpired(token)) {
|
|
throw KeycloakWebViewAuthException(
|
|
'$tokenType expiré',
|
|
code: 'TOKEN_EXPIRED',
|
|
);
|
|
}
|
|
|
|
// Parser le payload
|
|
final Map<String, dynamic> payload = JwtDecoder.decode(token);
|
|
|
|
// Validations de base
|
|
if (payload['iss'] == null) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'Token JWT invalide: issuer manquant',
|
|
code: 'INVALID_JWT',
|
|
);
|
|
}
|
|
|
|
// Vérifier l'issuer
|
|
final String expectedIssuer = '${KeycloakWebViewConfig.baseUrl}/realms/${KeycloakWebViewConfig.realm}';
|
|
if (payload['iss'] != expectedIssuer) {
|
|
throw KeycloakWebViewAuthException(
|
|
'Token JWT invalide: issuer incorrect (attendu: $expectedIssuer, reçu: ${payload['iss']})',
|
|
code: 'INVALID_ISSUER',
|
|
);
|
|
}
|
|
|
|
debugPrint('✅ $tokenType validé avec succès');
|
|
return payload;
|
|
|
|
} catch (e) {
|
|
if (e is KeycloakWebViewAuthException) rethrow;
|
|
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors de la validation du $tokenType: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Méthode principale d'authentification
|
|
///
|
|
/// Retourne les paramètres nécessaires pour lancer la WebView d'authentification
|
|
static Future<Map<String, String>> prepareAuthentication() async {
|
|
debugPrint('🚀 Préparation de l\'authentification WebView...');
|
|
|
|
try {
|
|
// Nettoyer les données d'authentification précédentes
|
|
await clearAuthData();
|
|
|
|
// Générer l'URL d'autorisation avec PKCE
|
|
final Map<String, String> authParams = await _buildAuthorizationUrl();
|
|
|
|
debugPrint('✅ Authentification préparée avec succès');
|
|
return authParams;
|
|
|
|
} catch (e) {
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors de la préparation de l\'authentification: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Traite le callback de redirection et finalise l'authentification
|
|
static Future<User> handleAuthCallback(String callbackUrl) async {
|
|
debugPrint('🔄 Traitement du callback d\'authentification...');
|
|
debugPrint('📋 URL de callback: $callbackUrl');
|
|
|
|
try {
|
|
// Récupérer les paramètres d'authentification stockés
|
|
debugPrint('🔍 Récupération de l\'état d\'authentification...');
|
|
final String? authStateJson = await _secureStorage.read(key: _authStateKey);
|
|
if (authStateJson == null) {
|
|
debugPrint('❌ État d\'authentification manquant');
|
|
throw const KeycloakWebViewAuthException(
|
|
'État d\'authentification manquant',
|
|
code: 'MISSING_AUTH_STATE',
|
|
);
|
|
}
|
|
|
|
final Map<String, dynamic> authState = jsonDecode(authStateJson);
|
|
final String expectedState = authState['state'];
|
|
final String codeVerifier = authState['code_verifier'];
|
|
debugPrint('✅ État d\'authentification récupéré');
|
|
|
|
// Valider le callback et extraire le code
|
|
debugPrint('🔍 Validation du callback...');
|
|
final String authCode = await _validateCallbackAndExtractCode(
|
|
callbackUrl,
|
|
expectedState,
|
|
);
|
|
debugPrint('✅ Code d\'autorisation extrait: ${authCode.substring(0, 10)}...');
|
|
|
|
// Échanger le code contre des tokens
|
|
debugPrint('🔄 Échange du code contre les tokens...');
|
|
final WebViewAuthResult authResult = await _exchangeCodeForTokens(
|
|
authCode,
|
|
codeVerifier,
|
|
);
|
|
debugPrint('✅ Tokens reçus avec succès');
|
|
|
|
// Stocker les tokens
|
|
debugPrint('💾 Stockage des tokens...');
|
|
await _storeTokens(authResult);
|
|
debugPrint('✅ Tokens stockés');
|
|
|
|
// Créer l'utilisateur depuis les tokens
|
|
debugPrint('👤 Création de l\'utilisateur...');
|
|
final User user = await _createUserFromTokens(authResult);
|
|
debugPrint('✅ Utilisateur créé: ${user.fullName}');
|
|
|
|
// Nettoyer l'état d'authentification temporaire
|
|
await _secureStorage.delete(key: _authStateKey);
|
|
|
|
debugPrint('🎉 Authentification WebView terminée avec succès');
|
|
return user;
|
|
|
|
} catch (e, stackTrace) {
|
|
debugPrint('💥 Erreur lors du traitement du callback: $e');
|
|
debugPrint('📋 Stack trace: $stackTrace');
|
|
|
|
// Nettoyer en cas d'erreur
|
|
await _secureStorage.delete(key: _authStateKey);
|
|
|
|
if (e is KeycloakWebViewAuthException) rethrow;
|
|
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors du traitement du callback: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Crée un utilisateur depuis les tokens JWT
|
|
static Future<User> _createUserFromTokens(WebViewAuthResult authResult) async {
|
|
debugPrint('👤 Création de l\'utilisateur depuis les tokens...');
|
|
|
|
try {
|
|
// Parser et valider les tokens
|
|
final Map<String, dynamic> accessTokenPayload = _parseAndValidateJWT(
|
|
authResult.accessToken,
|
|
'Access Token',
|
|
);
|
|
final Map<String, dynamic> idTokenPayload = _parseAndValidateJWT(
|
|
authResult.idToken,
|
|
'ID Token',
|
|
);
|
|
|
|
// 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'] ?? '';
|
|
|
|
if (userId.isEmpty || email.isEmpty) {
|
|
throw const KeycloakWebViewAuthException(
|
|
'Informations utilisateur manquantes dans les tokens',
|
|
code: 'MISSING_USER_INFO',
|
|
);
|
|
}
|
|
|
|
// Extraire les rôles Keycloak
|
|
final List<String> keycloakRoles = _extractKeycloakRoles(accessTokenPayload);
|
|
|
|
// Mapper vers notre système de rôles
|
|
final UserRole primaryRole = KeycloakRoleMapper.mapToUserRole(keycloakRoles);
|
|
debugPrint('🔐 [AUTH WebView] Rôles: $keycloakRoles → UserRole: ${primaryRole.name} (${primaryRole.displayName})');
|
|
final List<String> permissions = KeycloakRoleMapper.mapToPermissions(keycloakRoles);
|
|
|
|
// Créer l'utilisateur
|
|
final User user = User(
|
|
id: userId,
|
|
email: email,
|
|
firstName: firstName,
|
|
lastName: lastName,
|
|
primaryRole: primaryRole,
|
|
organizationContexts: const [],
|
|
additionalPermissions: permissions,
|
|
revokedPermissions: const [],
|
|
preferences: const UserPreferences(
|
|
language: 'fr',
|
|
theme: 'system',
|
|
notificationsEnabled: true,
|
|
emailNotifications: true,
|
|
pushNotifications: true,
|
|
dashboardLayout: 'adaptive',
|
|
timezone: 'Europe/Paris',
|
|
),
|
|
lastLoginAt: DateTime.now(),
|
|
createdAt: DateTime.now(),
|
|
isActive: true,
|
|
);
|
|
|
|
// Stocker les informations utilisateur
|
|
await _secureStorage.write(
|
|
key: _userInfoKey,
|
|
value: jsonEncode(user.toJson()),
|
|
);
|
|
|
|
debugPrint('✅ Utilisateur créé: ${user.fullName} (${user.primaryRole.displayName})');
|
|
return user;
|
|
|
|
} catch (e) {
|
|
if (e is KeycloakWebViewAuthException) rethrow;
|
|
|
|
throw KeycloakWebViewAuthException(
|
|
'Erreur lors de la création de l\'utilisateur: $e',
|
|
originalError: e,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Extrait les rôles Keycloak depuis le payload du token
|
|
static List<String> _extractKeycloakRoles(Map<String, dynamic> tokenPayload) {
|
|
try {
|
|
final List<String> roles = <String>[];
|
|
|
|
// Rôles realm
|
|
final Map<String, dynamic>? realmAccess = tokenPayload['realm_access'];
|
|
if (realmAccess != null && realmAccess['roles'] is List) {
|
|
roles.addAll(List<String>.from(realmAccess['roles']));
|
|
}
|
|
|
|
// Rôles client
|
|
final Map<String, dynamic>? resourceAccess = tokenPayload['resource_access'];
|
|
if (resourceAccess != null) {
|
|
final Map<String, dynamic>? clientAccess = resourceAccess[KeycloakWebViewConfig.clientId];
|
|
if (clientAccess != null && clientAccess['roles'] is List) {
|
|
roles.addAll(List<String>.from(clientAccess['roles']));
|
|
}
|
|
}
|
|
|
|
// Filtrer les rôles système
|
|
return roles.where((role) =>
|
|
!role.startsWith('default-roles-') &&
|
|
role != 'offline_access' &&
|
|
role != 'uma_authorization'
|
|
).toList();
|
|
|
|
} catch (e) {
|
|
debugPrint('💥 Erreur extraction rôles: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Nettoie toutes les données d'authentification
|
|
static Future<void> clearAuthData() async {
|
|
debugPrint('🧹 Nettoyage des données d\'authentification...');
|
|
|
|
try {
|
|
await Future.wait([
|
|
_secureStorage.delete(key: _accessTokenKey),
|
|
_secureStorage.delete(key: _idTokenKey),
|
|
_secureStorage.delete(key: _refreshTokenKey),
|
|
_secureStorage.delete(key: _userInfoKey),
|
|
_secureStorage.delete(key: _authStateKey),
|
|
]);
|
|
|
|
debugPrint('✅ Données d\'authentification nettoyées');
|
|
} catch (e) {
|
|
debugPrint('⚠️ Erreur lors du nettoyage: $e');
|
|
}
|
|
}
|
|
|
|
/// Vérifie si l'utilisateur est authentifié
|
|
static Future<bool> isAuthenticated() async {
|
|
try {
|
|
final String? accessToken = await _secureStorage.read(key: _accessTokenKey);
|
|
|
|
if (accessToken == null) {
|
|
return false;
|
|
}
|
|
|
|
// Vérifier si le token est expiré
|
|
return !JwtDecoder.isExpired(accessToken);
|
|
|
|
} catch (e) {
|
|
debugPrint('💥 Erreur vérification authentification: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Récupère l'utilisateur authentifié
|
|
static Future<User?> getCurrentUser() async {
|
|
try {
|
|
final String? userInfoJson = await _secureStorage.read(key: _userInfoKey);
|
|
|
|
if (userInfoJson == null) {
|
|
return null;
|
|
}
|
|
|
|
final Map<String, dynamic> userJson = jsonDecode(userInfoJson);
|
|
return User.fromJson(userJson);
|
|
|
|
} catch (e) {
|
|
debugPrint('💥 Erreur récupération utilisateur: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Déconnecte l'utilisateur
|
|
static Future<bool> logout() async {
|
|
debugPrint('🚪 Déconnexion de l\'utilisateur...');
|
|
|
|
try {
|
|
// Nettoyer les données locales
|
|
await clearAuthData();
|
|
|
|
debugPrint('✅ Déconnexion réussie');
|
|
return true;
|
|
|
|
} catch (e) {
|
|
debugPrint('💥 Erreur déconnexion: $e');
|
|
return false;
|
|
}
|
|
}
|
|
}
|