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
22 changed files with 249 additions and 264 deletions
@@ -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
}
}
@@ -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,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 {
@@ -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))
}
}
}
}