Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b32ff68b | ||
|
|
790ae41171 |
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.1.0] - 2026-01-04
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -341,10 +341,27 @@ class AuthNotifier extends _$AuthNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sign out and clear stored credentials.
|
/// Sign out and clear stored credentials.
|
||||||
|
///
|
||||||
|
/// On web, also redirects to Authentik's logout endpoint to end the SSO session.
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
|
// Clear local storage first
|
||||||
await _clearStoredAuth();
|
await _clearStoredAuth();
|
||||||
state = const AsyncData(AuthState());
|
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.
|
/// Update user preferences.
|
||||||
|
|||||||
@@ -209,4 +209,31 @@ class OidcServiceWeb implements OidcService {
|
|||||||
return List.generate(length, (_) => chars[random.nextInt(chars.length)])
|
return List.generate(length, (_) => chars[random.nextInt(chars.length)])
|
||||||
.join();
|
.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processCallback() async {
|
Future<void> _processCallback() async {
|
||||||
|
// Check mounted before any async work
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
// Check for error from Authentik
|
// Check for error from Authentik
|
||||||
if (widget.error != null) {
|
if (widget.error != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -150,12 +153,15 @@ class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get notifier reference before async gap to avoid disposed ref errors
|
||||||
|
final authNotifier = ref.read(authProvider.notifier);
|
||||||
|
|
||||||
// Exchange code for tokens
|
// Exchange code for tokens
|
||||||
try {
|
try {
|
||||||
await ref.read(authProvider.notifier).handleOidcCallback(
|
await authNotifier.handleOidcCallback(
|
||||||
widget.code!,
|
widget.code!,
|
||||||
widget.callbackState!,
|
widget.callbackState!,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate to home on success
|
// Navigate to home on success
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -45,6 +46,14 @@ class _AppScaffoldState extends ConsumerState<AppScaffold> {
|
|||||||
return _buildScaffold(context);
|
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);
|
final authAsync = ref.watch(authProvider);
|
||||||
|
|
||||||
return authAsync.when(
|
return authAsync.when(
|
||||||
|
|||||||
+1
-1
@@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.10.4
|
sdk: ^3.10.4
|
||||||
|
|||||||
Reference in New Issue
Block a user