Add AuthenticationService and RegistrationWizard. (#6056)

This commit is contained in:
Doug
2022-04-27 16:02:54 +01:00
committed by GitHub
parent 987eb7d5c0
commit abb8186a0a
16 changed files with 1373 additions and 5 deletions
@@ -0,0 +1,247 @@
//
// 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
@available(iOS 14.0, *)
protocol AuthenticationServiceDelegate: AnyObject {
func authenticationServiceDidUpdateRegistrationParameters(_ authenticationService: AuthenticationService)
}
@available(iOS 14.0, *)
class AuthenticationService: NSObject {
/// The shared service object.
static let shared = AuthenticationService()
// MARK: - Properties
// MARK: Private
/// The rest client used to make authentication requests.
private var client: MXRestClient
/// The object used to create a new `MXSession` when authentication has completed.
private var sessionCreator = SessionCreator()
// MARK: Public
/// The current state of the authentication flow.
private(set) var state: AuthenticationState
/// 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?
// MARK: - Setup
override init() {
if let homeserverURL = URL(string: RiotSettings.shared.homeserverUrlString) {
// Use the same homeserver that was last used.
state = AuthenticationState(flow: .login, homeserverAddress: RiotSettings.shared.homeserverUrlString)
client = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil)
} else if let homeserverURL = URL(string: BuildSettings.serverConfigDefaultHomeserverUrlString) {
// Fall back to the default homeserver if the stored one is invalid.
state = AuthenticationState(flow: .login, homeserverAddress: BuildSettings.serverConfigDefaultHomeserverUrlString)
client = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil)
} else {
MXLog.failure("[AuthenticationService]: Failed to create URL from default homeserver URL string.")
fatalError("Invalid default homeserver URL string.")
}
super.init()
}
// MARK: - Public
/// 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 {
reset()
let loginFlows = try await loginFlow(for: homeserverAddress)
// Valid Homeserver, add it to the history.
// Note: we add what the user has input, as the data can contain a different value.
RiotSettings.shared.homeserverUrlString = homeserverAddress
state.homeserver = .init(address: loginFlows.homeserverAddress,
addressFromUser: homeserverAddress,
preferredLoginMode: loginFlows.loginMode,
loginModeSupportedTypes: loginFlows.supportedLoginTypes)
let loginWizard = LoginWizard()
self.loginWizard = loginWizard
if flow == .registration {
let registrationWizard = RegistrationWizard(client: client)
state.homeserver.registrationFlow = try await registrationWizard.registrationFlow()
self.registrationWizard = registrationWizard
}
state.flow = flow
}
/// Get a SSO url
func getSSOURL(redirectUrl: String, deviceId: String?, providerId: String?) -> String? {
fatalError("Not implemented.")
}
/// Get the sign in or sign up fallback URL
func fallbackURL(for flow: AuthenticationFlow) -> URL {
switch flow {
case .login:
return client.loginFallbackURL
case .registration:
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.
self.state = AuthenticationState(flow: .login, homeserverAddress: state.homeserver.address)
}
/// Create a session after a SSO successful login
func makeSessionFromSSO(credentials: MXCredentials) -> MXSession {
sessionCreator.createSession(credentials: credentials, client: client)
}
// /// 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
/// Request the supported login flows for this 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.
private func loginFlow(for homeserverAddress: String) async throws -> LoginFlowResult {
let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress)
guard var homeserverURL = URL(string: homeserverAddress) else {
throw AuthenticationError.invalidHomeserver
}
let state = AuthenticationState(flow: .login, homeserverAddress: homeserverAddress)
if let wellKnown = try? await wellKnown(for: homeserverURL),
let baseURL = URL(string: wellKnown.homeServer.baseUrl) {
homeserverURL = baseURL
}
#warning("Add an unrecognized certificate handler.")
let client = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil)
let loginFlow = try await getLoginFlowResult(client: client)
self.client = client
self.state = state
return loginFlow
}
/// 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 -> LoginFlowResult {
guard let client = session.matrixRestClient else { throw AuthenticationError.missingMXRestClient }
let state = AuthenticationState(flow: .login, homeserverAddress: client.homeserver)
let loginFlow = try await getLoginFlowResult(client: session.matrixRestClient)
self.client = client
self.state = state
return loginFlow
}
private func getLoginFlowResult(client: MXRestClient) 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 = MXRestClient(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)
}
}
@@ -0,0 +1,48 @@
//
// Copyright 2022 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
import MatrixSDK
@available(iOS 14.0, *)
struct AuthenticationState {
// var serverType: ServerType = .unknown
var flow: AuthenticationFlow
/// Information about the currently selected homeserver.
var homeserver: Homeserver
var isForceLoginFallbackEnabled = false
init(flow: AuthenticationFlow, homeserverAddress: String) {
self.flow = flow
self.homeserver = Homeserver(address: homeserverAddress)
}
struct Homeserver {
/// The homeserver address as returned by the server.
var address: String
/// The homeserver address as input by the user (it can differ to the well-known request).
var addressFromUser: String?
/// The preferred login mode for the server
var preferredLoginMode: LoginMode = .unknown
/// Supported types for the login.
var loginModeSupportedTypes = [MXLoginFlow]()
/// The response returned when querying the homeserver for registration flows.
var registrationFlow: RegistrationResult?
}
}
@@ -0,0 +1,71 @@
//
// Copyright 2022 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
struct LoginFlowResult {
let supportedLoginTypes: [MXLoginFlow]
let ssoIdentityProviders: [SSOIdentityProvider]
let homeserverAddress: String
var loginMode: LoginMode {
if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }),
supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) {
return .ssoAndPassword(ssoIdentityProviders: ssoIdentityProviders)
} else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypeSSO }) {
return .sso(ssoIdentityProviders: ssoIdentityProviders)
} else if supportedLoginTypes.contains(where: { $0.type == kMXLoginFlowTypePassword }) {
return .password
} else {
return .unsupported
}
}
}
enum LoginMode {
case unknown
case password
case sso(ssoIdentityProviders: [SSOIdentityProvider])
case ssoAndPassword(ssoIdentityProviders: [SSOIdentityProvider])
case unsupported
var ssoIdentityProviders: [SSOIdentityProvider]? {
switch self {
case .sso(let ssoIdentityProviders), .ssoAndPassword(let ssoIdentityProviders):
return ssoIdentityProviders
default:
return nil
}
}
var hasSSO: Bool {
switch self {
case .sso, .ssoAndPassword:
return true
default:
return false
}
}
var supportsSignModeScreen: Bool {
switch self {
case .password, .ssoAndPassword:
return true
case .unknown, .unsupported, .sso:
return false
}
}
}
@@ -0,0 +1,31 @@
//
// Copyright 2022 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
class LoginWizard {
struct State {
/// For SSO session recovery
var deviceId: String?
var resetPasswordEmail: String?
// var resetPasswordData: ResetPasswordData?
var clientSecret = UUID().uuidString
var sendAttempt: UInt = 0
}
// TODO
}
@@ -0,0 +1,197 @@
//
// Copyright 2022 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
/// The parameters used for registration requests.
struct RegistrationParameters: Codable {
/// Authentication parameters
var auth: AuthenticationParameters?
/// The account username
var username: String?
/// The account password
var password: String?
/// Device name
var initialDeviceDisplayName: String?
/// Temporary flag to notify the server that we support MSISDN flow. Used to prevent old app
/// versions to end up in fallback because the HS returns the MSISDN flow which they don't support
var xShowMSISDN: Bool?
enum CodingKeys: String, CodingKey {
case auth
case username
case password
case initialDeviceDisplayName = "initial_device_display_name"
case xShowMSISDN = "x_show_msisdn"
}
/// The parameters as a JSON dictionary for use in MXRestClient.
func dictionary() throws -> [String: Any] {
let jsonData = try JSONEncoder().encode(self)
let object = try JSONSerialization.jsonObject(with: jsonData)
guard let dictionary = object as? [String: Any] else {
throw AuthenticationError.dictionaryError
}
return dictionary
}
}
/// The data passed to the `auth` parameter in authentication requests.
struct AuthenticationParameters: Codable {
/// The type of authentication taking place. The identifier from `MXLoginFlowType`.
let type: String
/// Note: session can be null for reset password request
var session: String?
/// parameter for "m.login.recaptcha" type
var captchaResponse: String?
/// parameter for "m.login.email.identity" type
var threePIDCredentials: ThreePIDCredentials?
enum CodingKeys: String, CodingKey {
case type
case session
case captchaResponse = "response"
case threePIDCredentials = "threepid_creds"
}
/// Creates the authentication parameters for a captcha step.
static func captchaParameters(session: String, captchaResponse: String) -> AuthenticationParameters {
AuthenticationParameters(type: kMXLoginFlowTypeRecaptcha, session: session, captchaResponse: captchaResponse)
}
/// Creates the authentication parameters for a third party ID step using an email address.
static func emailIdentityParameters(session: String, threePIDCredentials: ThreePIDCredentials) -> AuthenticationParameters {
AuthenticationParameters(type: kMXLoginFlowTypeEmailIdentity, session: session, threePIDCredentials: threePIDCredentials)
}
// Note that there is a bug in Synapse (needs investigation), but if we pass .msisdn,
// the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401.
/// Creates the authentication parameters for a third party ID step using a phone number.
static func msisdnIdentityParameters(session: String, threePIDCredentials: ThreePIDCredentials) -> AuthenticationParameters {
AuthenticationParameters(type: kMXLoginFlowTypeMSISDN, session: session, threePIDCredentials: threePIDCredentials)
}
/// Creates the authentication parameters for a password reset step.
static func resetPasswordParameters(clientSecret: String, sessionID: String) -> AuthenticationParameters {
AuthenticationParameters(type: kMXLoginFlowTypeEmailIdentity,
session: nil,
threePIDCredentials: ThreePIDCredentials(clientSecret: clientSecret, sessionID: sessionID))
}
}
/// The result from a response of a registration flow step.
enum RegistrationResult {
/// Registration has completed, creating an `MXSession` for the account.
case success(MXSession)
/// The request was successful but there are pending steps to complete.
case flowResponse(FlowResult)
}
/// The state of an authentication flow after a step has been completed.
struct FlowResult {
/// The stages in the flow that are yet to be completed.
let missingStages: [Stage]
/// The stages in the flow that have been completed.
let completedStages: [Stage]
/// A stage in the authentication flow.
enum Stage {
/// The stage with the type `m.login.recaptcha`.
case reCaptcha(mandatory: Bool, publicKey: String)
/// The stage with the type `m.login.email.identity`.
case email(mandatory: Bool)
/// The stage with the type `m.login.msisdn`.
case msisdn(mandatory: Bool)
/// The stage with the type `m.login.dummy`.
///
/// This stage can be mandatory if there is no other stages. In this case the account cannot
/// be created by just sending a username and a password, the dummy stage has to be completed.
case dummy(mandatory: Bool)
/// The stage with the type `m.login.terms`.
case terms(mandatory: Bool, policies: [String: String])
/// A stage of an unknown type.
case other(mandatory: Bool, type: String, params: [AnyHashable: Any])
/// Whether the stage is a dummy stage that is also mandatory.
var isDummyAndMandatory: Bool {
guard case let .dummy(isMandatory) = self else { return false }
return isMandatory
}
}
}
extension MXAuthenticationSession {
/// The flows from the session mapped as a `FlowResult` value.
var flowResult: FlowResult {
let allFlowTypes = Set(flows.flatMap { $0.stages ?? [] })
var missingStages = [FlowResult.Stage]()
var completedStages = [FlowResult.Stage]()
allFlowTypes.forEach { flow in
let isMandatory = flows.allSatisfy { $0.stages.contains(flow) }
let stage: FlowResult.Stage
switch flow {
case kMXLoginFlowTypeRecaptcha:
let parameters = params[flow] as? [AnyHashable: Any]
let publicKey = parameters?["public_key"] as? String
stage = .reCaptcha(mandatory: isMandatory, publicKey: publicKey ?? "")
case kMXLoginFlowTypeDummy:
stage = .dummy(mandatory: isMandatory)
case kMXLoginFlowTypeTerms:
let parameters = params[flow] as? [String: String]
stage = .terms(mandatory: isMandatory, policies: parameters ?? [:])
case kMXLoginFlowTypeMSISDN:
stage = .msisdn(mandatory: isMandatory)
case kMXLoginFlowTypeEmailIdentity:
stage = .email(mandatory: isMandatory)
default:
let parameters = params[flow] as? [AnyHashable: Any]
stage = .other(mandatory: isMandatory, type: flow, params: parameters ?? [:])
}
if let completed = completed, completed.contains(flow) {
completedStages.append(stage)
} else {
missingStages.append(stage)
}
}
return FlowResult(missingStages: missingStages, completedStages: completedStages)
}
/// Determines the next stage to be completed in the flow.
func nextUncompletedStage(flowIndex: Int = 0) -> String? {
guard flows.count < flowIndex else { return nil }
return flows[flowIndex].stages.first {
guard let completed = completed else { return false }
return !completed.contains($0)
}
}
}
@@ -0,0 +1,243 @@
//
// Copyright 2022 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
@available(iOS 14.0, *)
/// Set of methods to be able to create an account on a homeserver.
///
/// Common scenario to register an account successfully:
/// - Call `registrationFlow` to check that you application supports all the mandatory registration stages
/// - Call `createAccount` to start the account creation
/// - Fulfil all mandatory stages using the methods `performReCaptcha` `acceptTerms` `dummy`, etc.
///
/// More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md
/// and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
class RegistrationWizard {
struct State {
var currentSession: String?
var isRegistrationStarted = false
var currentThreePIDData: ThreePIDData?
var clientSecret = UUID().uuidString
var sendAttempt: UInt = 0
}
let client: MXRestClient
let sessionCreator: SessionCreator
private(set) var state: State
/// This is the current ThreePID, waiting for validation. The SDK will store it in database, so it can be
/// restored even if the app has been killed during the registration
var currentThreePID: String? {
guard let threePid = state.currentThreePIDData?.threePID else { return nil }
switch threePid {
case .email(let string):
return string
case .msisdn(let msisdn, _):
return state.currentThreePIDData?.registrationResponse.formattedMSISDN ?? msisdn
}
}
/// True when login and password have been sent with success to the homeserver,
/// i.e. `createAccount` has been called successfully.
var isRegistrationStarted: Bool {
state.isRegistrationStarted
}
init(client: MXRestClient, sessionCreator: SessionCreator = SessionCreator()) {
self.client = client
self.sessionCreator = sessionCreator
self.state = State()
}
/// Call this method to get the possible registration flow of the current homeserver.
/// It can be useful to ensure that your application implementation supports all the stages
/// required to create an account. If it is not the case, you will have to use the web fallback
/// to let the user create an account with your application.
/// See `AuthenticationService.getFallbackUrl`
func registrationFlow() async throws -> RegistrationResult {
let parameters = RegistrationParameters()
return try await performRegistrationRequest(parameters: parameters)
}
/// Can be call to check is the desired username is available for registration on the current homeserver.
/// It may also fails if the desired username is not correctly formatted or does not follow any restriction on
/// the homeserver. Ex: username with only digits may be rejected.
/// - Parameter username the desired username. Ex: "alice"
func registrationAvailable(username: String) async throws -> Bool {
try await client.isUsernameAvailable(username)
}
/// This is the first method to call in order to create an account and start the registration process.
///
/// - Parameter username the desired username. Ex: "alice"
/// - Parameter password the desired password
/// - Parameter initialDeviceDisplayName the device display name
func createAccount(username: String?,
password: String?,
initialDeviceDisplayName: String?) async throws -> RegistrationResult {
let parameters = RegistrationParameters(username: username, password: password, initialDeviceDisplayName: initialDeviceDisplayName)
let result = try await performRegistrationRequest(parameters: parameters)
state.isRegistrationStarted = true
return result
}
/// Perform the "m.login.recaptcha" stage.
///
/// - Parameter response: The response from ReCaptcha
func performReCaptcha(response: String) async throws -> RegistrationResult {
guard let session = state.currentSession else {
throw RegistrationError.createAccountNotCalled
}
let parameters = RegistrationParameters(auth: AuthenticationParameters.captchaParameters(session: session, captchaResponse: response))
return try await performRegistrationRequest(parameters: parameters)
}
/// Perform the "m.login.terms" stage.
func acceptTerms() async throws -> RegistrationResult {
guard let session = state.currentSession else {
throw RegistrationError.createAccountNotCalled
}
let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeTerms, session: session))
return try await performRegistrationRequest(parameters: parameters)
}
/// Perform the "m.login.dummy" stage.
func dummy() async throws -> RegistrationResult {
guard let session = state.currentSession else {
throw RegistrationError.createAccountNotCalled
}
let parameters = RegistrationParameters(auth: AuthenticationParameters(type: kMXLoginFlowTypeDummy, session: session))
return try await performRegistrationRequest(parameters: parameters)
}
/// Perform the "m.login.email.identity" or "m.login.msisdn" stage.
///
/// - Parameter threePID the threePID to add to the account. If this is an email, the homeserver will send an email
/// to validate it. For a msisdn a SMS will be sent.
func addThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult {
state.currentThreePIDData = nil
return try await sendThreePID(threePID: threePID)
}
/// Ask the homeserver to send again the current threePID (email or msisdn).
func sendAgainThreePID() async throws -> RegistrationResult {
guard let threePID = state.currentThreePIDData?.threePID else {
throw RegistrationError.createAccountNotCalled
}
return try await sendThreePID(threePID: threePID)
}
/// Send the code received by SMS to validate a msisdn.
/// If the code is correct, the registration request will be executed to validate the msisdn.
func handleValidateThreePID(code: String) async throws -> RegistrationResult {
return try await validateThreePid(code: code)
}
/// Useful to poll the homeserver when waiting for the email to be validated by the user.
/// Once the email is validated, this method will return successfully.
/// - Parameter delay How long to wait before sending the request.
func checkIfEmailHasBeenValidated(delay: TimeInterval) async throws -> RegistrationResult {
MXLog.failure("The delay on this method is no longer available. Move this to the object handling the polling.")
guard let parameters = state.currentThreePIDData?.registrationParameters else {
throw RegistrationError.missingThreePIDData
}
return try await performRegistrationRequest(parameters: parameters)
}
// MARK: - Private
private func validateThreePid(code: String) async throws -> RegistrationResult {
guard let threePIDData = state.currentThreePIDData else {
throw RegistrationError.missingThreePIDData
}
guard let submitURL = threePIDData.registrationResponse.submitURL else {
throw RegistrationError.missingThreePIDURL
}
let validationBody = ThreePIDValidationCodeBody(clientSecret: state.clientSecret,
sessionID: threePIDData.registrationResponse.sessionID,
code: code)
#warning("Seems odd to pass a nil baseURL and then the url as the path, yet this is how MXK3PID works")
guard let httpClient = MXHTTPClient(baseURL: nil, andOnUnrecognizedCertificateBlock: nil) else {
throw RegistrationError.threePIDClientFailure
}
guard try await httpClient.validateThreePIDCode(submitURL: submitURL, validationBody: validationBody) else {
throw RegistrationError.threePIDValidationFailure
}
let parameters = threePIDData.registrationParameters
MXLog.failure("This method used to add a 3-second delay to the request. This should be moved to the caller of `handleValidateThreePID`.")
return try await performRegistrationRequest(parameters: parameters)
}
private func sendThreePID(threePID: RegisterThreePID) async throws -> RegistrationResult {
guard let session = state.currentSession else {
throw RegistrationError.createAccountNotCalled
}
let response = try await client.requestTokenDuringRegistration(for: threePID,
clientSecret: state.clientSecret,
sendAttempt: state.sendAttempt)
state.sendAttempt += 1
let threePIDCredentials = ThreePIDCredentials(clientSecret: state.clientSecret, sessionID: response.sessionID)
let authenticationParameters: AuthenticationParameters
switch threePID {
case .email:
authenticationParameters = AuthenticationParameters.emailIdentityParameters(session: session, threePIDCredentials: threePIDCredentials)
case .msisdn:
authenticationParameters = AuthenticationParameters.msisdnIdentityParameters(session: session, threePIDCredentials: threePIDCredentials)
}
let parameters = RegistrationParameters(auth: authenticationParameters)
state.currentThreePIDData = ThreePIDData(threePID: threePID, registrationResponse: response, registrationParameters: parameters)
// Send the session id for the first time
return try await performRegistrationRequest(parameters: parameters)
}
private func performRegistrationRequest(parameters: RegistrationParameters) async throws -> RegistrationResult {
do {
let response = try await client.register(parameters: parameters)
let credentials = MXCredentials(loginResponse: response, andDefaultCredentials: client.credentials)
return .success(sessionCreator.createSession(credentials: credentials, client: client))
} catch {
let nsError = error as NSError
guard
let jsonResponse = nsError.userInfo[MXHTTPClientErrorResponseDataKey] as? [String: Any],
let authenticationSession = MXAuthenticationSession(fromJSON: jsonResponse)
else { throw error }
state.currentSession = authenticationSession.session
return .flowResponse(authenticationSession.flowResult)
}
}
}
@@ -0,0 +1,38 @@
//
// Copyright 2022 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
/// A WIP class that has common functionality to create a new session.
class SessionCreator {
/// Creates an `MXSession` using the supplied credentials and REST client.
func createSession(credentials: MXCredentials, client: MXRestClient) -> MXSession {
// Report the new account in account manager
if credentials.identityServer == nil {
#warning("Check that the client is actually updated with this info?")
credentials.identityServer = client.identityServer
}
let account = MXKAccount(credentials: credentials)
if let identityServer = credentials.identityServer {
account.identityServerURL = identityServer
}
MXKAccountManager.shared().addAccount(account, andOpenSession: true)
return account.mxSession
}
}
@@ -0,0 +1,89 @@
//
// Copyright 2022 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 RegisterThreePID {
case email(String)
case msisdn(msisdn: String, countryCode: String)
}
struct ThreePIDCredentials: Codable {
var clientSecret: String?
var identityServer: String?
var sessionID: String?
enum CodingKeys: String, CodingKey {
case clientSecret = "client_secret"
case identityServer = "id_server"
case sessionID = "sid"
}
}
struct ThreePIDData {
let threePID: RegisterThreePID
let registrationResponse: RegistrationThreePIDTokenResponse
let registrationParameters: RegistrationParameters
}
// TODO: This could potentially become an MXJSONModel?
struct RegistrationThreePIDTokenResponse {
/// Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
/// Their length must not exceed 255 characters and they must not be empty.
let sessionID: String
/// An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
/// Service API's POST /validate/email/submitToken endpoint. The homeserver must send this token to the user (if applicable),
/// who should then be prompted to provide it to the client.
///
/// If this field is not present, the client can assume that verification will happen without the client's involvement provided
/// the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
var submitURL: String? = nil
// MARK: - Additional data that may be needed
var msisdn: String? = nil
var formattedMSISDN: String? = nil
var success: Bool? = nil
enum CodingKeys: String, CodingKey {
case sessionID = "sid"
case submitURL = "submit_url"
case msisdn
case formattedMSISDN = "intl_fmt"
case success
}
}
struct ThreePIDValidationCodeBody: Codable {
let clientSecret: String
let sessionID: String
let code: String
enum CodingKeys: String, CodingKey {
case clientSecret = "client_secret"
case sessionID = "sid"
case code = "token"
}
func jsonData() throws -> Data {
try JSONEncoder().encode(self)
}
}