From c162cb6a78c3921d3e5360df790a2e5bedd9d0f2 Mon Sep 17 00:00:00 2001 From: manuroe Date: Fri, 26 Jun 2020 07:30:29 +0200 Subject: [PATCH] Add AuthenticatedSessionViewControllerFactory to set up a authenticated flow for a given CS API request --- CHANGES.rst | 1 + Riot.xcodeproj/project.pbxproj | 12 ++ Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 8 +- ...nticatedSessionViewControllerFactory.swift | 176 ++++++++++++++++++ 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift diff --git a/CHANGES.rst b/CHANGES.rst index a76971813..3b8ae0eae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ Improvements: * Key backup: Connect/restore backup created with SSSS (#3124). * E2E by default: Disable it if the HS admin disabled it (#3305). * Key backup: Add secure backup creation flow (#3344). + * Add AuthenticatedSessionViewControllerFactory to set up a authenticated flow for a given CS API request. Bug fix: * CallVC: Declined calls now properly reset call view controller, thanks to @Legi429 (#2877). diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 3c6cba965..c4d2fbe2e 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 32F6B96D2270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9672270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift */; }; 32F6B96E2270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9682270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift */; }; 32FDC1CD2386CD390084717A /* RiotSettingIntegrationProvisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */; }; + 32FEFA9924A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */; }; 3AF393339D2D566CE14AC200 /* Pods_RiotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */; }; 405FD41D306133A48D9B5AA1 /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */; }; 670966FEFE120D865FD8A5B6 /* Pods_RiotPods_SiriIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51187E952D5CECF6D6F5A28E /* Pods_RiotPods_SiriIntents.framework */; }; @@ -951,6 +952,7 @@ 32F6B9672270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyVerificationDataLoadingViewModel.swift; sourceTree = ""; }; 32F6B9682270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyVerificationDataLoadingViewModelType.swift; sourceTree = ""; }; 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiotSettingIntegrationProvisioning.swift; sourceTree = ""; }; + 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedSessionViewControllerFactory.swift; sourceTree = ""; }; 3942DD65EBEB7AE647C6392A /* Pods-RiotPods-SiriIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-SiriIntents.debug.xcconfig"; path = "Target Support Files/Pods-RiotPods-SiriIntents/Pods-RiotPods-SiriIntents.debug.xcconfig"; sourceTree = ""; }; 3D78489021AC9E6400B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 3D78489121AC9E6500B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -2200,6 +2202,14 @@ path = Modal; sourceTree = ""; }; + 32FEFA9724A52861005237F6 /* AuthenticatedSession */ = { + isa = PBXGroup; + children = ( + 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */, + ); + path = AuthenticatedSession; + sourceTree = ""; + }; 4220F60B660591FD80AF3428 /* Pods */ = { isa = PBXGroup; children = ( @@ -3014,6 +3024,7 @@ B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( + 32FEFA9724A52861005237F6 /* AuthenticatedSession */, B1B556EA20EE6C4C00210D55 /* Main */, B1B556CA20EE6C4C00210D55 /* TabBar */, B1B556F920EE6C4C00210D55 /* Authentication */, @@ -5456,6 +5467,7 @@ 3232AB4F2256558300AD6A5C /* TemplateScreenViewController.swift in Sources */, B1B558FC20EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleBubbleCell.m in Sources */, B1B5572920EE6C4D00210D55 /* RoomFilesViewController.m in Sources */, + 32FEFA9924A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift in Sources */, B1BEE74B23E093260003A4CB /* UserVerificationSessionStatusViewAction.swift in Sources */, B1098C1021ED07E4000DDA48 /* Presentable.swift in Sources */, B1BEE73923DF44A60003A4CB /* UserVerificationSessionsStatusViewController.swift in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index a7c8250e5..8df9909db 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -605,6 +605,8 @@ "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "This app does not support the authentication mechanism on your homeserver."; // Identity server settings "identity_server_settings_title" = "Identity Server"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 28a08579b..755d1949c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -310,6 +310,10 @@ internal enum VectorL10n { internal static var authUsernameInUse: String { return VectorL10n.tr("Vector", "auth_username_in_use") } + /// This app does not support the authentication mechanism on your homeserver. + internal static var authenticatedSessionFlowNotSupported: String { + return VectorL10n.tr("Vector", "authenticated_session_flow_not_supported") + } /// Back internal static var back: String { return VectorL10n.tr("Vector", "back") @@ -1394,7 +1398,7 @@ internal enum VectorL10n { internal static var keyBackupRecoverTitle: String { return VectorL10n.tr("Vector", "key_backup_recover_title") } - /// Safeguard against losing access to encrypted messages & data + /// Start using Key Backup internal static var keyBackupSetupBannerSubtitle: String { return VectorL10n.tr("Vector", "key_backup_setup_banner_subtitle") } @@ -3066,7 +3070,7 @@ internal enum VectorL10n { internal static var secureKeyBackupSetupCancelAlertTitle: String { return VectorL10n.tr("Vector", "secure_key_backup_setup_cancel_alert_title") } - /// Safe guard against losing access to encrypted messages & data by backing up encryption keys on your server. + /// Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server. internal static var secureKeyBackupSetupIntroInfo: String { return VectorL10n.tr("Vector", "secure_key_backup_setup_intro_info") } diff --git a/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift new file mode 100644 index 000000000..11a76e8a9 --- /dev/null +++ b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift @@ -0,0 +1,176 @@ +/* + Copyright 2020 New Vector Ltd + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import Foundation + +enum AuthenticatedSessionViewControllerFactoryErrorCode: Int { + case flowNotSupported = 0 +} + +/// This class creates view controllers that can handle an authentication flow for given requests. +@objcMembers +final class AuthenticatedSessionViewControllerFactory: NSObject { + + // MARK: - Constants + + static let errorDomain = "AuthenticatedSessionViewControllerFactoryErrorDomain" + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + + + // MARK: Public + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } + + + // MARK: - Public methods + + /// Create a view controller to handle an authentication flow for a given request. + /// + /// - Parameters: + /// - path: the request path. + /// - httpMethod: the request http method. + /// - title: the title to use in the view controller. + /// - message: the information to display in the view controller. + /// - onViewController: the block called when the view controller is ready. The caller must display it. + /// - onAuthenticated: the block called when the user finished to enter their credentials. + /// - onCancelled: the block called when the user cancelled the authentication. + /// - onFailure: the blocked called on error. + func viewController(forPath path: String, + httpMethod: String, + title: String?, + message: String?, + onViewController: @escaping (UIViewController) -> Void, + onAuthenticated: @escaping (NSDictionary) -> Void, + onCancelled: @escaping () -> Void, + onFailure: @escaping (NSError) -> Void) { + + // Get the authentication flow required for this API + session.matrixRestClient.authSessionForRequest(withMethod: httpMethod, path: path, parameters: [:], success: { [weak self] (authenticationSession) in + guard let self = self else { + return + } + + guard let authenticationSession = authenticationSession, let flows = authenticationSession.flows else { + onFailure(self.unsupportedFlowError()) + return + } + + // Return the corresponding VC + if self.hasPasswordFlow(inFlows: flows) { + let authViewController = self.createPasswordViewController(title: title, + message: message, + authenticationSession: authenticationSession, + onAuthenticated: onAuthenticated, + onCancelled: onCancelled, + onFailure: onFailure) + onViewController(authViewController) + } else { + // Flow not supported yet + onFailure(self.unsupportedFlowError()) + } + + }, failure: { (error) in + guard let error = error as NSError? else { + return + } + + onFailure(error) + }) + } + + + // MARK: - Private methods + + private func unsupportedFlowError() -> NSError { + return NSError(domain: AuthenticatedSessionViewControllerFactory.errorDomain, + code: AuthenticatedSessionViewControllerFactoryErrorCode.flowNotSupported.rawValue, + userInfo: [NSLocalizedDescriptionKey: VectorL10n.authenticatedSessionFlowNotSupported]) + } + + + // MARK: - Password flow + + private func hasPasswordFlow(inFlows flows: [MXLoginFlow]) -> Bool { + for flow in flows { + if flow.type == kMXLoginFlowTypePassword || flow.stages.contains(kMXLoginFlowTypePassword) { + return true + } + } + + return false + } + + private func createPasswordViewController( + title: String?, + message: String?, + authenticationSession: MXAuthenticationSession, + onAuthenticated: @escaping (NSDictionary) -> Void, + onCancelled: @escaping () -> Void, + onFailure: @escaping (NSError) -> Void) -> UIViewController { + + // Use a simple UIAlertController as before + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + alertController.addTextField { (textField) in + textField.isSecureTextEntry = true + textField.placeholder = VectorL10n.authPasswordPlaceholder + textField.keyboardType = .default + } + + alertController.addAction(UIAlertAction(title: Bundle.mxk_localizedString(forKey: "cancel"), style: .cancel, handler: { _ in + onCancelled() + })) + + alertController.addAction(UIAlertAction(title: Bundle.mxk_localizedString(forKey: "ok"), style: .default, handler: { _ in + + guard let password = alertController.textFields?.first?.text else { + // Should not happen + return + } + + guard let authParams = self.createAuthParams(password: password, authenticationSession: authenticationSession) else { + onFailure(self.unsupportedFlowError()) + return + } + + onAuthenticated(authParams) + })) + + return alertController + } + + private func createAuthParams(password: String, + authenticationSession: MXAuthenticationSession) -> NSDictionary? { + guard let userId = self.session.myUserId, let session = authenticationSession.session else { + return nil + } + + return [ + "type": kMXLoginFlowTypePassword, + "session": session, + "user": userId, + "password": password + ] + } +}