diff --git a/config.sample.json b/config.sample.json index 5aa0a4bb0..c9a090aa9 100644 --- a/config.sample.json +++ b/config.sample.json @@ -25,5 +25,6 @@ "noEncryptionWarningShown": false, "displayChatDetailsColumn": false, "colorSchemeSeedInt": 4283835834, - "enableSoftLogout": false + "enableSoftLogout": false, + "autoSsoRedirect": false } \ No newline at end of file diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 60e788786..19fa47902 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; +import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/archive/archive.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; @@ -63,6 +64,8 @@ abstract class AppRoutes { redirect: (context, state) => Matrix.of(context).widget.clients.any((client) => client.isLogged()) ? '/rooms' + : AppSettings.autoSsoRedirect.value + ? '/home/sign_in' : '/home', ), GoRoute( diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index 7011e8645..03d5cd8a5 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -52,7 +52,8 @@ enum AppSettings { // colorSchemeSeed stored as ARGB int colorSchemeSeedInt('chat.fluffy.color_scheme_seed', 0xFF5625BA), emojiSuggestionLocale('emoji_suggestion_locale', ''), - enableSoftLogout('chat.fluffy.enable_soft_logout', false); + enableSoftLogout('chat.fluffy.enable_soft_logout', false), + autoSsoRedirect('chat.fluffy.auto_sso_redirect', false); final String key; final T defaultValue; diff --git a/lib/pages/sign_in/sign_in_page.dart b/lib/pages/sign_in/sign_in_page.dart index 8dd4390a2..354491a16 100644 --- a/lib/pages/sign_in/sign_in_page.dart +++ b/lib/pages/sign_in/sign_in_page.dart @@ -22,6 +22,7 @@ class SignInPage extends StatelessWidget { final theme = Theme.of(context); return ViewModelBuilder( create: () => SignInViewModel(Matrix.of(context), signUp: signUp), + onCreated: (context, viewModel) => viewModel.autoSsoIfEnabled(context), builder: (context, viewModel, _) { final state = viewModel.value; final publicHomeservers = state.filteredPublicHomeservers; diff --git a/lib/pages/sign_in/view_model/sign_in_view_model.dart b/lib/pages/sign_in/view_model/sign_in_view_model.dart index 55ff005c1..d03159c95 100644 --- a/lib/pages/sign_in/view_model/sign_in_view_model.dart +++ b/lib/pages/sign_in/view_model/sign_in_view_model.dart @@ -1,15 +1,19 @@ import 'dart:convert'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:matrix/matrix_api_lite/utils/logs.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pages/sign_in/view_model/flows/sort_homeservers.dart'; import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart'; import 'package:fluffychat/pages/sign_in/view_model/sign_in_state.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; class SignInViewModel extends ValueNotifier { @@ -109,4 +113,70 @@ class SignInViewModel extends ValueNotifier { void setLoginLoading(AsyncSnapshot loginLoading) { value = value.copyWith(loginLoading: loginLoading); } + + void autoSsoIfEnabled(BuildContext context) async { + if (!kIsWeb || signUp) return; + if (!AppSettings.autoSsoRedirect.value) return; + final defaultHS = AppSettings.defaultHomeserver.value; + if (defaultHS.isEmpty || + defaultHS == AppSettings.defaultHomeserver.defaultValue) { + Logs().w( + '[AutoSSO] autoSsoRedirect requires defaultHomeserver to be set', + ); + return; + } + + setLoginLoading(AsyncSnapshot.waiting()); + try { + var homeserver = Uri.parse(defaultHS); + if (homeserver.scheme.isEmpty) { + homeserver = Uri.https(defaultHS, ''); + } + final client = await matrixService.getLoginClient(); + final (_, _, loginFlows, _) = await client.checkHomeserver(homeserver); + + // Check localStorage for a pending SSO token from a previous redirect. + final pendingResult = html.window.localStorage['flutter-web-auth-2']; + if (pendingResult != null && pendingResult.isNotEmpty) { + html.window.localStorage.remove('flutter-web-auth-2'); + final token = Uri.parse(pendingResult).queryParameters['loginToken']; + if (token != null && token.isNotEmpty) { + await client.login( + LoginType.mLoginToken, + token: token, + initialDeviceDisplayName: PlatformInfos.clientName, + ); + setLoginLoading(AsyncSnapshot.withData(ConnectionState.done, true)); + return; + } + } + + // No pending token — verify SSO support and redirect. + if (!loginFlows.any((flow) => flow.type == 'm.login.sso')) { + Logs().w('[AutoSSO] Homeserver does not support SSO'); + setLoginLoading(AsyncSnapshot.withData(ConnectionState.done, false)); + return; + } + + final redirectUrl = Uri.parse( + html.window.location.href, + ).resolveUri(Uri(pathSegments: ['auth.html'])).toString(); + final ssoUrl = client.homeserver!.replace( + path: '/_matrix/client/v3/login/sso/redirect', + queryParameters: {'redirectUrl': redirectUrl, 'action': 'login'}, + ); + html.window.location.href = ssoUrl.toString(); + } catch (e, s) { + Logs().w('[AutoSSO] Failed', e, s); + setLoginLoading(AsyncSnapshot.withError(ConnectionState.done, e, s)); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + e.toLocalizedString(context, ExceptionContext.checkHomeserver), + ), + ), + ); + } + } } diff --git a/lib/widgets/view_model_builder.dart b/lib/widgets/view_model_builder.dart index 11c30b6e5..4cd992b78 100644 --- a/lib/widgets/view_model_builder.dart +++ b/lib/widgets/view_model_builder.dart @@ -5,11 +5,13 @@ class ViewModelBuilder extends StatefulWidget { final Widget Function(BuildContext context, T viewModel, Widget? child) builder; final Widget? child; + final void Function(BuildContext context, T viewModel)? onCreated; const ViewModelBuilder({ super.key, required this.create, required this.builder, this.child, + this.onCreated, }); @override @@ -23,6 +25,12 @@ class _ViewModelBuilderState @override void initState() { _viewModel = widget.create(); + if (widget.onCreated != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + widget.onCreated!(context, _viewModel); + }); + } super.initState(); } diff --git a/web/auth.html b/web/auth.html index 806485b73..9e380e9ee 100644 --- a/web/auth.html +++ b/web/auth.html @@ -2,12 +2,14 @@ Authentication complete

Authentication is complete. If this does not happen automatically, please close the window.

\ No newline at end of file