Add initial implementation of the LoginWizard.

This commit is contained in:
Doug
2022-05-19 12:53:46 +01:00
committed by Doug
parent 7dbe5b17e9
commit ac755f11f5
10 changed files with 391 additions and 29 deletions
@@ -93,7 +93,7 @@ class AuthenticationService: NSObject {
preferredLoginMode: loginFlows.loginMode,
loginModeSupportedTypes: loginFlows.supportedLoginTypes)
let loginWizard = LoginWizard()
let loginWizard = LoginWizard(client: client)
self.loginWizard = loginWizard
if flow == .register {
@@ -16,6 +16,7 @@
import Foundation
/// The result returned when querying a homeserver's available login flows.
struct LoginFlowResult {
let supportedLoginTypes: [MXLoginFlow]
let ssoIdentityProviders: [SSOIdentityProvider]
@@ -35,11 +36,17 @@ struct LoginFlowResult {
}
}
/// The supported forms of login that a homeserver allows.
enum LoginMode {
/// The login mode hasn't been determined yet.
case unknown
/// The homeserver supports login with a password.
case password
/// The homeserver supports login via one or more SSO providers.
case sso(ssoIdentityProviders: [SSOIdentityProvider])
/// The homeserver supports login with either a password or via an SSO provider.
case ssoAndPassword(ssoIdentityProviders: [SSOIdentityProvider])
/// The homeserver only allows login with unsupported mechanisms. Use fallback instead.
case unsupported
var ssoIdentityProviders: [SSOIdentityProvider]? {
@@ -60,7 +67,7 @@ enum LoginMode {
}
}
var supportsSignModeScreen: Bool {
var supportsPasswordFlow: Bool {
switch self {
case .password, .ssoAndPassword:
return true
@@ -69,3 +76,11 @@ enum LoginMode {
}
}
}
/// Data obtained when calling `LoginWizard.resetPassword` that will be used
/// when calling `LoginWizard.checkResetPasswordMailConfirmed`.
struct ResetPasswordData {
let newPassword: String
let addThreePIDSessionID: String
}
@@ -0,0 +1,114 @@
//
// 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
/// An `Encodable` type that can be used as the parameters of a login request.
protocol LoginParameters: DictionaryEncodable {
var type: String { get }
}
/// The parameters used for a login request with a token.
struct LoginTokenParameters: LoginParameters {
let type = kMXLoginFlowTypeToken
let token: String
}
/// The parameters used for a login request with an ID and password.
struct LoginPasswordParameters: LoginParameters {
let id: Identifier
let password: String
let type: String = kMXLoginFlowTypePassword
let deviceDisplayName: String?
let deviceID: String?
enum CodingKeys: String, CodingKey {
case id = "identifier"
case password
case type
case deviceDisplayName = "initial_device_display_name"
case deviceID = "device_id"
}
enum ThreePIDMedium: String { case email, msisdn }
enum Identifier: Encodable {
case user(String)
case thirdParty(medium: ThreePIDMedium, address: String)
case phone(country: String, phone: String)
private enum Constants {
static let typeKey = "type"
static let userType = "m.id.user"
static let thirdPartyType = "m.id.thirdparty"
static let phoneType = "m.id.phone"
static let userKey = "user"
static let mediumKey = "medium"
static let addressKey = "address"
static let countryKey = "country"
static let phoneKey = "phone"
}
var dictionary: [String: String] {
switch self {
case .user(let user):
return [
Constants.typeKey: Constants.userType,
Constants.userKey: user
]
case .thirdParty(let medium, let address):
return [
Constants.typeKey: Constants.thirdPartyType,
Constants.mediumKey: medium.rawValue,
Constants.addressKey: address
]
case .phone(let country, let phone):
return [
Constants.typeKey: Constants.phoneType,
Constants.countryKey: country,
Constants.phoneKey: phone
]
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(dictionary)
}
}
}
/// The parameters used when checking the user has confirmed their email to reset their password.
struct CheckResetPasswordParameters: DictionaryEncodable {
/// Authentication parameters
let auth: AuthenticationParameters
/// The new password
let newPassword: String
enum CodingKeys: String, CodingKey {
case auth
case newPassword = "new_password"
}
init(clientSecret: String, sessionID: String, newPassword: String) {
self.auth = AuthenticationParameters.resetPasswordParameters(clientSecret: clientSecret, sessionID: sessionID)
self.newPassword = newPassword
}
}
@@ -16,16 +16,106 @@
import Foundation
/// Set of methods to be able to login to an existing account on a homeserver.
///
/// More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md
class LoginWizard {
struct State {
/// For SSO session recovery
var deviceId: String?
var resetPasswordEmail: String?
// var resetPasswordData: ResetPasswordData?
var resetPasswordData: ResetPasswordData?
var clientSecret = UUID().uuidString
var sendAttempt: UInt = 0
}
// TODO
let client: MXRestClient
let sessionCreator: SessionCreator
private(set) var state: State
init(client: MXRestClient, sessionCreator: SessionCreator = SessionCreator()) {
self.client = client
self.sessionCreator = sessionCreator
self.state = State()
}
// /// Get some information about a matrixId: displayName and avatar url
// func profileInfo(for matrixID: String) async -> LoginProfileInfo {
//
// }
/// Login to the homeserver.
/// - Parameters:
/// - login: The login field. Can be a user name, or a msisdn (email or phone number) associated to the account.
/// - password: The password of the account.
/// - initialDeviceName: The initial device name.
/// - deviceID: The device ID, optional. If not provided or nil, the server will generate one.
/// - Returns: An `MXSession` if the login is successful.
func login(login: String, password: String, initialDeviceName: String, deviceID: String? = nil) async throws -> MXSession {
let parameters: LoginPasswordParameters
if MXTools.isEmailAddress(login) {
parameters = LoginPasswordParameters(id: .thirdParty(medium: .email, address: login),
password: password,
deviceDisplayName: initialDeviceName,
deviceID: deviceID)
} else {
parameters = LoginPasswordParameters(id: .user(login),
password: password,
deviceDisplayName: initialDeviceName,
deviceID: deviceID)
}
let credentials = try await client.login(parameters: parameters)
return sessionCreator.createSession(credentials: credentials, client: client)
}
/// Exchange a login token to an access token.
/// - Parameter loginToken: A login token, obtained when login has happened in a WebView, using SSO.
/// - Returns: An `MXSession` if the login is successful.
func login(with token: String) async throws -> MXSession {
let parameters = LoginTokenParameters(token: token)
let credentials = try await client.login(parameters: parameters)
return sessionCreator.createSession(credentials: credentials, client: client)
}
// /// Login to the homeserver by sending a custom JsonDict.
// /// The data should contain at least one entry `type` with a String value.
// func loginCustom(data: Codable) async -> MXSession {
//
// }
/// Ask the homeserver to reset the user password. The password will not be
/// reset until `checkResetPasswordMailConfirmed` is successfully called.
/// - Parameters:
/// - email: An email previously associated to the account the user wants the password to be reset.
/// - newPassword: The desired new password
func resetPassword(email: String, newPassword: String) async throws {
let result = try await client.forgetPassword(for: email,
clientSecret: state.clientSecret,
sendAttempt: state.sendAttempt)
state.sendAttempt += 1
state.resetPasswordData = ResetPasswordData(newPassword: newPassword, addThreePIDSessionID: result)
}
/// Confirm the new password, once the user has checked their email.
/// When this method succeeds, the account password will be effectively modified.
func checkResetPasswordMailConfirmed() async throws {
guard let resetPasswordData = state.resetPasswordData else {
MXLog.error("[LoginWizard] resetPasswordMailConfirmed: Reset password data missing. Call resetPassword first.")
throw LoginError.resetPasswordNotStarted
}
let parameters = CheckResetPasswordParameters(clientSecret: state.clientSecret,
sessionID: resetPasswordData.addThreePIDSessionID,
newPassword: resetPasswordData.newPassword)
try await client.resetPassword(parameters: parameters)
state.resetPasswordData = nil
}
}
@@ -17,7 +17,7 @@
import Foundation
/// The parameters used for registration requests.
struct RegistrationParameters: Codable {
struct RegistrationParameters: DictionaryEncodable {
/// Authentication parameters
var auth: AuthenticationParameters?
@@ -41,22 +41,10 @@ struct RegistrationParameters: Codable {
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 {
MXLog.error("[RegistrationParameters] dictionary: Unexpected type decoded \(type(of: object)). Expected a Dictionary.")
throw AuthenticationError.dictionaryError
}
return dictionary
}
}
/// The data passed to the `auth` parameter in registration requests.
struct AuthenticationParameters: Codable {
/// The data passed to the `auth` parameter in authentication requests.
struct AuthenticationParameters: Encodable {
/// The type of authentication taking place. The identifier from `MXLoginFlowType`.
let type: String