mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Remove AppAuth and prepare for Rust OIDC. (#870)
This commit is contained in:
parent
a28e686c8d
commit
185710adf4
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<T>(_ transform: (Key) -> T) -> [T: Value] {
|
||||
.init(map { (transform($0.key), $0.value) }) { first, _ in first }
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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<Void, Error>? {
|
||||
willSet {
|
||||
currentTask?.cancel()
|
||||
}
|
||||
}
|
||||
@CancellableTask private var currentTask: Task<Void, Error>?
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<UserSessionProtocol, AuthenticationServiceError> {
|
||||
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<Result<UserSessionProtocol, AuthenticationServiceError>, 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
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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<UserSessionProtocol, AuthenticationServiceError> {
|
||||
guard case let .oidc(issuerURL) = homeserver.loginMode else {
|
||||
return .failure(.oidcError(.notSupported))
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthenticationDataProxy, AuthenticationServiceError> {
|
||||
.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))
|
||||
// }
|
||||
}
|
||||
|
||||
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 loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
.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<UserSessionProtocol, AuthenticationServiceError> {
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<Void, AuthenticationServiceError>
|
||||
/// Performs login using OIDC for the current homeserver.
|
||||
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthenticationDataProxy, AuthenticationServiceError>
|
||||
/// Add docs.
|
||||
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
/// Performs a password login using the current homeserver.
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError>
|
||||
}
|
||||
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
|
@ -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<Void, AuthenticationServiceError> {
|
||||
// Map the address to the mock homeservers
|
||||
@ -42,7 +42,11 @@ class MockAuthenticationServiceProxy: AuthenticationServiceProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func loginWithOIDC(userAgent: OIDExternalUserAgentIOS) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
func urlForOIDCLogin() async -> Result<OIDCAuthenticationDataProxy, AuthenticationServiceError> {
|
||||
.failure(.oidcError(.notSupported))
|
||||
}
|
||||
|
||||
func loginWithOIDCCallback(_ callbackURL: URL, data: OIDCAuthenticationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
.failure(.oidcError(.notSupported))
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,13 +28,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
|
||||
|
||||
func didReceiveSyncUpdate(summary: UpdateSummary) {
|
||||
@ -554,6 +557,10 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func updateRestorationToken() {
|
||||
callbacks.send(.updateRestorationToken)
|
||||
}
|
||||
|
||||
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
||||
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ enum ClientProxyCallback {
|
||||
case receivedSyncUpdate
|
||||
case receivedAuthError(isSoftLogout: Bool)
|
||||
case receivedNotification(NotificationItemProxyProtocol)
|
||||
case updateRestorationToken
|
||||
}
|
||||
|
||||
enum ClientProxyError: Error {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ enum UserSessionCallback {
|
||||
case sessionVerificationNeeded
|
||||
case didVerifySession
|
||||
case didReceiveAuthError(isSoftLogout: Bool)
|
||||
case updateRestorationToken
|
||||
}
|
||||
|
||||
protocol UserSessionProtocol {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,6 @@ targets:
|
||||
- package: Compound
|
||||
- package: Algorithms
|
||||
- package: AnalyticsEvents
|
||||
- package: AppAuth
|
||||
- package: Collections
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
@ -29,7 +29,6 @@ targets:
|
||||
- target: ElementX
|
||||
- package: MatrixRustSDK
|
||||
- package: AnalyticsEvents
|
||||
- package: AppAuth
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
- package: KeychainAccess
|
||||
|
1
changelog.d/261.wip
Normal file
1
changelog.d/261.wip
Normal file
@ -0,0 +1 @@
|
||||
Remove AppAuth library and prepare for Rust OIDC.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user