mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Add an AccountProvider type and use it within the authentication flow. (#6103)
* Add an AccountProvider type and use it within AppSettings. * Use the AccountProvider type within the Authentication flow too. * Fix unit test compilation with Swift 6.4 * Make sure defaultAccountProvider takes allowOtherAccountProviders into account. * Document a caveat on using AuthenticationService.homeserver.value.accountProvider.
This commit is contained in:
@@ -22,6 +22,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
private let appHooks: AppHooks
|
||||
|
||||
private let homeserverSubject: CurrentValueSubject<LoginHomeserver, Never>
|
||||
/// The currently configured homeserver.
|
||||
///
|
||||
/// **Note:** The value's `accountProvider` will always be `.generic` after calling `configure(for:flow:)`.
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> {
|
||||
homeserverSubject.asCurrentValuePublisher()
|
||||
}
|
||||
@@ -64,17 +67,17 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
|
||||
// When updating these, don't forget to update the reset method too.
|
||||
homeserverSubject = .init(LoginHomeserver(address: appSettings.defaultServer, loginMode: .unknown))
|
||||
homeserverSubject = .init(LoginHomeserver(accountProvider: appSettings.defaultAccountProvider, loginMode: .unknown))
|
||||
flow = .login
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
|
||||
func configure(for serverNameOrBaseURL: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError> {
|
||||
do {
|
||||
var homeserver = LoginHomeserver(address: homeserverAddress, loginMode: .unknown)
|
||||
var homeserver = LoginHomeserver(accountProvider: .generic(serverNameOrBaseURL), loginMode: .unknown)
|
||||
|
||||
let client = try await makeClient(homeserverAddress: homeserverAddress)
|
||||
let client = try await makeClient(serverNameOrBaseURL: serverNameOrBaseURL)
|
||||
let loginDetails = await client.homeserverLoginDetails()
|
||||
|
||||
homeserver.loginMode = if loginDetails.supportsOauthLogin() {
|
||||
@@ -106,7 +109,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
return .failure(.elementProRequired(serverName: serverName))
|
||||
} catch {
|
||||
MXLog.error("Failed configuring a server: \(error)")
|
||||
return .failure(.invalidHomeserverAddress)
|
||||
return .failure(.invalidServerNameOrBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +195,8 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
|
||||
// n.b. We rely on the SDK checking that the intent of the QR is suitable for us to login with.
|
||||
|
||||
// Future versions of the QR should always give us the baseUrl
|
||||
guard let scannedServerNameOrBaseUrl = qrData.baseUrl() ?? qrData.serverName() else {
|
||||
// Future versions of the QR should always give us the baseURL
|
||||
guard let scannedServerNameOrBaseURL = qrData.baseUrl() ?? qrData.serverName() else {
|
||||
// With the older version of QR we treat the presence of serverName as meaning that the other device
|
||||
// is not signed in.
|
||||
MXLog.error("The QR code is from a device that is not yet signed in.")
|
||||
@@ -201,7 +204,20 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
// n.b. We deliberatley don't check whether the received server is in our appSettings.accountProviders
|
||||
// When account provider selection is restricted, make sure the scanned server is allowed.
|
||||
// However, generic providers aren't detailed enough, so continue if one is present.
|
||||
if !appSettings.allowOtherAccountProviders, !appSettings.accountProviders.contains(where: \.isGeneric) {
|
||||
// A base URL from the QR code is expected to be a valid URL when present.
|
||||
let scannedBaseURL = qrData.baseUrl().flatMap(URL.init(string:))
|
||||
|
||||
guard appSettings.accountProviders.contains(serverName: qrData.serverName(), orBaseURL: scannedBaseURL) else {
|
||||
MXLog.error("The scanned QR code is for an account provider that isn't allowed.")
|
||||
let error = QRCodeLoginError.providerNotAllowed(scannedProvider: scannedServerNameOrBaseURL,
|
||||
allowedProviders: appSettings.accountProviders.map(\.serverNameOrBaseURL))
|
||||
progressSubject.send(completion: .failure(.qrCodeError(error)))
|
||||
return progressSubject.asCurrentValuePublisher()
|
||||
}
|
||||
}
|
||||
|
||||
// The SDK calls the listener from arbitrary threads; onMainActor forwards the progress
|
||||
// updates on the main actor in FIFO order.
|
||||
@@ -212,7 +228,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
|
||||
Task {
|
||||
do {
|
||||
let client = try await makeClient(homeserverAddress: scannedServerNameOrBaseUrl)
|
||||
let client = try await makeClient(serverNameOrBaseURL: scannedServerNameOrBaseURL)
|
||||
let qrCodeHandler = client.newLoginWithQrCodeHandler(oauthConfiguration: appSettings.oAuthConfiguration.rustValue)
|
||||
try await qrCodeHandler.scan(qrCodeData: qrData, progressListener: listener)
|
||||
|
||||
@@ -240,19 +256,19 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
}
|
||||
|
||||
func reset() {
|
||||
homeserverSubject.send(LoginHomeserver(address: appSettings.defaultServer, loginMode: .unknown))
|
||||
homeserverSubject.send(LoginHomeserver(accountProvider: appSettings.defaultAccountProvider, loginMode: .unknown))
|
||||
flow = .login
|
||||
client = nil
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func makeClient(homeserverAddress: String) async throws -> ClientProtocol {
|
||||
private func makeClient(serverNameOrBaseURL: String) async throws -> ClientProtocol {
|
||||
// Use a fresh session directory each time the user enters a different server
|
||||
// so that caches (e.g. server versions) are always fresh for the new server.
|
||||
rotateSessionDirectory()
|
||||
|
||||
let client = try await clientFactory.makeAuthenticationClient(homeserverAddress: homeserverAddress,
|
||||
let client = try await clientFactory.makeAuthenticationClient(serverNameOrBaseURL: serverNameOrBaseURL,
|
||||
sessionDirectories: sessionDirectories,
|
||||
passphrase: passphrase,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
@@ -286,7 +302,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
MXLog.info("Checking Classic app account: \(classicAppAccount)")
|
||||
|
||||
do {
|
||||
let client = try await clientFactory.makeInMemoryClient(homeserverAddress: classicAppAccount.homeserverURL.absoluteString,
|
||||
let client = try await clientFactory.makeInMemoryClient(serverNameOrBaseURL: classicAppAccount.homeserverURL.absoluteString,
|
||||
clientSessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appSettings: appSettings,
|
||||
appHooks: appHooks)
|
||||
|
||||
@@ -26,7 +26,7 @@ enum AuthenticationServiceError: Error, Equatable {
|
||||
|
||||
case invalidServer
|
||||
case invalidCredentials
|
||||
case invalidHomeserverAddress
|
||||
case invalidServerNameOrBaseURL
|
||||
case invalidWellKnown(String)
|
||||
case slidingSyncNotAvailable
|
||||
case loginNotSupported
|
||||
@@ -39,12 +39,14 @@ enum AuthenticationServiceError: Error, Equatable {
|
||||
|
||||
protocol AuthenticationServiceProtocol: QRCodeLoginServiceProtocol {
|
||||
/// The currently configured homeserver.
|
||||
///
|
||||
/// **Note:** The value's `accountProvider` will always be `.generic` after calling `configure(for:flow:)`.
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { get }
|
||||
/// The type of flow the service is currently configured with.
|
||||
var flow: AuthenticationFlow { get }
|
||||
|
||||
/// Sets up the service for login on the specified homeserver address.
|
||||
func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Sets up the service for login or registration using the supplied server name or base URL.
|
||||
func configure(for serverNameOrBaseURL: String, flow: AuthenticationFlow) async -> Result<Void, AuthenticationServiceError>
|
||||
/// Performs login using OAuth for the current homeserver.
|
||||
func urlForOAuthLogin(loginHint: String?) async -> Result<OAuthAuthorizationDataProxy, AuthenticationServiceError>
|
||||
/// Asks the SDK to abort an ongoing OAuth login if we didn't get a callback to complete the request with.
|
||||
|
||||
@@ -14,7 +14,7 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
|
||||
// MARK: Authentication
|
||||
|
||||
#if IS_MAIN_APP
|
||||
func makeAuthenticationClient(homeserverAddress: String,
|
||||
func makeAuthenticationClient(serverNameOrBaseURL: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
@@ -29,12 +29,12 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
|
||||
.enableAutomaticBackPagination(enableAutomaticBackPagination: appSettings.automaticBackPaginationEnabled)
|
||||
.sqliteStore(config: .init(dataPath: sessionDirectories.dataPath, cachePath: sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: passphrase))
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
|
||||
|
||||
return try await build(builder, for: .authentication, appHooks: appHooks)
|
||||
}
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: String,
|
||||
func makeInMemoryClient(serverNameOrBaseURL: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol {
|
||||
@@ -45,7 +45,7 @@ nonisolated struct ClientFactory: ClientFactoryProtocol {
|
||||
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
|
||||
threadsEnabled: appSettings.threadsEnabled)
|
||||
.inMemoryStore()
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: homeserverAddress)
|
||||
.serverNameOrHomeserverUrl(serverNameOrUrl: serverNameOrBaseURL)
|
||||
|
||||
return try await build(builder, for: .classicAppAccount, appHooks: appHooks)
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ nonisolated protocol ClientFactoryProtocol {
|
||||
// MARK: Authentication
|
||||
|
||||
#if IS_MAIN_APP
|
||||
func makeAuthenticationClient(homeserverAddress: String,
|
||||
func makeAuthenticationClient(serverNameOrBaseURL: String,
|
||||
sessionDirectories: SessionDirectories,
|
||||
passphrase: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
|
||||
func makeInMemoryClient(homeserverAddress: String,
|
||||
func makeInMemoryClient(serverNameOrBaseURL: String,
|
||||
clientSessionDelegate: ClientSessionDelegate,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) async throws -> ClientProtocol
|
||||
|
||||
@@ -67,7 +67,7 @@ class HomeserverHistoryManager {
|
||||
/// in all lowercase. This is the list that matches are made against.
|
||||
private func updateCachedServers() {
|
||||
let previous = appSettings.previousServers.map { $0.lowercased() }
|
||||
let defaultProviders = appSettings.accountProviders.map { $0.lowercased() }
|
||||
let defaultProviders = appSettings.accountProviders.map { $0.serverNameOrBaseURL.lowercased() }
|
||||
|
||||
cachedServers = previous + defaultProviders
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user