From d1d470deb600806b60cbbb56fabe44260be01b03 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Mon, 18 Jan 2021 22:59:02 +0100 Subject: [PATCH] feat: Implement sso --- lib/app_config.dart | 1 + lib/components/matrix.dart | 9 +++-- lib/config/routes.dart | 14 ++++++++ lib/views/homeserver_picker.dart | 29 +++++++++++++-- lib/views/settings_devices.dart | 53 ++++++++++++++-------------- lib/views/sso_web_view.dart | 60 ++++++++++++++++++++++++++++++++ 6 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 lib/views/sso_web_view.dart diff --git a/lib/app_config.dart b/lib/app_config.dart index 0b373b97..cafe9f28 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -10,6 +10,7 @@ abstract class AppConfig { static const bool enableRegistration = true; static String _privacyUrl = 'https://fluffychat.im/en/privacy.html'; static String get privacyUrl => _privacyUrl; + static const String appId = 'im.fluffychat.FluffyChat'; static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat'; static const String supportUrl = 'https://gitlab.com/famedly/fluffychat/issues'; diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart index 398d2337..93878834 100644 --- a/lib/components/matrix.dart +++ b/lib/components/matrix.dart @@ -156,8 +156,13 @@ class MatrixState extends State { ), ); default: - Logs().w('Warning! Cannot handle the stage "$stage"'); - return; + await widget.apl.currentState.pushNamed( + '/authwebview/$stage/${uiaRequest.session}', + arguments: () => null, + ); + return uiaRequest.completeStage( + AuthenticationData(session: uiaRequest.session), + ); } } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 047206cb..331f3f1a 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -27,6 +27,7 @@ import 'package:fluffychat/views/settings_notifications.dart'; import 'package:fluffychat/views/settings_style.dart'; import 'package:fluffychat/views/sign_up.dart'; import 'package:fluffychat/views/sign_up_password.dart'; +import 'package:fluffychat/views/sso_web_view.dart'; import 'package:flutter/material.dart'; class FluffyRoutes { @@ -47,6 +48,8 @@ class FluffyRoutes { return ViewData(mainView: (_) => HomeserverPicker()); case 'login': return ViewData(mainView: (_) => Login()); + case 'sso': + return ViewData(mainView: (_) => SsoWebView()); case 'signup': if (parts.length == 5 && parts[2] == 'password') { return ViewData( @@ -129,6 +132,17 @@ class FluffyRoutes { mainView: (_) => Archive(), emptyView: (_) => EmptyPage(), ); + case 'authwebview': + if (parts.length == 4) { + return ViewData( + mainView: (_) => AuthWebView( + parts[2], + Uri.decodeComponent(parts[3]), + settings.arguments, + ), + ); + } + break; case 'discover': return ViewData( mainView: (_) => diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index 30d5b23d..0762923a 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/app_config.dart'; import 'package:fluffychat/components/sentry_switch_list_tile.dart'; @@ -30,10 +31,32 @@ class _HomeserverPickerState extends State { } setState(() => _isLoading = true); + + // Look up well known try { - await Matrix.of(context).client.checkHomeserver(homeserver); - await AdaptivePageLayout.of(context) - .pushNamed(AppConfig.enableRegistration ? '/signup' : '/login'); + final wellKnown = await MatrixApi(homeserver: Uri.parse(homeserver)) + .requestWellKnownInformations(); + homeserver = wellKnown.mHomeserver.baseUrl; + } catch (e) { + Logs().v('Found no well known information', e); + } + + try { + await Matrix.of(context) + .client + .checkHomeserver(homeserver, supportedLoginTypes: { + AuthenticationTypes.password, + if (PlatformInfos.isMobile) AuthenticationTypes.sso + }); + final loginTypes = await Matrix.of(context).client.requestLoginTypes(); + if (loginTypes.flows + .any((flow) => flow.type == AuthenticationTypes.password)) { + await AdaptivePageLayout.of(context) + .pushNamed(AppConfig.enableRegistration ? '/signup' : '/login'); + } else if (loginTypes.flows + .any((flow) => flow.type == AuthenticationTypes.sso)) { + await AdaptivePageLayout.of(context).pushNamed('/sso'); + } } catch (e) { // ignore: unawaited_futures FlushbarHelper.createError( diff --git a/lib/views/settings_devices.dart b/lib/views/settings_devices.dart index 6d70068e..8f896635 100644 --- a/lib/views/settings_devices.dart +++ b/lib/views/settings_devices.dart @@ -22,6 +22,9 @@ class DevicesSettingsState extends State { void reload() => setState(() => devices = null); + bool _loadingDeletingDevices = false; + String _errorDeletingDevices; + void _removeDevicesAction(BuildContext context, List devices) async { if (await showOkCancelAlertDialog( context: context, @@ -33,33 +36,24 @@ class DevicesSettingsState extends State { for (var userDevice in devices) { deviceIds.add(userDevice.deviceId); } - final password = await showTextInputDialog( - title: L10n.of(context).pleaseEnterYourPassword, - context: context, - textFields: [ - DialogTextField( - hintText: '******', - obscureText: true, - minLines: 1, - maxLines: 1, - ) - ], - ); - if (password == null) return; - final success = await showFutureLoadingDialog( - context: context, - future: () => matrix.client.deleteDevices( - deviceIds, - auth: AuthenticationPassword( - password: password.single, - user: matrix.client.userID, - identifier: AuthenticationUserIdentifier(user: matrix.client.userID), + try { + setState(() { + _loadingDeletingDevices = true; + _errorDeletingDevices = null; + }); + await matrix.client.uiaRequestBackground( + (auth) => matrix.client.deleteDevices( + deviceIds, + auth: auth, ), - ), - ); - if (success.error == null) { + ); reload(); + } catch (e, s) { + Logs().v('Error while deleting devices', e, s); + setState(() => _errorDeletingDevices = e.toString()); + } finally { + setState(() => _loadingDeletingDevices = false); } } @@ -127,11 +121,16 @@ class DevicesSettingsState extends State { if (devices.isNotEmpty) ListTile( title: Text( - L10n.of(context).removeAllOtherDevices, + _errorDeletingDevices ?? + L10n.of(context).removeAllOtherDevices, style: TextStyle(color: Colors.red), ), - trailing: Icon(Icons.delete_outline), - onTap: () => _removeDevicesAction(context, devices), + trailing: _loadingDeletingDevices + ? CircularProgressIndicator() + : Icon(Icons.delete_outline), + onTap: _loadingDeletingDevices + ? null + : () => _removeDevicesAction(context, devices), ), Divider(height: 1), Expanded( diff --git a/lib/views/sso_web_view.dart b/lib/views/sso_web_view.dart new file mode 100644 index 00000000..1d5f25a7 --- /dev/null +++ b/lib/views/sso_web_view.dart @@ -0,0 +1,60 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../app_config.dart'; + +class SsoWebView extends StatefulWidget { + @override + _SsoWebViewState createState() => _SsoWebViewState(); +} + +class _SsoWebViewState extends State { + bool _loading = false; + String _error; + + void _login(BuildContext context, String token) async { + setState(() => _loading = true); + try { + await Matrix.of(context).client.login( + type: AuthenticationTypes.token, + userIdentifierType: null, + token: token, + initialDeviceDisplayName: Matrix.of(context).clientName, + ); + } catch (e, s) { + Logs().e('Login with token failed', e, s); + setState(() => _error = e.toString()); + } + } + + @override + Widget build(BuildContext context) { + final url = + '${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(AppConfig.appId.toLowerCase() + '://sso')}'; + return Scaffold( + appBar: AppBar( + title: Text( + L10n.of(context).logInTo(Matrix.of(context).client.homeserver?.host ?? + L10n.of(context).oopsSomethingWentWrong), + ), + ), + body: _error != null + ? Center(child: Text(_error)) + : _loading + ? Center(child: CircularProgressIndicator()) + : WebView( + initialUrl: url, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (url) { + if (url.startsWith(AppConfig.appId.toLowerCase())) { + _login(context, + Uri.parse(url).queryParameters['loginToken']); + } + }, + ), + ); + } +}