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:
parent
298a2d0760
commit
c581377999
7 changed files with 93 additions and 7 deletions
|
|
@ -25,5 +25,6 @@
|
|||
"noEncryptionWarningShown": false,
|
||||
"displayChatDetailsColumn": false,
|
||||
"colorSchemeSeedInt": 4283835834,
|
||||
"enableSoftLogout": false
|
||||
"enableSoftLogout": false,
|
||||
"autoSsoRedirect": false
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Reference in a new issue