Phase B: Token-Provider und AuthBloc robust gegen Storage-Plugin-Fehler

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.
This commit is contained in:
Dennis Nemec
2026-05-14 23:04:12 +02:00
parent 6d7e58fc0f
commit 08824290ff
2 changed files with 36 additions and 7 deletions

View File

@ -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<bool> 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<void> 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<void> _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() {

View File

@ -74,10 +74,15 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
RestoreSessionRequested event,
Emitter<AuthState> 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.
}
}