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.
This commit is contained in:
swherdman 2026-02-20 17:51:02 +11:00
parent 298a2d0760
commit c581377999
No known key found for this signature in database
GPG key ID: 7FB31F005CB18764
7 changed files with 93 additions and 7 deletions

View file

@ -25,5 +25,6 @@
"noEncryptionWarningShown": false,
"displayChatDetailsColumn": false,
"colorSchemeSeedInt": 4283835834,
"enableSoftLogout": false
"enableSoftLogout": false,
"autoSsoRedirect": false
}

View file

@ -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(

View file

@ -52,7 +52,8 @@ enum AppSettings<T> {
// colorSchemeSeed stored as ARGB int
colorSchemeSeedInt<int>('chat.fluffy.color_scheme_seed', 0xFF5625BA),
emojiSuggestionLocale<String>('emoji_suggestion_locale', ''),
enableSoftLogout<bool>('chat.fluffy.enable_soft_logout', false);
enableSoftLogout<bool>('chat.fluffy.enable_soft_logout', false),
autoSsoRedirect<bool>('chat.fluffy.auto_sso_redirect', false);
final String key;
final T defaultValue;

View file

@ -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;

View file

@ -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<SignInState> {
@ -109,4 +113,70 @@ class SignInViewModel extends ValueNotifier<SignInState> {
void setLoginLoading(AsyncSnapshot<bool> 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),
),
),
);
}
}
}

View file

@ -5,11 +5,13 @@ class ViewModelBuilder<T extends ValueNotifier> 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<T extends ValueNotifier>
@override
void initState() {
_viewModel = widget.create();
if (widget.onCreated != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
widget.onCreated!(context, _viewModel);
});
}
super.initState();
}

View file

@ -2,12 +2,14 @@
<title>Authentication complete</title>
<p>Authentication is complete. If this does not happen automatically, please close the window.</p>
<script>
var key = 'flutter-web-auth-2';
if (window.opener) {
window.opener.postMessage({
'flutter-web-auth-2': window.location.href
[key]: window.location.href
}, window.location.origin);
window.close();
} else {
localStorage.setItem('flutter-web-auth-2', window.location.href);
localStorage.setItem(key, window.location.href);
window.location.replace('/');
}
window.close();
</script>