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:
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_appauth/flutter_appauth.dart';
|
import 'package:flutter_appauth/flutter_appauth.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.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
|
/// Versucht, eine vorhandene Session aus der Secure Storage zu
|
||||||
/// reaktivieren. Liefert `true`, wenn anschließend ein gültiger
|
/// reaktivieren. Liefert `true`, wenn anschließend ein gültiger
|
||||||
/// Access-Token verfügbar ist.
|
/// 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 {
|
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;
|
if (stored == null || stored.isEmpty) return false;
|
||||||
_refreshToken = stored;
|
_refreshToken = stored;
|
||||||
|
|
||||||
@ -119,7 +133,11 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
|||||||
/// in der Session zu halten.
|
/// in der Session zu halten.
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
_clearSession();
|
_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());
|
_events.add(const AuthLoggedOut());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +205,13 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
|||||||
Future<void> _persistRefreshToken() async {
|
Future<void> _persistRefreshToken() async {
|
||||||
final rt = _refreshToken;
|
final rt = _refreshToken;
|
||||||
if (rt == null) return;
|
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() {
|
void _clearSession() {
|
||||||
|
|||||||
@ -74,10 +74,15 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
RestoreSessionRequested event,
|
RestoreSessionRequested event,
|
||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
final restored = await tokenProvider.restoreSession();
|
try {
|
||||||
if (!restored) {
|
await tokenProvider.restoreSession();
|
||||||
// Kein gültiger Token → bleibt bei Unauthenticated, kein Snackbar
|
// Erfolg landet via Stream als ProviderSessionChanged.loggedIn.
|
||||||
// (das ist der normale Cold-Start-Pfad).
|
// 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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user