From c581377999bde36c142724f43468dddf42277db6 Mon Sep 17 00:00:00 2001 From: swherdman Date: Fri, 20 Feb 2026 17:51:02 +1100 Subject: [PATCH] feat: add autoSsoRedirect config option for automatic SSO login on web When enabled via config.json, unauthenticated web users are automatically redirected to the homeserver's SSO provider on page load, bypassing the homeserver picker UI. Uses full-page redirect with localStorage token recovery to avoid browser popup blocking. Requires defaultHomeserver to also be set in config.json. --- config.sample.json | 3 +- lib/config/routes.dart | 3 + lib/config/setting_keys.dart | 3 +- lib/pages/sign_in/sign_in_page.dart | 1 + .../view_model/sign_in_view_model.dart | 74 ++++++++++++++++++- lib/widgets/view_model_builder.dart | 8 ++ web/auth.html | 8 +- 7 files changed, 93 insertions(+), 7 deletions(-) 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