mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
#40: Use the Rust AuthenticationService.
* Update SDK package to 1.0.12-alpha. * Use an app group for storage and stop stripping the http from the homeserver when configuring the service. * Rename access token to restore token. * Remove matrix.org server description inline with latest FTUE changes.
This commit is contained in:
+36
-17
@@ -9,42 +9,61 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class AuthenticationService: AuthenticationServiceProtocol {
|
||||
class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private(set) var homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverURLString)
|
||||
private let authenticationService: AuthenticationService
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
|
||||
// MARK: Public
|
||||
|
||||
private(set) var homeserver = LoginHomeserver(address: BuildSettings.defaultHomeserverAddress, loginMode: .unknown)
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol) {
|
||||
self.userSessionStore = userSessionStore
|
||||
authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectoryPath)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
homeserver = LoginHomeserver(address: homeserverAddress)
|
||||
return .success(())
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
let task = Task.detached { () -> LoginHomeserver in
|
||||
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
||||
|
||||
try self.authenticationService.configureHomeserver(serverName: homeserverAddress)
|
||||
|
||||
guard let details = self.authenticationService.homeserverDetails() else { return homeserver }
|
||||
|
||||
if let issuer = details.authenticationIssuer(), let issuerURL = URL(string: issuer) {
|
||||
homeserver.loginMode = .oidc(issuerURL)
|
||||
} else if details.supportsPasswordLogin() {
|
||||
homeserver.loginMode = .password
|
||||
} else {
|
||||
homeserver.loginMode = .unsupported
|
||||
}
|
||||
|
||||
return homeserver
|
||||
}
|
||||
|
||||
switch await task.result {
|
||||
case .success(let homeserver):
|
||||
self.homeserver = homeserver
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed configuring a server: \(error)")
|
||||
return .failure(.invalidHomeserverAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started new login")
|
||||
|
||||
// Workaround whilst the SDK requires a full MXID.
|
||||
let username = username.isMatrixUserID ? username : "@\(username):\(homeserver.address)"
|
||||
|
||||
let basePath = userSessionStore.baseDirectoryPath(for: username)
|
||||
let builder = ClientBuilder()
|
||||
.basePath(path: basePath)
|
||||
.username(username: username)
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
let client = try builder.build()
|
||||
try client.login(username: username, password: password)
|
||||
return client
|
||||
try self.authenticationService.login(username: username, password: password)
|
||||
}
|
||||
|
||||
switch await loginTask.result {
|
||||
@@ -55,7 +74,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
Benchmark.endTrackingForIdentifier("Login", message: "Login failed")
|
||||
|
||||
MXLog.error("Failed logging in with error: \(error)")
|
||||
guard let error = error as? ClientError else { return .failure(.failedLoggingIn) }
|
||||
guard let error = error as? AuthenticationError else { return .failure(.failedLoggingIn) }
|
||||
|
||||
switch error.code {
|
||||
case .forbidden:
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// AuthenticationServiceProtocol.swift
|
||||
// AuthenticationServiceProxyProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Doug on 29/06/2022.
|
||||
@@ -11,16 +11,17 @@ import Foundation
|
||||
enum AuthenticationServiceError: Error {
|
||||
case invalidServer
|
||||
case invalidCredentials
|
||||
case invalidHomeserverAddress
|
||||
case accountDeactivated
|
||||
case failedLoggingIn
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol AuthenticationServiceProtocol {
|
||||
protocol AuthenticationServiceProxyProtocol {
|
||||
var homeserver: LoginHomeserver { get }
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Performs a password login using the current homeserver.
|
||||
func login(username: String, password: String) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
}
|
||||
+2
-2
@@ -8,11 +8,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class MockAuthenticationService: AuthenticationServiceProtocol {
|
||||
class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
let validCredentials = (username: "alice", password: "12345678")
|
||||
private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg
|
||||
|
||||
func startLogin(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
func configure(for homeserverAddress: String) async -> Result<Void, AuthenticationServiceError> {
|
||||
// Map the address to the mock homeservers
|
||||
if LoginHomeserver.mockMatrixDotOrg.address.contains(homeserverAddress) {
|
||||
homeserver = .mockMatrixDotOrg
|
||||
@@ -15,7 +15,7 @@ enum MatrixErrorCode: String, CaseIterable {
|
||||
case forbidden = "M_FORBIDDEN"
|
||||
}
|
||||
|
||||
extension ClientError {
|
||||
extension AuthenticationError {
|
||||
var code: MatrixErrorCode {
|
||||
guard case let .Generic(message) = self else { return .unknown }
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// FileManager.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Doug on 19/07/2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
/// The URL of the primary app group container.
|
||||
var appGroupContainerURL: URL? {
|
||||
containerURL(forSecurityApplicationGroupIdentifier: ElementInfoPlist.appGroupIdentifier)
|
||||
}
|
||||
}
|
||||
@@ -16,42 +16,42 @@ class KeychainController: KeychainControllerProtocol {
|
||||
keychain = Keychain(service: identifier)
|
||||
}
|
||||
|
||||
func setAccessToken(_ accessToken: String, forUsername username: String) {
|
||||
func setRestoreToken(_ restoreToken: String, forUsername username: String) {
|
||||
do {
|
||||
try keychain.set(accessToken, key: username)
|
||||
try keychain.set(restoreToken, key: username)
|
||||
} catch {
|
||||
MXLog.error("Failed storing user access token with error: \(error)")
|
||||
MXLog.error("Failed storing user restore token with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func accessTokenForUsername(_ username: String) -> String? {
|
||||
func restoreTokenForUsername(_ username: String) -> String? {
|
||||
do {
|
||||
return try keychain.get(username)
|
||||
} catch {
|
||||
MXLog.error("Failed retrieving user access token")
|
||||
MXLog.error("Failed retrieving user restore token")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func accessTokens() -> [(username: String, accessToken: String)] {
|
||||
func restoreTokens() -> [KeychainCredentials] {
|
||||
keychain.allKeys().compactMap { username in
|
||||
guard let accessToken = accessTokenForUsername(username) else {
|
||||
guard let restoreToken = restoreTokenForUsername(username) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (username, accessToken)
|
||||
return KeychainCredentials(userID: username, restoreToken: restoreToken)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAccessTokenForUsername(_ username: String) {
|
||||
func removeRestoreTokenForUsername(_ username: String) {
|
||||
do {
|
||||
try keychain.remove(username)
|
||||
} catch {
|
||||
MXLog.error("Failed removing access token with error: \(error)")
|
||||
MXLog.error("Failed removing restore token with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllAccessTokens() {
|
||||
func removeAllRestoreTokens() {
|
||||
do {
|
||||
try keychain.removeAll()
|
||||
} catch {
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol KeychainControllerProtocol {
|
||||
func setAccessToken(_ accessToken: String, forUsername username: String)
|
||||
func accessTokenForUsername(_ username: String) -> String?
|
||||
func accessTokens() -> [(username: String, accessToken: String)]
|
||||
func removeAccessTokenForUsername(_ username: String)
|
||||
func removeAllAccessTokens()
|
||||
struct KeychainCredentials {
|
||||
let userID: String
|
||||
let restoreToken: String
|
||||
}
|
||||
|
||||
protocol KeychainControllerProtocol {
|
||||
func setRestoreToken(_ accessToken: String, forUsername username: String)
|
||||
func restoreTokenForUsername(_ username: String) -> String?
|
||||
func restoreTokens() -> [KeychainCredentials]
|
||||
func removeRestoreTokenForUsername(_ username: String)
|
||||
func removeAllRestoreTokens()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
/// Whether or not there are sessions in the store.
|
||||
var hasSessions: Bool { !keychainController.accessTokens().isEmpty }
|
||||
var hasSessions: Bool { !keychainController.restoreTokens().isEmpty }
|
||||
|
||||
/// The base directory where all session data is stored.
|
||||
var baseDirectoryPath: String { baseDirectory().path }
|
||||
|
||||
init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
keychainController = KeychainController(identifier: bundleIdentifier)
|
||||
@@ -31,13 +34,13 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
|
||||
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
||||
let availableAccessTokens = keychainController.accessTokens()
|
||||
let availableCredentials = keychainController.restoreTokens()
|
||||
|
||||
guard let usernameTokenTuple = availableAccessTokens.first else {
|
||||
guard let credentials = availableCredentials.first else {
|
||||
return .failure(.missingCredentials)
|
||||
}
|
||||
|
||||
switch await restorePreviousLogin(usernameTokenTuple) {
|
||||
switch await restorePreviousLogin(credentials) {
|
||||
case .success(let clientProxy):
|
||||
return .success(UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
||||
@@ -47,8 +50,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
|
||||
// On any restoration failure reset the token and restart
|
||||
keychainController.removeAllAccessTokens()
|
||||
deleteBaseDirectory(for: usernameTokenTuple.username)
|
||||
keychainController.removeAllRestoreTokens()
|
||||
deleteSessionDirectory(for: credentials.userID)
|
||||
|
||||
return .failure(error)
|
||||
}
|
||||
@@ -68,22 +71,21 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
}
|
||||
|
||||
func logout(userSession: UserSessionProtocol) {
|
||||
let username = userSession.clientProxy.userIdentifier
|
||||
keychainController.removeAccessTokenForUsername(username)
|
||||
deleteBaseDirectory(for: username)
|
||||
let userID = userSession.clientProxy.userIdentifier
|
||||
keychainController.removeRestoreTokenForUsername(userID)
|
||||
deleteSessionDirectory(for: userID)
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, accessToken: String)) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result<ClientProxyProtocol, UserSessionStoreError> {
|
||||
Benchmark.startTrackingForIdentifier("Login", message: "Started restoring previous login")
|
||||
|
||||
let basePath = baseDirectoryPath(for: usernameTokenTuple.username)
|
||||
let builder = ClientBuilder()
|
||||
.basePath(path: basePath)
|
||||
.username(username: usernameTokenTuple.username)
|
||||
.basePath(path: baseDirectoryPath)
|
||||
.username(username: credentials.userID)
|
||||
|
||||
let loginTask: Task<Client, Error> = Task.detached {
|
||||
let client = try builder.build()
|
||||
try client.restoreLogin(restoreToken: usernameTokenTuple.accessToken)
|
||||
try client.restoreLogin(restoreToken: credentials.restoreToken)
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -101,7 +103,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
let accessToken = try client.restoreToken()
|
||||
let userId = try client.userId()
|
||||
|
||||
keychainController.setAccessToken(accessToken, forUsername: userId)
|
||||
keychainController.setRestoreToken(accessToken, forUsername: userId)
|
||||
} catch {
|
||||
MXLog.error("Failed setting up user session with error: \(error)")
|
||||
return .failure(.failedSettingUpSession)
|
||||
@@ -112,25 +114,30 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .success(clientProxy)
|
||||
}
|
||||
|
||||
func baseDirectoryPath(for username: String) -> String {
|
||||
guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
fatalError("Should always be able to retrieve the caches directory")
|
||||
private func deleteSessionDirectory(for userID: String) {
|
||||
// Rust sanitises the user ID replacing invalid characters with an _
|
||||
let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_")
|
||||
let url = baseDirectory().appendingPathComponent(sanitisedUserID)
|
||||
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
MXLog.failure("Failed deleting the session data: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func baseDirectory() -> URL {
|
||||
guard let appGroupContainerURL = FileManager.default.appGroupContainerURL else {
|
||||
fatalError("Should always be able to retrieve the container directory")
|
||||
}
|
||||
|
||||
url = url.appendingPathComponent(username)
|
||||
let url = appGroupContainerURL
|
||||
.appendingPathComponent("Library", isDirectory: true)
|
||||
.appendingPathComponent("Caches", isDirectory: true)
|
||||
.appendingPathComponent("Sessions", isDirectory: true)
|
||||
|
||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
return url.path
|
||||
}
|
||||
|
||||
private func deleteBaseDirectory(for username: String) {
|
||||
guard var url = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
fatalError("Should always be able to retrieve the caches directory")
|
||||
}
|
||||
|
||||
url = url.appendingPathComponent(username)
|
||||
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ protocol UserSessionStoreProtocol {
|
||||
/// Whether or not there are sessions in the store.
|
||||
var hasSessions: Bool { get }
|
||||
|
||||
/// Returns the location to store user data for a particular username.
|
||||
var baseDirectoryPath: String { get }
|
||||
|
||||
/// Restores an existing user session.
|
||||
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError>
|
||||
|
||||
@@ -36,7 +39,4 @@ protocol UserSessionStoreProtocol {
|
||||
|
||||
/// Logs out of the specified session.
|
||||
func logout(userSession: UserSessionProtocol)
|
||||
|
||||
/// Returns the location to store user data for a particular username.
|
||||
func baseDirectoryPath(for username: String) -> String
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user