// // Copyright 2021 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 protocol AuthenticationServiceDelegate: AnyObject { /// The authentication service received an SSO login token via a deep link. /// This only occurs when SSOAuthenticationPresenter uses an SFSafariViewController. /// - Parameters: /// - service: The authentication service. /// - ssoLoginToken: The login token provided when SSO succeeded. /// - transactionID: The transaction ID generated during SSO page presentation. /// - Returns: `true` if the SSO login can be continued. func authenticationService(_ service: AuthenticationService, didReceive ssoLoginToken: String, with transactionID: String) -> Bool func authenticationService(_ service: AuthenticationService, didUpdateStateWithLink link: UniversalLink) } @objcMembers class AuthenticationService: NSObject { /// The shared service object. static let shared = AuthenticationService() // MARK: - Properties // MARK: Private /// The object used to create a new `MXSession` when authentication has completed. private var sessionCreator: SessionCreatorProtocol // MARK: Public /// The current state of the authentication flow. private(set) var state: AuthenticationState /// The rest client used to make authentication requests. private(set) var client: AuthenticationRestClient /// The current login wizard or `nil` if `startFlow` hasn't been called. private(set) var loginWizard: LoginWizard? /// The current registration wizard or `nil` if `startFlow` hasn't been called for `.registration`. private(set) var registrationWizard: RegistrationWizard? /// The authentication service's delegate. weak var delegate: AuthenticationServiceDelegate? /// The type of client to use during the flow. var clientType: AuthenticationRestClient.Type = MXRestClient.self // MARK: - Setup init(sessionCreator: SessionCreatorProtocol = SessionCreator()) { guard let homeserverURL = URL(string: BuildSettings.serverConfigDefaultHomeserverUrlString) else { MXLog.failure("[AuthenticationService]: Failed to create URL from default homeserver URL string.") fatalError("Invalid default homeserver URL string.") } state = AuthenticationState(flow: .login, homeserverAddress: BuildSettings.serverConfigDefaultHomeserverUrlString) client = clientType.init(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) self.sessionCreator = sessionCreator super.init() } // MARK: - Public /// Parse and handle a server provisioning link. /// - Parameter universalLink: A link such as https://mobile.element.io/?hs_url=matrix.example.com&is_url=identity.example.com /// - Returns: `true` if a provisioning link was detected and handled. @discardableResult func handleServerProvisioningLink(_ universalLink: UniversalLink) -> Bool { MXLog.debug("[AuthenticationService] handleServerProvisioningLink: \(universalLink)") let hsUrl = universalLink.homeserverUrl let isUrl = universalLink.identityServerUrl if hsUrl == nil && isUrl == nil { MXLog.debug("[AuthenticationService] handleServerProvisioningLink: no hsUrl or isUrl") return false } let isRegister = universalLink.pathParams.first == "register" let flow: AuthenticationFlow = isRegister ? .register : .login if needsAuthentication { reset() // not logged in // update the state with given HS and IS addresses state = AuthenticationState(flow: flow, homeserverAddress: hsUrl ?? BuildSettings.serverConfigDefaultHomeserverUrlString, identityServer: isUrl ?? BuildSettings.serverConfigDefaultIdentityServerUrlString) delegate?.authenticationService(self, didUpdateStateWithLink: universalLink) } else { // logged in AppDelegate.theDelegate().displayLogoutConfirmation(for: universalLink, completion: nil) } return true } /// Whether authentication is needed by checking for any accounts. /// - Returns: `true` there are no accounts or if there is an inactive account that has had a soft logout. var needsAuthentication: Bool { MXKAccountManager.shared().accounts.isEmpty || softLogoutCredentials != nil } /// Credentials to be used when authenticating after soft logout, otherwise `nil`. var softLogoutCredentials: MXCredentials? { guard MXKAccountManager.shared().activeAccounts.isEmpty else { return nil } for account in MXKAccountManager.shared().accounts { if account.isSoftLogout { return account.mxCredentials } } return nil } /// Get the last authenticated [Session], if there is an active session. /// - Returns: The last active session if any, or `nil` var lastAuthenticatedSession: MXSession? { MXKAccountManager.shared().activeAccounts?.first?.mxSession } func startFlow(_ flow: AuthenticationFlow, for homeserverAddress: String) async throws { var (client, homeserver) = try await loginFlow(for: homeserverAddress) let loginWizard = LoginWizard(client: client, sessionCreator: sessionCreator) self.loginWizard = loginWizard if flow == .register { do { let registrationWizard = RegistrationWizard(client: client, sessionCreator: sessionCreator) homeserver.registrationFlow = try await registrationWizard.registrationFlow() self.registrationWizard = registrationWizard } catch { guard homeserver.preferredLoginMode.hasSSO, error as? RegistrationError == .registrationDisabled else { throw error } // Continue without throwing when registration is disabled but SSO is available. } } // The state and client are set after trying the registration flow to // ensure the existing state isn't wiped out when an error occurs. self.state = AuthenticationState(flow: flow, homeserver: homeserver) self.client = client } /// Get the sign in or sign up fallback URL func fallbackURL(for flow: AuthenticationFlow) -> URL { switch flow { case .login: return client.loginFallbackURL case .register: return client.registerFallbackURL } } /// True when login and password has been sent with success to the homeserver var isRegistrationStarted: Bool { registrationWizard?.isRegistrationStarted ?? false } /// Reset the service to a fresh state. func reset() { loginWizard = nil registrationWizard = nil // The previously used homeserver is re-used as `startFlow` will be called again a replace it anyway. let address = state.homeserver.addressFromUser ?? state.homeserver.address let identityServer = state.identityServer self.state = AuthenticationState(flow: .login, homeserverAddress: address, identityServer: identityServer) } /// Continues an SSO flow when completion comes via a deep link. /// - Parameters: /// - token: The login token provided when SSO succeeded. /// - transactionID: The transaction ID generated during SSO page presentation. /// - Returns: `true` if the SSO login can be continued. func continueSSOLogin(with token: String, and transactionID: String) -> Bool { delegate?.authenticationService(self, didReceive: token, with: transactionID) ?? false } // /// Perform a well-known request, using the domain from the matrixId // func getWellKnownData(matrixId: String, // homeServerConnectionConfig: HomeServerConnectionConfig?) async -> WellknownResult { // // } // // /// Authenticate with a matrixId and a password // /// Usually call this after a successful call to getWellKnownData() // /// - Parameter homeServerConnectionConfig the information about the homeserver and other configuration // /// - Parameter matrixId the matrixId of the user // /// - Parameter password the password of the account // /// - Parameter initialDeviceName the initial device name // /// - Parameter deviceId the device id, optional. If not provided or null, the server will generate one. // func directAuthentication(homeServerConnectionConfig: HomeServerConnectionConfig, // matrixId: String, // password: String, // initialDeviceName: String, // deviceId: String? = nil) async -> MXSession { // // } // MARK: - Private /// Query the supported login flows for the supplied homeserver. /// This is the first method to call to be able to get a wizard to login or to create an account /// - Parameter homeserverAddress: The homeserver string entered by the user. /// - Returns: A tuple containing the REST client for the server along with the homeserver state containing the login flows. private func loginFlow(for homeserverAddress: String) async throws -> (AuthenticationRestClient, AuthenticationState.Homeserver) { let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress) guard var homeserverURL = URL(string: homeserverAddress) else { MXLog.error("[AuthenticationService] Unable to create a URL from the supplied homeserver address when calling loginFlow.") throw AuthenticationError.invalidHomeserver } var identityServerURL: URL? if let wellKnown = try? await wellKnown(for: homeserverURL) { if let baseURL = URL(string: wellKnown.homeServer.baseUrl) { homeserverURL = baseURL } if let identityServer = wellKnown.identityServer, let baseURL = URL(string: identityServer.baseUrl) { identityServerURL = baseURL } } #warning("Add an unrecognized certificate handler.") let client = clientType.init(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) if let identityServerURL = identityServerURL { client.identityServer = identityServerURL.absoluteString } let loginFlow = try await getLoginFlowResult(client: client) let homeserver = AuthenticationState.Homeserver(address: loginFlow.homeserverAddress, addressFromUser: homeserverAddress, preferredLoginMode: loginFlow.loginMode) return (client, homeserver) } /// Request the supported login flows for the corresponding session. /// This method is used to get the flows for a server after a soft-logout. /// - Parameter session: The MXSession where a soft-logout has occurred. private func loginFlow(for session: MXSession) async throws -> (AuthenticationRestClient, AuthenticationState.Homeserver) { guard let client = session.matrixRestClient else { MXLog.error("[AuthenticationService] loginFlow called on a session that doesn't have a matrixRestClient.") throw AuthenticationError.missingMXRestClient } let loginFlow = try await getLoginFlowResult(client: session.matrixRestClient) let homeserver = AuthenticationState.Homeserver(address: loginFlow.homeserverAddress, preferredLoginMode: loginFlow.loginMode) return (client, homeserver) } private func getLoginFlowResult(client: AuthenticationRestClient) async throws -> LoginFlowResult { // Get the login flow let loginFlowResponse = try await client.getLoginSession() let identityProviders = loginFlowResponse.flows?.compactMap { $0 as? MXLoginSSOFlow }.first?.identityProviders ?? [] return LoginFlowResult(supportedLoginTypes: loginFlowResponse.flows?.compactMap { $0 } ?? [], ssoIdentityProviders: identityProviders.sorted { $0.name < $1.name }.map { $0.ssoIdentityProvider }, homeserverAddress: client.homeserver) } /// Perform a well-known request on the specified homeserver URL. private func wellKnown(for homeserverURL: URL) async throws -> MXWellKnown { let wellKnownClient = clientType.init(homeServer: homeserverURL, unrecognizedCertificateHandler: nil) // The .well-known/matrix/client API is often just a static file returned with no content type. // Make our HTTP client compatible with this behaviour wellKnownClient.acceptableContentTypes = nil return try await wellKnownClient.wellKnown() } } extension MXLoginSSOIdentityProvider { var ssoIdentityProvider: SSOIdentityProvider { SSOIdentityProvider(id: identifier, name: name, brand: brand, iconURL: icon) } }