From 08824290ff021841dd5fb8bfddfd7f3b3de6df5b Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Thu, 14 May 2026 23:04:12 +0200 Subject: [PATCH] Phase B: Token-Provider und AuthBloc robust gegen Storage-Plugin-Fehler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beobachtung: Nach 'flutter_secure_storage' frisch dazugepackt ohne Cold-Restart kam eine MissingPluginException auf dem AuthBloc-Stream durch (read auf channel plugins.it_nomads.com/flutter_secure_storage) und hat den ganzen Bloc-Event-Loop mitgerissen. Fix: - KeycloakOidcTokenProvider.restoreSession / _persistRefreshToken / logout fangen Plugin-Exceptions ab und loggen sie über debugPrint, statt sie hochzureichen. Restore-Pfad endet sauber mit 'kein Restore möglich', Login-Pfad hält den Token in Memory weiter. - AuthBloc._handleRestore mit eigener try/catch als zweite Schutzschicht für jeden anderen Fehler aus dem Provider. Bestehender Cold-Restart-Workaround (App stoppen + flutter run) für die ursprüngliche MissingPluginException bleibt natürlich nötig — diese Änderung sorgt nur dafür, dass künftige Storage-Probleme (Keychain zerschossen, Restore-Backup, …) nicht die Auth komplett killen. --- .../network/keycloak_oidc_token_provider.dart | 30 +++++++++++++++++-- .../authentication/bloc/auth_bloc.dart | 13 +++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/data/network/keycloak_oidc_token_provider.dart b/lib/data/network/keycloak_oidc_token_provider.dart index f2e1869..e46eef3 100644 --- a/lib/data/network/keycloak_oidc_token_provider.dart +++ b/lib/data/network/keycloak_oidc_token_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -100,8 +101,21 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider { /// Versucht, eine vorhandene Session aus der Secure Storage zu /// reaktivieren. Liefert `true`, wenn anschließend ein gültiger /// Access-Token verfügbar ist. + /// + /// Schluckt Fehler aus dem Storage-Plugin (z. B. nicht-registriertes + /// Native-Modul nach Plugin-Update ohne Cold-Restart) — der Caller + /// landet dann sauber im "kein Restore möglich"-Pfad statt mit einer + /// MissingPluginException auf dem AuthBloc-Stream. Future restoreSession() async { - final stored = await _storage.read(key: _refreshTokenStorageKey); + final String? stored; + try { + stored = await _storage.read(key: _refreshTokenStorageKey); + } catch (e, st) { + debugPrint( + 'restoreSession: konnte Refresh-Token nicht lesen: $e\n$st', + ); + return false; + } if (stored == null || stored.isEmpty) return false; _refreshToken = stored; @@ -119,7 +133,11 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider { /// in der Session zu halten. Future logout() async { _clearSession(); - await _storage.delete(key: _refreshTokenStorageKey); + try { + await _storage.delete(key: _refreshTokenStorageKey); + } catch (e) { + debugPrint('logout: Refresh-Token konnte nicht gelöscht werden: $e'); + } _events.add(const AuthLoggedOut()); } @@ -187,7 +205,13 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider { Future _persistRefreshToken() async { final rt = _refreshToken; if (rt == null) return; - await _storage.write(key: _refreshTokenStorageKey, value: rt); + try { + await _storage.write(key: _refreshTokenStorageKey, value: rt); + } catch (e) { + // Nicht fatal — Refresh-Token bleibt in-memory verfügbar, + // wir verlieren nur das Restore-Verhalten beim nächsten Start. + debugPrint('Refresh-Token konnte nicht persistiert werden: $e'); + } } void _clearSession() { diff --git a/lib/feature/authentication/bloc/auth_bloc.dart b/lib/feature/authentication/bloc/auth_bloc.dart index 1c744e6..586cb08 100644 --- a/lib/feature/authentication/bloc/auth_bloc.dart +++ b/lib/feature/authentication/bloc/auth_bloc.dart @@ -74,10 +74,15 @@ class AuthBloc extends Bloc { RestoreSessionRequested event, Emitter emit, ) async { - final restored = await tokenProvider.restoreSession(); - if (!restored) { - // Kein gültiger Token → bleibt bei Unauthenticated, kein Snackbar - // (das ist der normale Cold-Start-Pfad). + try { + await tokenProvider.restoreSession(); + // Erfolg landet via Stream als ProviderSessionChanged.loggedIn. + // Misserfolg: bleibt bei Unauthenticated — kein Snackbar, das + // ist der normale Cold-Start-Pfad. + } catch (err, st) { + debugPrint('Restore-Session fehlgeschlagen: $err\n$st'); + // State unverändert (Unauthenticated). Kein Snackbar — das + // wäre für den Cold-Start zu viel Lärm. } }