mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
Use a universal link for the OIDC callback. (#1681)
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user