diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7128a57ac..9464d7857 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS", - "state" : { - "revision" : "71cde449f13d453227e687458144bde372d30fc7", - "version" : "1.6.2" - } - }, { "identity" : "compound-design-tokens", "kind" : "remoteSourceControl", @@ -181,7 +172,7 @@ { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", "version" : "1.11.0" diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 930ed84b2..a4b9b1bc1 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -159,6 +159,10 @@ class AppCoordinator: AppCoordinatorProtocol { self.presentSplashScreen() case (.restoringSession, .createdUserSession, .signedIn): self.setupUserSession() + case (.signingOut, .signOut, .signingOut): + // We can ignore signOut when already in the process of signing out, + // such as the SDK sending an authError due to token invalidation. + break case (_, .signOut(let isSoft), .signingOut): self.logout(isSoft: isSoft) case (.signingOut, .completedSigningOut(let isSoft), .signedOut): @@ -336,7 +340,9 @@ class AppCoordinator: AppCoordinatorProtocol { guard let self else { return } switch callback { case .didReceiveAuthError(let isSoftLogout): - self.stateMachine.processEvent(.signOut(isSoft: isSoftLogout)) + stateMachine.processEvent(.signOut(isSoft: isSoftLogout)) + case .updateRestorationToken: + userSessionStore.refreshRestorationToken(for: userSession) default: break } diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 95ae56016..e8765adcd 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -89,6 +89,17 @@ final class AppSettings { /// The URL that is opened when tapping the Learn more button on the sliding sync alert during authentication. let slidingSyncLearnMoreURL = URL(staticString: "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md") + /// The redirect URL used for OIDC. + let oidcRedirectURL = URL(staticString: "io.element:/callback") + /// The app's main URL shown when using OIDC. + let oidcClientURL = URL(staticString: "https://element.io") + /// The app's Terms of Service URL shown when using OIDC. + let oidcTermsURL = URL(staticString: "https://element.io/user-terms-of-service") + /// The app's Privacy Policy URL shown when using OIDC. + let oidcPolicyURL = URL(staticString: "https://element.io/privacy") + /// Any pre-defined static client registrations for OIDC issuers. + let oidcStaticRegistrations = [URL(staticString: "https://id.thirdroom.io/realms/thirdroom"): "elementx"] + // MARK: - Notifications var pusherAppId: String { diff --git a/ElementX/Sources/Other/Extensions/Dictionary.swift b/ElementX/Sources/Other/Extensions/Dictionary.swift index 7e2c7e0a1..4793c6463 100644 --- a/ElementX/Sources/Other/Extensions/Dictionary.swift +++ b/ElementX/Sources/Other/Extensions/Dictionary.swift @@ -24,4 +24,9 @@ extension Dictionary { } return String(data: data, encoding: .utf8) } + + /// Returns a dictionary containing the original values keyed by the results of mapping the given closure over its keys. + func mapKeys(_ transform: (Key) -> T) -> [T: Value] { + .init(map { (transform($0.key), $0.value) }) { first, _ in first } + } } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift index 3321f307e..c2d58c7dd 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginHomeserver.swift @@ -64,8 +64,7 @@ extension LoginHomeserver { /// A mock homeserver that supports only supports authentication via a single SSO provider. static var mockOIDC: LoginHomeserver { - let issuerURL = URL(staticString: "https://auth.company.com") - return LoginHomeserver(address: "company.com", loginMode: .oidc(issuerURL)) + LoginHomeserver(address: "company.com", loginMode: .oidc) } /// A mock homeserver that only with no supported login flows. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift index ce7109f26..3a7aefdc3 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginMode.swift @@ -20,8 +20,8 @@ import Foundation enum LoginMode: Equatable { /// The login mode hasn't been determined yet. case unknown - /// The homeserver supports login via OpenID Connect at the associated URL. - case oidc(URL) + /// The homeserver supports login via OpenID Connect. + case oidc /// The homeserver supports login with a password. case password /// The homeserver only allows login with unsupported mechanisms. Use fallback instead. diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index e266164c8..2e2ede22f 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -14,7 +14,7 @@ // limitations under the License. // -import AppAuth +import Combine import SwiftUI struct LoginScreenCoordinatorParameters { @@ -32,16 +32,10 @@ enum LoginScreenCoordinatorAction { final class LoginScreenCoordinator: CoordinatorProtocol { private let parameters: LoginScreenCoordinatorParameters private var viewModel: LoginScreenViewModelProtocol - private let hostingController: UIViewController - /// Passed to the OIDC service to provide a view controller from which to present the authentication session. - private let oidcUserAgent: OIDExternalUserAgentIOS? - private var currentTask: Task? { - willSet { - currentTask?.cancel() - } - } + @CancellableTask private var currentTask: Task? + private let oidcAuthenticationPresenter: OIDCAuthenticationPresenter private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator } @@ -54,8 +48,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { viewModel = LoginScreenViewModel(homeserver: parameters.authenticationService.homeserver) - hostingController = UIHostingController(rootView: LoginScreen(context: viewModel.context)) - oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController) + oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService) } // MARK: - Public @@ -74,7 +67,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { case .login(let username, let password): self.login(username: username, password: password) case .continueWithOIDC: - self.loginWithOIDC() + self.continueWithOIDC() } } } @@ -126,27 +119,34 @@ final class LoginScreenCoordinator: CoordinatorProtocol { viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount)) case .slidingSyncNotAvailable: viewModel.displayError(.slidingSyncAlert) + case .oidcError(.notSupported): + // Temporary alert hijacking the use of .notSupported, can be removed when OIDC support is in the SDK. + viewModel.displayError(.alert(L10n.commonServerNotSupported)) + case .oidcError(.userCancellation): + // No need to show an error, the user cancelled authentication. + break default: viewModel.displayError(.alert(L10n.errorUnknown)) } } - private func loginWithOIDC() { - guard let oidcUserAgent else { - handleError(AuthenticationServiceError.oidcError(.notSupported)) - return - } - + private func continueWithOIDC() { startLoading(isInteractionBlocking: true) Task { - switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) { - case .success(let userSession): - callback?(.signedIn(userSession)) - stopLoading() + switch await authenticationService.urlForOIDCLogin() { case .failure(let error): stopLoading() handleError(error) + case .success(let oidcData): + stopLoading() + + switch await oidcAuthenticationPresenter.authenticate(using: oidcData) { + case .success(let userSession): + callback?(.signedIn(userSession)) + case .failure(let error): + handleError(error) + } } } } diff --git a/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift new file mode 100644 index 000000000..2ee458819 --- /dev/null +++ b/ElementX/Sources/Screens/Authentication/OIDCAuthenticationPresenter.swift @@ -0,0 +1,77 @@ +// +// Copyright 2023 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 AuthenticationServices + +/// Presents a web authentication session for an OIDC request. +class OIDCAuthenticationPresenter { + private let authenticationService: AuthenticationServiceProxyProtocol + private let oidcPresentationContent = OIDCPresentationContext() + + init(authenticationService: AuthenticationServiceProxyProtocol) { + self.authenticationService = authenticationService + } + + /// Presents a web authentication session for the supplied data. + func authenticate(using oidcData: OIDCAuthenticationDataProxy) async -> Result { + await withCheckedContinuation { continuation in + let session = ASWebAuthenticationSession(url: oidcData.url, + callbackURLScheme: ServiceLocator.shared.settings.oidcRedirectURL.scheme) { [weak self] url, error in + guard let self else { return } + + guard let url else { + if let nsError = error as? NSError, + nsError.domain == ASWebAuthenticationSessionErrorDomain, + nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue { + continuation.resume(returning: .failure(AuthenticationServiceError.oidcError(.userCancellation))) + return + } + + continuation.resume(returning: .failure(AuthenticationServiceError.oidcError(.unknown))) + return + } + + completeAuthentication(callbackURL: url, data: oidcData, continuation: continuation) + } + + session.prefersEphemeralWebBrowserSession = false + session.presentationContextProvider = oidcPresentationContent + session.start() + } + } + + private func completeAuthentication(callbackURL: URL, + data: OIDCAuthenticationDataProxy, + continuation: CheckedContinuation, Never>) { + Task { + switch await authenticationService.loginWithOIDCCallback(callbackURL, data: data) { + case .success(let userSession): + continuation.resume(returning: .success(userSession)) + case .failure(let error): + continuation.resume(returning: .failure(error)) + } + } + } +} + +class OIDCPresentationContext: NSObject, ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + guard let window = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first?.keyWindow else { + fatalError("Failed to find the main window.") + } + return window + } +} diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift index d4b1c45a1..a86009106 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift @@ -14,7 +14,6 @@ // limitations under the License. // -import AppAuth import SwiftUI struct SoftLogoutScreenCoordinatorParameters { @@ -43,11 +42,8 @@ enum SoftLogoutScreenCoordinatorResult: CustomStringConvertible { final class SoftLogoutScreenCoordinator: CoordinatorProtocol { private let parameters: SoftLogoutScreenCoordinatorParameters private var viewModel: SoftLogoutScreenViewModelProtocol - private let hostingController: UIViewController - /// Passed to the OIDC service to provide a view controller from which to present the authentication session. - private let oidcUserAgent: OIDExternalUserAgentIOS? - /// The wizard used to handle the registration flow. + private let oidcAuthenticationPresenter: OIDCAuthenticationPresenter private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService } var callback: (@MainActor (SoftLogoutScreenCoordinatorResult) -> Void)? @@ -56,13 +52,11 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { self.parameters = parameters let homeserver = parameters.authenticationService.homeserver - viewModel = SoftLogoutScreenViewModel(credentials: parameters.credentials, homeserver: homeserver, keyBackupNeeded: parameters.keyBackupNeeded) - hostingController = UIHostingController(rootView: SoftLogoutScreen(context: viewModel.context)) - oidcUserAgent = OIDExternalUserAgentIOS(presenting: hostingController) + oidcAuthenticationPresenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService) } // MARK: - Public @@ -80,7 +74,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { case .clearAllData: self.callback?(.clearAllData) case .continueWithOIDC: - self.loginWithOIDC() + self.continueWithOIDC() } } } @@ -136,22 +130,23 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { } } - private func loginWithOIDC() { - guard let oidcUserAgent else { - handleError(AuthenticationServiceError.oidcError(.notSupported)) - return - } - + private func continueWithOIDC() { startLoading() - + Task { - switch await authenticationService.loginWithOIDC(userAgent: oidcUserAgent) { - case .success(let userSession): - callback?(.signedIn(userSession)) - stopLoading() + switch await authenticationService.urlForOIDCLogin() { case .failure(let error): stopLoading() handleError(error) + case .success(let oidcData): + stopLoading() + + switch await oidcAuthenticationPresenter.authenticate(using: oidcData) { + case .success(let userSession): + callback?(.signedIn(userSession)) + case .failure(let error): + handleError(error) + } } } } @@ -163,6 +158,12 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { viewModel.displayError(.alert(L10n.screenLoginErrorInvalidCredentials)) case .accountDeactivated: viewModel.displayError(.alert(L10n.screenLoginErrorDeactivatedAccount)) + case .oidcError(.notSupported): + // Temporary alert hijacking the use of .notSupported, can be removed when OIDC support is in the SDK. + viewModel.displayError(.alert(L10n.commonServerNotSupported)) + case .oidcError(.userCancellation): + // No need to show an error, the user cancelled authentication. + break default: viewModel.displayError(.alert(L10n.errorUnknown)) } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift index 7e5c3de70..2f02c3649 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift @@ -14,7 +14,6 @@ // limitations under the License. // -import AppAuth import Foundation import MatrixRustSDK @@ -26,8 +25,18 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { init(userSessionStore: UserSessionStoreProtocol) { self.userSessionStore = userSessionStore + + // guard let settings = ServiceLocator.shared.settings else { fatalError("The settings must be set.") } + // let oidcConfiguration = OidcConfiguration(clientName: InfoPlistReader.main.bundleDisplayName, + // redirectUri: settings.oidcRedirectURL.absoluteString, + // clientUri: settings.oidcClientURL.absoluteString, + // tosUri: settings.oidcTermsURL.absoluteString, + // policyUri: settings.oidcPolicyURL.absoluteString, + // staticRegistrations: settings.oidcStaticRegistrations.mapKeys { $0.absoluteString }) + authenticationService = AuthenticationService(basePath: userSessionStore.baseDirectory.path, passphrase: nil, + // oidcConfiguration: oidcConfiguration, customSlidingSyncProxy: ServiceLocator.shared.settings.slidingSyncProxyURL?.absoluteString) } @@ -42,8 +51,8 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } if let details = authenticationService.homeserverDetails() { - if let issuer = details.authenticationIssuer(), let issuerURL = URL(string: issuer) { - homeserver.loginMode = .oidc(issuerURL) + if details.authenticationIssuer() != nil { + homeserver.loginMode = .oidc } else if details.supportsPasswordLogin() { homeserver.loginMode = .password } else { @@ -62,40 +71,32 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } } - func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result { - guard case let .oidc(issuerURL) = homeserver.loginMode else { - return .failure(.oidcError(.notSupported)) - } - - let token: String - let deviceID = generateDeviceID() - do { - let oidcService = OIDCService(issuerURL: issuerURL) - let configuration = try await oidcService.metadata() - let registationResponse = try await oidcService.registerClient(metadata: configuration) - let authResponse = try await oidcService.presentWebAuthentication(metadata: configuration, - clientID: registationResponse.clientID, - scope: "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:\(deviceID)", - userAgent: userAgent) - let tokenResponse = try await oidcService.redeemCodeForTokens(authResponse: authResponse) - - guard let accessToken = tokenResponse.accessToken else { return .failure(.oidcError(.unknown)) } - token = accessToken - } catch let error as OIDCError { - MXLog.error("Login with OIDC failed: \(error)") - return .failure(.oidcError(error)) - } catch { - MXLog.error("Login with OIDC failed: \(error)") - return .failure(.failedLoggingIn) - } - - do { - let client = try authenticationService.restoreWithAccessToken(token: token, deviceId: deviceID) - return await userSession(for: client) - } catch { - MXLog.error("Failed restoring with access token: \(error)") - return .failure(.failedLoggingIn) - } + func urlForOIDCLogin() async -> Result { + .failure(.oidcError(.notSupported)) +// do { +// let oidcData = try await Task.dispatch(on: .global()) { +// try self.authenticationService.urlForOidcLogin() +// } +// return .success(OIDCAuthenticationDataProxy(underlyingData: oidcData)) +// } catch { +// MXLog.error("Failed to get URL for OIDC login: \(error)") +// return .failure(.oidcError(.urlFailure)) +// } + } + + func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result { + .failure(.oidcError(.notSupported)) +// do { +// let client = try await Task.dispatch(on: .global()) { +// try self.authenticationService.loginWithOidcCallback(authenticationData: data.underlyingData, callbackUrl: callbackURL.absoluteString) +// } +// return await userSession(for: client) +// } catch AuthenticationError.OidcCancelled { +// return .failure(.oidcError(.userCancellation)) +// } catch { +// MXLog.error("Login with OIDC failed: \(error)") +// return .failure(.failedLoggingIn) +// } } func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result { @@ -133,13 +134,4 @@ class AuthenticationServiceProxy: AuthenticationServiceProxyProtocol { return .failure(.failedLoggingIn) } } - - private func generateDeviceID() -> String { - var deviceID = "" - for _ in 0..<10 { - guard let scalar = UnicodeScalar(Int.random(in: 65...90)) else { fatalError() } - deviceID.append(Character(scalar)) - } - return deviceID - } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift index f9325c94f..940863e6e 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProxyProtocol.swift @@ -14,8 +14,8 @@ // limitations under the License. // -import AppAuth import Foundation +import MatrixRustSDK enum AuthenticationServiceError: Error { /// An error occurred during OIDC authentication. @@ -34,7 +34,37 @@ protocol AuthenticationServiceProxyProtocol { /// Sets up the service for login on the specified homeserver address. func configure(for homeserverAddress: String) async -> Result /// Performs login using OIDC for the current homeserver. - func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result + func urlForOIDCLogin() async -> Result + /// Add docs. + func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result /// Performs a password login using the current homeserver. func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result } + +// MARK: - OIDC + +enum OIDCError: Error { + /// Failed to get the URL that should be presented for login. + case urlFailure + /// The user cancelled the login. + case userCancellation + /// OIDC isn't supported on the currently configured server. + case notSupported + /// An unknown error occurred. + case unknown +} + +struct OIDCAuthenticationDataProxy: Equatable { +// let underlyingData: OidcAuthenticationData +// +// var url: URL { +// URL(string: underlyingData.loginUrl())! +// } + let url = URL(staticString: "https://theroadtonowhere") +} + +// extension OidcAuthenticationData: Equatable { +// public static func == (lhs: MatrixRustSDK.OidcAuthenticationData, rhs: MatrixRustSDK.OidcAuthenticationData) -> Bool { +// lhs.loginUrl() == rhs.loginUrl() +// } +// } diff --git a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift index bffb55be6..3e07eea6f 100644 --- a/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift +++ b/ElementX/Sources/Services/Authentication/MockAuthenticationServiceProxy.swift @@ -14,13 +14,13 @@ // limitations under the License. // -import AppAuth +import Foundation +import MatrixRustSDK class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { let validCredentials = (username: "alice", password: "12345678") private(set) var homeserver: LoginHomeserver = .mockMatrixDotOrg - var oidcUserAgent: OIDExternalUserAgentIOS? func configure(for homeserverAddress: String) async -> Result { // Map the address to the mock homeservers @@ -42,10 +42,14 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol { } } - func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result { + func urlForOIDCLogin() async -> Result { .failure(.oidcError(.notSupported)) } - + + func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result { + .failure(.oidcError(.notSupported)) + } + func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result { // Login only succeeds if the username and password match the valid credentials property guard username == validCredentials.username, password == validCredentials.password else { diff --git a/ElementX/Sources/Services/Authentication/OIDCService.swift b/ElementX/Sources/Services/Authentication/OIDCService.swift deleted file mode 100644 index 961d8d071..000000000 --- a/ElementX/Sources/Services/Authentication/OIDCService.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// 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 AppAuth - -/// Errors thrown by the OIDC service. -enum OIDCError: Error { - case notSupported - case metadataMissingRegistrationEndpoint - case userCancellation - case missingTokenExchangeRequest - case unknown -} - -/// A proof of concept implementation of a service that assists with authentication via OIDC. -/// It will be replaced by an implementation in the Rust SDK tracked in the following issue: -/// https://github.com/matrix-org/matrix-rust-sdk/issues/859 -class OIDCService { - private let issuerURL: URL - private var authState: OIDAuthState - - private var metadata: OIDServiceConfiguration? - /// Redirect URI for the request. Must match the `client_uri` in reverse DNS format. - private var redirectURI = URL(staticString: "io.element:/callback") - - /// Maintains a strong ref to the authorization session that's in progress. - private var session: OIDExternalUserAgentSession? - - init(issuerURL: URL) { - self.issuerURL = issuerURL - authState = OIDAuthState(authorizationResponse: nil, tokenResponse: nil, registrationResponse: nil) - } - - /// Get OpenID Connect endpoints and ensure that dynamic client registration is configured. - func metadata() async throws -> OIDServiceConfiguration { - let metadata = try await OIDAuthorizationService.discoverConfiguration(forIssuer: issuerURL) - - guard metadata.registrationEndpoint != nil else { - throw OIDCError.metadataMissingRegistrationEndpoint - } - - return metadata - } - - /// Perform dynamic client registration and then store the response - func registerClient(metadata: OIDServiceConfiguration) async throws -> OIDRegistrationResponse { - let extraParams = [ - "client_name": "ElementX iOS", - "client_uri": "https://element.io", - "tos_uri": "https://element.io/user-terms-of-service", - "policy_uri": "https://element.io/privacy" - ] - - let nonTemplatizedRequest = OIDRegistrationRequest(configuration: metadata, - redirectURIs: [redirectURI], - responseTypes: nil, - grantTypes: [OIDGrantTypeAuthorizationCode, OIDGrantTypeRefreshToken], - subjectType: nil, - tokenEndpointAuthMethod: "none", - additionalParameters: extraParams) - - let registrationResponse = try await OIDAuthorizationService.perform(nonTemplatizedRequest) - - MXLog.info("Registration data retrieved successfully") - MXLog.debug("Created dynamic client: ID: \(registrationResponse.clientID)") - - // This is a PoC, a complete implementation would persist the client ID and secret for reuse. - - return registrationResponse - } - - /// Trigger a redirect with standard parameters. - /// `acr_values` can be sent as an extra parameter, to control authentication methods. - func presentWebAuthentication(metadata: OIDServiceConfiguration, - clientID: String, - scope: String, - userAgent: OIDExternalUserAgent) async throws -> OIDAuthorizationResponse { - let scopesArray = scope.components(separatedBy: " ") - let request = OIDAuthorizationRequest(configuration: metadata, - clientId: clientID, - clientSecret: nil, - scopes: scopesArray, - redirectURL: redirectURI, - responseType: OIDResponseTypeCode, - additionalParameters: nil) - let result: OIDAuthorizationResponse = try await withCheckedThrowingContinuation { continuation in - self.session = OIDAuthorizationService.present(request, externalUserAgent: userAgent) { response, error in - guard let response else { - if let error { - MXLog.info("User cancelled the ASWebAuthenticationSession window") - continuation.resume(with: .failure(self.isUserCancellationError(error) ? OIDCError.userCancellation : error)) - } else { - continuation.resume(with: .failure(OIDCError.unknown)) - } - return - } - - MXLog.info("Authorization response received successfully") - continuation.resume(with: .success(response)) - } - } - return result - } - - /// Handle the authorization response, including the user closing the Chrome Custom Tab - func redeemCodeForTokens(authResponse: OIDAuthorizationResponse) async throws -> OIDTokenResponse { - guard let request = authResponse.tokenExchangeRequest() else { throw OIDCError.missingTokenExchangeRequest } - return try await OIDAuthorizationService.perform(request, originalAuthorizationResponse: authResponse) - } - - /// We can check for specific error codes to handle the user cancelling the ASWebAuthenticationSession window. - private func isUserCancellationError(_ error: Error) -> Bool { - let error = error as NSError - return error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue - } -} - -extension OIDAuthorizationService { - /// An async version of `perform(_:originalAuthorizationResponse:callback:)`. - class func perform(_ request: OIDTokenRequest, - originalAuthorizationResponse authorizationResponse: OIDAuthorizationResponse?) async throws -> OIDTokenResponse { - try await withCheckedThrowingContinuation { continuation in - perform(request, originalAuthorizationResponse: authorizationResponse) { response, error in - guard let response else { - continuation.resume(with: .failure(error ?? OIDCError.unknown)) - return - } - continuation.resume(with: .success(response)) - } - } - } -} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 60850725a..b16f6a078 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -27,13 +27,16 @@ private class WeakClientProxyWrapper: ClientDelegate, NotificationDelegate, Slid } // MARK: - ClientDelegate - - func didReceiveSyncUpdate() { } func didReceiveAuthError(isSoftLogout: Bool) { MXLog.error("Received authentication error, softlogout=\(isSoftLogout)") clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout) } + + func didRefreshTokens() { + MXLog.info("The session has updated tokens.") + clientProxy?.updateRestorationToken() + } // MARK: - SlidingSyncDelegate @@ -554,6 +557,10 @@ class ClientProxy: ClientProxyProtocol { } } + fileprivate func updateRestorationToken() { + callbacks.send(.updateRestorationToken) + } + fileprivate func didReceiveAuthError(isSoftLogout: Bool) { callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout)) } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 3bb962ea7..32d55a3ea 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -22,6 +22,7 @@ enum ClientProxyCallback { case receivedSyncUpdate case receivedAuthError(isSoftLogout: Bool) case receivedNotification(NotificationItemProxyProtocol) + case updateRestorationToken } enum ClientProxyError: Error { diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index 32a394b46..71c371616 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -89,9 +89,15 @@ class UserSession: UserSessionProtocol { authErrorCancellable = clientProxy.callbacks .receive(on: DispatchQueue.main) .sink { [weak self] callback in - if case .receivedAuthError(let isSoftLogout) = callback { - self?.callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout)) - self?.tearDownAuthErrorWatchdog() + guard let self else { return } + switch callback { + case .receivedAuthError(let isSoftLogout): + callbacks.send(.didReceiveAuthError(isSoftLogout: isSoftLogout)) + tearDownAuthErrorWatchdog() + case .updateRestorationToken: + callbacks.send(.updateRestorationToken) + default: + break } } } diff --git a/ElementX/Sources/Services/Session/UserSessionProtocol.swift b/ElementX/Sources/Services/Session/UserSessionProtocol.swift index b8da4cd65..0d7caa855 100644 --- a/ElementX/Sources/Services/Session/UserSessionProtocol.swift +++ b/ElementX/Sources/Services/Session/UserSessionProtocol.swift @@ -21,6 +21,7 @@ enum UserSessionCallback { case sessionVerificationNeeded case didVerifySession case didReceiveAuthError(isSoftLogout: Bool) + case updateRestorationToken } protocol UserSessionProtocol { diff --git a/ElementX/Sources/Services/UserSession/RestorationToken.swift b/ElementX/Sources/Services/UserSession/RestorationToken.swift index c8fddc62b..38bbe8405 100644 --- a/ElementX/Sources/Services/UserSession/RestorationToken.swift +++ b/ElementX/Sources/Services/UserSession/RestorationToken.swift @@ -43,6 +43,7 @@ extension MatrixRustSDK.Session: Codable { deviceId: container.decode(String.self, forKey: .deviceId), homeserverUrl: container.decode(String.self, forKey: .homeserverUrl), slidingSyncProxy: container.decode(String.self, forKey: .slidingSyncProxy)) + // oidcData: container.decodeIfPresent(String.self, forKey: .oidcData) } public func encode(to encoder: Encoder) throws { @@ -53,9 +54,11 @@ extension MatrixRustSDK.Session: Codable { try container.encode(deviceId, forKey: .deviceId) try container.encode(homeserverUrl, forKey: .homeserverUrl) try container.encode(slidingSyncProxy, forKey: .slidingSyncProxy) + // try container.encode(oidcData, forKey: .oidcData) } enum CodingKeys: String, CodingKey { case accessToken, refreshToken, userId, deviceId, homeserverUrl, slidingSyncProxy + // case oidcData } } diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 9774b1899..a79944099 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -132,7 +132,6 @@ targets: - package: Compound - package: Algorithms - package: AnalyticsEvents - - package: AppAuth - package: Collections - package: DeviceKit - package: DTCoreText diff --git a/UITests/SupportingFiles/target.yml b/UITests/SupportingFiles/target.yml index bf615d31f..2eab735e7 100644 --- a/UITests/SupportingFiles/target.yml +++ b/UITests/SupportingFiles/target.yml @@ -29,7 +29,6 @@ targets: - target: ElementX - package: MatrixRustSDK - package: AnalyticsEvents - - package: AppAuth - package: DeviceKit - package: DTCoreText - package: KeychainAccess diff --git a/changelog.d/261.wip b/changelog.d/261.wip new file mode 100644 index 000000000..e1df16258 --- /dev/null +++ b/changelog.d/261.wip @@ -0,0 +1 @@ +Remove AppAuth library and prepare for Rust OIDC. diff --git a/project.yml b/project.yml index 46f793cf5..1333cba2b 100644 --- a/project.yml +++ b/project.yml @@ -56,9 +56,6 @@ packages: AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events minorVersion: 0.5.0 - AppAuth: - url: https://github.com/openid/AppAuth-iOS - minorVersion: 1.6.0 Collections: url: https://github.com/apple/swift-collections minorVersion: 1.0.0