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
@@ -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
@@ -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
}
}
}