From 790ae411716d84cf27f99507a117fc93896f4830 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 4 Jan 2026 14:50:32 +0100 Subject: [PATCH] fix(auth): Riverpod lifecycle error + Authentik logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "Cannot use Ref after disposed" error in OIDC callback page - Store notifier reference before async gap - Add mounted check at start of processing - Add proper SSO logout via Authentik end_session_endpoint - Clears local tokens AND redirects to Authentik logout - Returns to app after Authentik session ends 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 14 ++++++++++++++ lib/core/auth/auth_provider.dart | 19 ++++++++++++++++++- lib/core/auth/oidc_service_web.dart | 27 +++++++++++++++++++++++++++ lib/routing/app_router.dart | 14 ++++++++++---- pubspec.yaml | 2 +- 5 files changed, 70 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e80fb4d..25cad49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.1] - 2026-01-04 + +### Added +- Logout now redirects to Authentik to end SSO session + - Clears local tokens AND invalidates Authentik session + - Uses OIDC end_session_endpoint from discovery document + - Redirects back to app after Authentik logout completes + +### Fixed +- Fixed Riverpod lifecycle error in OIDC callback page + - "Cannot use the Ref of authProvider after it has been disposed" + - Store notifier reference before async gap to prevent disposed ref access + - Add mounted check at start of callback processing + ## [1.1.0] - 2026-01-04 ### Changed diff --git a/lib/core/auth/auth_provider.dart b/lib/core/auth/auth_provider.dart index bb4b0a5..14a1320 100644 --- a/lib/core/auth/auth_provider.dart +++ b/lib/core/auth/auth_provider.dart @@ -341,10 +341,27 @@ class AuthNotifier extends _$AuthNotifier { } /// Sign out and clear stored credentials. + /// + /// On web, also redirects to Authentik's logout endpoint to end the SSO session. Future signOut() async { + // Clear local storage first await _clearStoredAuth(); state = const AsyncData(AuthState()); - developer.log('Signed out', name: 'auth'); + developer.log('Signed out locally', name: 'auth'); + + // On web, redirect to Authentik logout to end SSO session + if (kIsWeb && AppConfig.requiresAuth) { + try { + final oidcService = OidcServiceWeb(); + final logoutUrl = await oidcService.getLogoutUrl(); + developer.log('Redirecting to Authentik logout', name: 'auth'); + web_utils.redirectTo(logoutUrl); + } catch (e) { + developer.log('Failed to get logout URL: $e', name: 'auth'); + // Local logout already done, just reload to trigger re-auth + web_utils.redirectTo('/'); + } + } } /// Update user preferences. diff --git a/lib/core/auth/oidc_service_web.dart b/lib/core/auth/oidc_service_web.dart index 107f8ed..d86459e 100644 --- a/lib/core/auth/oidc_service_web.dart +++ b/lib/core/auth/oidc_service_web.dart @@ -209,4 +209,31 @@ class OidcServiceWeb implements OidcService { return List.generate(length, (_) => chars[random.nextInt(chars.length)]) .join(); } + + /// Get the logout URL to redirect the browser to for SSO logout. + /// + /// [idToken] is optional but recommended for logout verification. + /// After logout, Authentik redirects back to [postLogoutRedirectUri]. + Future getLogoutUrl({String? idToken}) async { + final discovery = await _fetchDiscovery(); + final endSessionEndpoint = discovery['end_session_endpoint'] as String?; + + if (endSessionEndpoint == null) { + // Fallback: just redirect to home, local state already cleared + developer.log('No end_session_endpoint in discovery', name: 'oidc_web'); + return AppConfig.webBaseUrl; + } + + final params = { + 'post_logout_redirect_uri': AppConfig.webBaseUrl, + }; + + if (idToken != null) { + params['id_token_hint'] = idToken; + } + + final uri = Uri.parse(endSessionEndpoint).replace(queryParameters: params); + developer.log('Logout URL: $uri', name: 'oidc_web'); + return uri.toString(); + } } diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index affd72f..252b6bb 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -132,6 +132,9 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> { } Future _processCallback() async { + // Check mounted before any async work + if (!mounted) return; + // Check for error from Authentik if (widget.error != null) { setState(() { @@ -150,12 +153,15 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> { return; } + // Get notifier reference before async gap to avoid disposed ref errors + final authNotifier = ref.read(authProvider.notifier); + // Exchange code for tokens try { - await ref.read(authProvider.notifier).handleOidcCallback( - widget.code!, - widget.callbackState!, - ); + await authNotifier.handleOidcCallback( + widget.code!, + widget.callbackState!, + ); // Navigate to home on success if (mounted) { diff --git a/pubspec.yaml b/pubspec.yaml index 17cdae6..4eb66a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.0+1 +version: 1.1.1+1 environment: sdk: ^3.10.4