import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:http/http.dart' as http; import '../config.dart'; /// Auth OIDC pocket-id : Authorization Code + PKCE via flutter_web_auth_2 /// (schéma custom capté de façon fiable), échange de token en direct. class AuthService { final _storage = const FlutterSecureStorage(); static const _kIdToken = 'id_token'; static const _kRefresh = 'refresh_token'; static const _kExpiry = 'expiry'; Map? _discovery; Future> _disco() async { _discovery ??= jsonDecode((await http.get( Uri.parse('${Config.oidcIssuer}/.well-known/openid-configuration'), )) .body) as Map; return _discovery!; } Future get idToken => _storage.read(key: _kIdToken); Future isLoggedIn() async => (await idToken) != null; String _rand(int n) { final r = Random.secure(); return base64UrlEncode(List.generate(n, (_) => r.nextInt(256))) .replaceAll('=', '') .substring(0, n); } Future login() async { final disco = await _disco(); final verifier = _rand(64); final challenge = base64UrlEncode(sha256.convert(ascii.encode(verifier)).bytes) .replaceAll('=', ''); final state = _rand(24); final authUrl = Uri.parse(disco['authorization_endpoint'] as String).replace(queryParameters: { 'response_type': 'code', 'client_id': Config.oidcClientId, 'redirect_uri': Config.oidcRedirect, 'scope': Config.oidcScopes.join(' '), 'state': state, 'code_challenge': challenge, 'code_challenge_method': 'S256', 'prompt': 'login', }); final result = await FlutterWebAuth2.authenticate( url: authUrl.toString(), callbackUrlScheme: Config.oidcScheme, ); final params = Uri.parse(result).queryParameters; if (params['state'] != state || params['code'] == null) return false; final token = await http.post( Uri.parse(disco['token_endpoint'] as String), headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'grant_type': 'authorization_code', 'code': params['code']!, 'redirect_uri': Config.oidcRedirect, 'client_id': Config.oidcClientId, 'code_verifier': verifier, }, ); if (token.statusCode != 200) return false; await _store(jsonDecode(token.body) as Map); return true; } Future _store(Map t) async { await _storage.write(key: _kIdToken, value: t['id_token'] as String?); if (t['refresh_token'] != null) { await _storage.write(key: _kRefresh, value: t['refresh_token'] as String); } final expiresIn = (t['expires_in'] as num?)?.toInt() ?? 3600; await _storage.write( key: _kExpiry, value: DateTime.now().add(Duration(seconds: expiresIn)).toIso8601String(), ); } /// Rafraîchit si expiré, renvoie l'id_token courant. Future validToken() async { final expiry = DateTime.tryParse(await _storage.read(key: _kExpiry) ?? ''); final refresh = await _storage.read(key: _kRefresh); if (expiry != null && refresh != null && expiry.isBefore(DateTime.now().add(const Duration(seconds: 30)))) { try { final disco = await _disco(); final r = await http.post( Uri.parse(disco['token_endpoint'] as String), headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'grant_type': 'refresh_token', 'refresh_token': refresh, 'client_id': Config.oidcClientId, }, ); if (r.statusCode == 200) await _store(jsonDecode(r.body) as Map); } catch (_) {/* garde l'ancien */} } return idToken; } Future logout() async => _storage.deleteAll(); }