Remove AppAuth and prepare for Rust OIDC. (#870)

This commit is contained in:
Doug 2023-05-11 16:46:36 +01:00 committed by GitHub
parent a28e686c8d
commit 185710adf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 249 additions and 264 deletions

View File

@ -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"

View File

@ -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
}

View File

@ -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 {

View File

@ -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 }
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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)
}
}
}
}

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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))
}
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<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))
// }
}
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
}
}

View File

@ -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()
// }
// }

View File

@ -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,10 +42,14 @@ 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))
}
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
// Login only succeeds if the username and password match the valid credentials property
guard username == validCredentials.username, password == validCredentials.password else {

View File

@ -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))
}
}
}
}

View File

@ -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))
}

View File

@ -22,6 +22,7 @@ enum ClientProxyCallback {
case receivedSyncUpdate
case receivedAuthError(isSoftLogout: Bool)
case receivedNotification(NotificationItemProxyProtocol)
case updateRestorationToken
}
enum ClientProxyError: Error {

View File

@ -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
}
}
}

View File

@ -21,6 +21,7 @@ enum UserSessionCallback {
case sessionVerificationNeeded
case didVerifySession
case didReceiveAuthError(isSoftLogout: Bool)
case updateRestorationToken
}
protocol UserSessionProtocol {

View File

@ -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
}
}

View File

@ -132,7 +132,6 @@ targets:
- package: Compound
- package: Algorithms
- package: AnalyticsEvents
- package: AppAuth
- package: Collections
- package: DeviceKit
- package: DTCoreText

View File

@ -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
View File

@ -0,0 +1 @@
Remove AppAuth library and prepare for Rust OIDC.

View File

@ -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