mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-21 00:52:43 +02:00
Add AuthenticationService and RegistrationWizard. (#6056)
This commit is contained in:
+247
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user