Use a universal link for the OIDC callback. (#1681)

This commit is contained in:
Doug
2023-09-28 14:12:57 +01:00
committed by GitHub
parent 718e01f3bc
commit ec3f5d425e
9 changed files with 162 additions and 53 deletions
@@ -45,6 +45,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
private var userSessionFlowCoordinator: UserSessionFlowCoordinator?
private var authenticationCoordinator: AuthenticationCoordinator?
private var softLogoutCoordinator: SoftLogoutScreenCoordinator?
private let backgroundTaskService: BackgroundTaskServiceProtocol
@@ -145,6 +146,12 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
if let route = appRouteURLParser.route(from: url) {
switch route {
case .oidcCallback(let url):
if stateMachine.state == .softLogout {
softLogoutCoordinator?.handleOIDCRedirectURL(url)
} else {
authenticationCoordinator?.handleOIDCRedirectURL(url)
}
case .genericCallLink(let url):
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
@@ -158,11 +165,6 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
return true
}
// Until we have an OIDC callback AppRoute, handle it manually.
if url.absoluteString.starts(with: appSettings.oidcRedirectURL.absoluteString) {
MXLog.error("OIDC callback through Universal Links not implemented.")
}
return false
}
@@ -170,6 +172,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
func authenticationCoordinator(_ authenticationCoordinator: AuthenticationCoordinator, didLoginWithSession userSession: UserSessionProtocol) {
self.userSession = userSession
self.authenticationCoordinator = nil
stateMachine.processEvent(.createdUserSession)
}
@@ -305,8 +308,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
break
case (_, .signOut(let isSoft), .signingOut):
self.logout(isSoft: isSoft)
case (.signingOut, .completedSigningOut(let isSoft), .signedOut):
self.presentSplashScreen(isSoftLogout: isSoft)
case (.signingOut, .completedSigningOut, .signedOut):
self.presentSplashScreen(isSoftLogout: false)
case (.signingOut, .showSoftLogout, .softLogout):
self.presentSplashScreen(isSoftLogout: true)
case (.signedIn, .clearCache, .initial):
self.clearCache()
default:
@@ -370,7 +375,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
keyBackupNeeded: false,
userIndicatorController: ServiceLocator.shared.userIndicatorController)
let coordinator = SoftLogoutScreenCoordinator(parameters: parameters)
self.softLogoutCoordinator = coordinator
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
@@ -378,8 +383,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
switch action {
case .signedIn(let session):
self.userSession = session
self.softLogoutCoordinator = nil
stateMachine.processEvent(.createdUserSession)
case .clearAllData:
self.softLogoutCoordinator = nil
stateMachine.processEvent(.signOut(isSoft: false))
}
}
@@ -437,7 +444,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
userSessionFlowCoordinator?.stop()
guard !isSoft else {
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
stateMachine.processEvent(.showSoftLogout)
hideLoadingIndicator()
return
}
@@ -459,7 +466,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate,
ServiceLocator.shared.analytics.optOut()
ServiceLocator.shared.analytics.resetConsentState()
stateMachine.processEvent(.completedSigningOut(isSoft: isSoft))
stateMachine.processEvent(.completedSigningOut)
// Handle OIDC's RP-Initiated Logout if needed. Don't fallback to an ASWebAuthenticationSession
// as it looks weird to show an alert to the user asking them to sign in to their provider.
@@ -22,8 +22,10 @@ class AppCoordinatorStateMachine {
enum State: StateType {
/// The initial state, used before the AppCoordinator starts
case initial
/// Showing the login screen
/// Showing the authentication flow
case signedOut
/// Showing the soft logout flow
case softLogout
/// Opening an existing session.
case restoringSession
@@ -45,15 +47,17 @@ class AppCoordinatorStateMachine {
/// Restoring session failed.
case failedRestoringSession
/// A session has been created
/// A session has been created.
case createdUserSession
/// Request sign out
/// Request sign out.
case signOut(isSoft: Bool)
/// Signing out completed
case completedSigningOut(isSoft: Bool)
/// Request the soft logout screen.
case showSoftLogout
/// Signing out completed.
case completedSigningOut
/// Request cache clearing
/// Request cache clearing.
case clearCache
}
@@ -70,11 +74,15 @@ class AppCoordinatorStateMachine {
private func configure() {
stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut])
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn])
stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn,
.softLogout => .signedIn])
stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession])
stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn])
stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut])
stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false) => .signedOut])
stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true) => .softLogout])
stateMachine.addRoutes(event: .clearCache, transitions: [.signedIn => .initial])
// Transitions with associated values need to be handled through `addRouteMapping`
@@ -82,8 +90,6 @@ class AppCoordinatorStateMachine {
switch (event, fromState) {
case (.signOut(let isSoft), _):
return .signingOut(isSoft: isSoft)
case (.completedSigningOut, .signingOut):
return .signedOut
default:
return nil
}
@@ -120,10 +120,15 @@ final class AppSettings {
/// The URL that is opened when tapping the Learn more button on the sliding sync alert during authentication.
let slidingSyncLearnMoreURL: URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
/// The redirect URL used for OIDC.
let oidcRedirectURL: URL = "io.element:/callback"
/// 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. The bundle ID suffix avoids app association conflicts between Element X, Nightly and PR builds.
let oidcRedirectURL = {
guard let bundleIDSuffix = InfoPlistReader.main.bundleIdentifier.split(separator: ".").last,
let redirectURL = URL(string: "https://mobile.element.io/oidc/\(bundleIDSuffix)")
else { fatalError("Failed creating a valid OIDC redirect URL.") }
return redirectURL
}()
/// The date that the call to `/login` completed successfully. This is used to put
/// a hard wall on the history of encrypted messages until we have key backup.
@@ -30,7 +30,7 @@ struct AppRouteURLParser {
init(appSettings: AppSettings) {
urlParsers = [
ElementAppURLParser(appSettings: appSettings),
OIDCCallbackURLParser(appSettings: appSettings),
ElementCallURLParser()
]
}
@@ -50,34 +50,24 @@ struct AppRouteURLParser {
///
/// The following Universal Links are missing parsers.
/// - app.element.io
/// - staging.element.io
/// - develop.element.io
/// - mobile.element.io
protocol URLParser {
func route(from url: URL) -> AppRoute?
}
/// The parser for the main Element website.
struct ElementAppURLParser: URLParser {
private let knownHosts = ["app.element.io", "staging.element.io", "develop.element.io"]
/// 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 let host = url.host, knownHosts.contains(host) else {
return nil
}
let pathComponents = url.pathComponents
// OIDC callback URL.
if pathComponents.count == 3, pathComponents[0] == "mobile", pathComponents[1] == "oidc" {
return .oidcCallback(url: url)
}
return nil
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`
/// The parser for Element Call links. This always returns a `.genericCallLink`.
struct ElementCallURLParser: URLParser {
private let knownHosts = ["call.element.io"]
private let customSchemeURLQueryParameterName = "url"