Use an https callback for OIDC once again. (#3461)

* Use the new WAS callback type and return back to the https callback for OIDC.

* Simplify OIDCAuthenticationPresenter now it doesn't need to handle universal links.

* Remove old unit tests.
This commit is contained in:
Doug 2024-10-29 11:28:05 +00:00 committed by GitHub
parent a583892e01
commit 589df7d76e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 34 additions and 143 deletions

View File

@ -195,12 +195,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
if let route = appRouteURLParser.route(from: url) {
switch route {
case .oidcCallback(let url):
if stateMachine.state == .softLogout {
softLogoutCoordinator?.handleOIDCRedirectURL(url)
} else {
authenticationFlowCoordinator?.handleOIDCRedirectURL(url)
}
case .genericCallLink(let url):
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(route, animated: true)

View File

@ -153,14 +153,8 @@ final class AppSettings {
/// Any pre-defined static client registrations for OIDC issuers.
let oidcStaticRegistrations: [URL: String] = ["https://id.thirdroom.io/realms/thirdroom": "elementx"]
/// The redirect URL used for OIDC.
let oidcRedirectURL = {
guard let url = URL(string: "\(InfoPlistReader.main.appScheme):/callback") else {
fatalError("Invalid OIDC redirect URL")
}
return url
}()
/// The redirect URL used for OIDC. This no longer uses universal links so we don't need the bundle ID to avoid conflicts between Element X, Nightly and PR builds.
let oidcRedirectURL: URL = "https://element.io/oidc/login"
private(set) lazy var oidcConfiguration = OIDCConfigurationProxy(clientName: InfoPlistReader.main.bundleDisplayName,
redirectURI: oidcRedirectURL,

View File

@ -9,8 +9,6 @@ import Foundation
import MatrixRustSDK
enum AppRoute: Equatable {
/// The callback used to complete login with OIDC.
case oidcCallback(url: URL)
/// The app's home screen.
case roomList
/// A room, shown as the root of the stack (popping any child rooms).
@ -52,7 +50,6 @@ struct AppRouteURLParser {
urlParsers = [
MatrixPermalinkParser(),
ElementWebURLParser(domains: appSettings.elementWebHosts),
OIDCCallbackURLParser(appSettings: appSettings),
ElementCallURLParser()
]
}
@ -76,16 +73,6 @@ protocol URLParser {
func route(from url: URL) -> AppRoute?
}
/// The parser for the OIDC callback URL. This always returns a `.oidcCallback`.
struct OIDCCallbackURLParser: URLParser {
let appSettings: AppSettings
func route(from url: URL) -> AppRoute? {
guard url.absoluteString.starts(with: appSettings.oidcRedirectURL.absoluteString) else { return nil }
return .oidcCallback(url: url)
}
}
/// The parser for Element Call links. This always returns a `.genericCallLink`.
struct ElementCallURLParser: URLParser {
private let knownHosts = ["call.element.io"]

View File

@ -65,15 +65,6 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
fatalError()
}
func handleOIDCRedirectURL(_ url: URL) {
guard let oidcPresenter else {
MXLog.error("Failed to find an OIDC request in progress.")
return
}
oidcPresenter.handleUniversalLinkCallback(url)
}
// MARK: - Private
private func showStartScreen() {

View File

@ -163,7 +163,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
break // These are converted to a room ID route one level above.
case .roomList, .userProfile, .call, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
case .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings:
break // These routes can't be handled.
}
}

View File

@ -282,8 +282,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentCallScreen(genericCallLink: url)
case .settings, .chatBackupSettings:
settingsFlowCoordinator.handleAppRoute(appRoute, animated: animated)
case .oidcCallback:
break
}
}

View File

@ -14,16 +14,6 @@ class OIDCAuthenticationPresenter: NSObject {
private let oidcRedirectURL: URL
private let presentationAnchor: UIWindow
/// The data required to complete a request.
struct Request {
let session: ASWebAuthenticationSession
let oidcData: OIDCAuthorizationDataProxy
let continuation: CheckedContinuation<Result<UserSessionProtocol, AuthenticationServiceError>, Never>
}
/// The current request in progress. This is a single use value and will be moved on access.
@Consumable private var request: Request?
init(authenticationService: AuthenticationServiceProtocol, oidcRedirectURL: URL, presentationAnchor: UIWindow) {
self.authenticationService = authenticationService
self.oidcRedirectURL = oidcRedirectURL
@ -33,74 +23,35 @@ class OIDCAuthenticationPresenter: NSObject {
/// Presents a web authentication session for the supplied data.
func authenticate(using oidcData: OIDCAuthorizationDataProxy) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
await withCheckedContinuation { continuation in
let session = ASWebAuthenticationSession(url: oidcData.url,
callbackURLScheme: oidcRedirectURL.scheme) { [weak self] url, error in
// This closure won't be called if the scheme is https, see handleUniversalLinkCallback for more info.
guard let self else { return }
guard let url else {
// Check for user cancellation to avoid showing an alert in that instance.
if let nsError = error as? NSError,
nsError.domain == ASWebAuthenticationSessionErrorDomain,
nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
self.completeAuthentication(throwing: .oidcError(.userCancellation))
return
}
self.completeAuthentication(throwing: .oidcError(.unknown))
return
}
completeAuthentication(callbackURL: url)
let (url, error) = await withCheckedContinuation { continuation in
let session = ASWebAuthenticationSession(url: oidcData.url, callback: .oidcRedirectURL(oidcRedirectURL)) { url, error in
continuation.resume(returning: (url, error))
}
session.prefersEphemeralWebBrowserSession = false
session.presentationContextProvider = self
request = Request(session: session, oidcData: oidcData, continuation: continuation)
session.start()
}
}
/// This method will be used if the `appSettings.oidcRedirectURL`'s scheme is `https`.
/// When using a custom scheme, the redirect will be handled by the web auth session's closure.
func handleUniversalLinkCallback(_ url: URL) {
completeAuthentication(callbackURL: url)
}
/// Completes the authentication by exchanging the callback URL for a user session.
private func completeAuthentication(callbackURL: URL) {
guard let request else {
MXLog.error("Failed to complete authentication. Missing request.")
return
}
if callbackURL.scheme?.starts(with: "http") == true {
request.session.cancel()
}
Task {
switch await authenticationService.loginWithOIDCCallback(callbackURL, data: request.oidcData) {
case .success(let userSession):
request.continuation.resume(returning: .success(userSession))
case .failure(let error):
request.continuation.resume(returning: .failure(error))
guard let url else {
// Check for user cancellation to avoid showing an alert in that instance.
if let nsError = error as? NSError,
nsError.domain == ASWebAuthenticationSessionErrorDomain,
nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
await authenticationService.abortOIDCLogin(data: oidcData)
return .failure(.oidcError(.userCancellation))
}
}
}
/// Aborts the authentication with the supplied error.
private func completeAuthentication(throwing error: AuthenticationServiceError) {
guard let request else {
MXLog.error("Failed to throw authentication error. Missing request.")
return
await authenticationService.abortOIDCLogin(data: oidcData)
return .failure(.oidcError(.unknown))
}
Task {
await authenticationService.abortOIDCLogin(data: request.oidcData)
request.continuation.resume(returning: .failure(error))
switch await authenticationService.loginWithOIDCCallback(url, data: oidcData) {
case .success(let userSession):
return .success(userSession)
case .failure(let error):
return .failure(error)
}
}
}
@ -110,3 +61,15 @@ class OIDCAuthenticationPresenter: NSObject {
extension OIDCAuthenticationPresenter: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { presentationAnchor }
}
extension ASWebAuthenticationSession.Callback {
static func oidcRedirectURL(_ url: URL) -> Self {
if url.scheme == "https", let host = url.host() {
.https(host: host, path: url.path())
} else if let scheme = url.scheme {
.customScheme(scheme)
} else {
fatalError("Invalid OIDC redirect URL: \(url)")
}
}
}

View File

@ -85,15 +85,6 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
AnyView(SoftLogoutScreen(context: viewModel.context))
}
func handleOIDCRedirectURL(_ url: URL) {
guard let oidcPresenter else {
MXLog.error("Failed to find an OIDC request in progress.")
return
}
oidcPresenter.handleUniversalLinkCallback(url)
}
// MARK: - Private
private static let loadingIndicatorIdentifier = "\(SoftLogoutScreenCoordinator.self)-Loading"

View File

@ -28,7 +28,7 @@ class OIDCAccountSettingsPresenter: NSObject {
/// Presents a web authentication session for the supplied data.
func start() {
let session = ASWebAuthenticationSession(url: accountURL, callbackURLScheme: oidcRedirectURL.scheme) { _, _ in }
let session = ASWebAuthenticationSession(url: accountURL, callback: .oidcRedirectURL(oidcRedirectURL)) { _, _ in }
session.prefersEphemeralWebBrowserSession = false
session.presentationContextProvider = self
session.start()

View File

@ -73,33 +73,6 @@ class AppRouteURLParserTests: XCTestCase {
XCTAssertEqual(appRouteURLParser.route(from: customSchemeURL), nil)
}
func testOIDCCallbackRoute() {
// Given an OIDC callback for this app.
let callbackURL = appSettings.oidcRedirectURL.appending(queryItems: [URLQueryItem(name: "state", value: "12345"),
URLQueryItem(name: "code", value: "67890")])
// When parsing that route.
let route = appRouteURLParser.route(from: callbackURL)
// Then it should be considered a valid OIDC callback.
XCTAssertEqual(route, AppRoute.oidcCallback(url: callbackURL))
}
func testOIDCCallbackAppVariantRoute() {
// Given an OIDC callback for a different app variant.
let callbackURL = appSettings.oidcRedirectURL
.deletingLastPathComponent()
.appending(component: "elementz")
.appending(queryItems: [URLQueryItem(name: "state", value: "12345"),
URLQueryItem(name: "code", value: "67890")])
// When parsing that route in this app.
let route = appRouteURLParser.route(from: callbackURL)
// Then the route shouldn't be considered valid and should be ignored.
XCTAssertEqual(route, nil)
}
func testMatrixUserURL() {
let userID = "@test:matrix.org"
guard let url = URL(string: "https://matrix.to/#/\(userID)") else {