Compare commits

...
2 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 f0b32ff68b fix(auth): skip Flutter OIDC on web, rely on NPM forward auth
Build and Push / release (push) Successful in 4s
Build and Push / build (push) Successful in 3m9s
On web, NPM forward auth handles authentication at the proxy level.
By the time the Flutter app loads, the user is already authenticated.
Skip the redundant Flutter OIDC flow that was causing Riverpod
"Ref disposed" errors from conflicting auth state updates.

Mobile still uses Flutter's OIDC flow as before.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:01:33 +01:00
Jeroen SchweitzerandClaude Opus 4.5 790ae41171 fix(auth): Riverpod lifecycle error + Authentik logout
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m2s
- 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 <noreply@anthropic.com>
2026-01-04 14:50:32 +01:00
6 changed files with 80 additions and 6 deletions
+15
View File
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.2] - 2026-01-04
### Changed
- **Web auth simplified**: Skip Flutter OIDC on web - NPM forward auth handles it
- NPM authenticates at proxy level before app loads
- No more redundant OIDC redirect after NPM auth completes
- Fixes "Cannot use Ref after disposed" error from conflicting auth flows
- Mobile still uses Flutter OIDC flow
### 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
## [1.1.0] - 2026-01-04
### Changed
+18 -1
View File
@@ -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<void> 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.
+27
View File
@@ -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<String> 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 = <String, String>{
'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();
}
}
+10 -4
View File
@@ -132,6 +132,9 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
}
Future<void> _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) {
+9
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -45,6 +46,14 @@ class _AppScaffoldState extends ConsumerState<AppScaffold> {
return _buildScaffold(context);
}
// On web, NPM forward auth handles authentication at the proxy level.
// If we reach this point, the user is already authenticated by NPM.
// No need for Flutter's OIDC flow - just show the app.
if (kIsWeb) {
return _buildScaffold(context);
}
// Mobile: Use Flutter's OIDC flow
final authAsync = ref.watch(authProvider);
return authAsync.when(
+1 -1
View File
@@ -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.2+1
environment:
sdk: ^3.10.4