mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Use a universal link for the OIDC callback. (#1681)
This commit is contained in:
@@ -32,6 +32,8 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var oidcPresenter: OIDCAuthenticationPresenter?
|
||||
|
||||
weak var delegate: AuthenticationCoordinatorDelegate?
|
||||
|
||||
init(authenticationService: AuthenticationServiceProxyProtocol,
|
||||
@@ -53,6 +55,15 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
func handleOIDCRedirectURL(_ url: URL) {
|
||||
guard let oidcPresenter else {
|
||||
MXLog.error("Failed to find an OIDC request in progress.")
|
||||
return
|
||||
}
|
||||
|
||||
oidcPresenter.handleUniversalLinkCallback(url)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@@ -167,12 +178,14 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
||||
let presenter = OIDCAuthenticationPresenter(authenticationService: authenticationService,
|
||||
oidcRedirectURL: appSettings.oidcRedirectURL,
|
||||
presentationAnchor: presentationAnchor)
|
||||
self.oidcPresenter = presenter
|
||||
switch await presenter.authenticate(using: oidcData) {
|
||||
case .success(let userSession):
|
||||
userHasSignedIn(userSession: userSession)
|
||||
case .failure(let error):
|
||||
handleError(error)
|
||||
}
|
||||
oidcPresenter = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,16 @@ 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: OIDCAuthenticationDataProxy
|
||||
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: AuthenticationServiceProxyProtocol, oidcRedirectURL: URL, presentationAnchor: UIWindow) {
|
||||
self.authenticationService = authenticationService
|
||||
self.oidcRedirectURL = oidcRedirectURL
|
||||
@@ -35,41 +45,70 @@ class OIDCAuthenticationPresenter: NSObject {
|
||||
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 {
|
||||
continuation.resume(returning: .failure(AuthenticationServiceError.oidcError(.userCancellation)))
|
||||
self.completeAuthentication(throwing: .oidcError(.userCancellation))
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(returning: .failure(AuthenticationServiceError.oidcError(.unknown)))
|
||||
self.completeAuthentication(throwing: .oidcError(.unknown))
|
||||
return
|
||||
}
|
||||
|
||||
completeAuthentication(callbackURL: url, data: oidcData, continuation: continuation)
|
||||
completeAuthentication(callbackURL: url)
|
||||
}
|
||||
|
||||
session.prefersEphemeralWebBrowserSession = false
|
||||
session.presentationContextProvider = self
|
||||
|
||||
request = Request(session: session, oidcData: oidcData, continuation: continuation)
|
||||
|
||||
session.start()
|
||||
}
|
||||
}
|
||||
|
||||
private func completeAuthentication(callbackURL: URL,
|
||||
data: OIDCAuthenticationDataProxy,
|
||||
continuation: CheckedContinuation<Result<UserSessionProtocol, AuthenticationServiceError>, Never>) {
|
||||
/// 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: data) {
|
||||
switch await authenticationService.loginWithOIDCCallback(callbackURL, data: request.oidcData) {
|
||||
case .success(let userSession):
|
||||
continuation.resume(returning: .success(userSession))
|
||||
request.continuation.resume(returning: .success(userSession))
|
||||
case .failure(let error):
|
||||
continuation.resume(returning: .failure(error))
|
||||
request.continuation.resume(returning: .failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
request.continuation.resume(returning: .failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ASWebAuthenticationPresentationContextProviding
|
||||
|
||||
+12
@@ -48,6 +48,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var authenticationService: AuthenticationServiceProxyProtocol { parameters.authenticationService }
|
||||
private var oidcPresenter: OIDCAuthenticationPresenter?
|
||||
|
||||
var actions: AnyPublisher<SoftLogoutScreenCoordinatorResult, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
@@ -92,6 +93,15 @@ 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 = "SoftLogoutLoading"
|
||||
@@ -151,12 +161,14 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
||||
let presenter = OIDCAuthenticationPresenter(authenticationService: parameters.authenticationService,
|
||||
oidcRedirectURL: ServiceLocator.shared.settings.oidcRedirectURL,
|
||||
presentationAnchor: presentationAnchor)
|
||||
self.oidcPresenter = presenter
|
||||
switch await presenter.authenticate(using: oidcData) {
|
||||
case .success(let userSession):
|
||||
actionsSubject.send(.signedIn(userSession))
|
||||
case .failure(let error):
|
||||
handleError(error)
|
||||
}
|
||||
self.oidcPresenter = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user