From f0510076709f1f5741066752e08604b81c5d16a5 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Thu, 29 Feb 2024 09:59:45 +0000 Subject: [PATCH] Add support for HTTPS callbacks [SDK-4749] (#832) --- Auth0/ASProvider.swift | 58 +++++++++++++++------ Auth0/Auth0WebAuth.swift | 35 +++++++++---- Auth0/SafariProvider.swift | 4 +- Auth0/WebAuth.swift | 18 ++++++- Auth0Tests/ASProviderSpec.swift | 18 ++++--- Auth0Tests/SafariProviderSpec.swift | 78 +++++++++++++++-------------- Auth0Tests/WebAuthSpec.swift | 62 +++++++++++++++++++---- 7 files changed, 191 insertions(+), 82 deletions(-) diff --git a/Auth0/ASProvider.swift b/Auth0/ASProvider.swift index eac4c467..aa845191 100644 --- a/Auth0/ASProvider.swift +++ b/Auth0/ASProvider.swift @@ -1,23 +1,36 @@ #if WEB_AUTH_PLATFORM import AuthenticationServices +typealias ASHandler = ASWebAuthenticationSession.CompletionHandler + extension WebAuthentication { - static func asProvider(urlScheme: String, ephemeralSession: Bool = false) -> WebAuthProvider { + static func asProvider(redirectURL: URL, ephemeralSession: Bool = false) -> WebAuthProvider { return { url, callback in - let session = ASWebAuthenticationSession(url: url, callbackURLScheme: urlScheme) { - guard let callbackURL = $0, $1 == nil else { - if let error = $1, case ASWebAuthenticationSessionError.canceledLogin = error { - return callback(.failure(WebAuthError(code: .userCancelled))) - } else if let error = $1 { - return callback(.failure(WebAuthError(code: .other, cause: error))) - } - - return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed")))) - } + let session: ASWebAuthenticationSession - _ = TransactionStore.shared.resume(callbackURL) + #if compiler(>=5.10) + if #available(iOS 17.4, macOS 14.4, *) { + if redirectURL.scheme == "https" { + session = ASWebAuthenticationSession(url: url, + callback: .https(host: redirectURL.host!, + path: redirectURL.path), + completionHandler: completionHandler(callback)) + } else { + session = ASWebAuthenticationSession(url: url, + callback: .customScheme(redirectURL.scheme!), + completionHandler: completionHandler(callback)) + } + } else { + session = ASWebAuthenticationSession(url: url, + callbackURLScheme: redirectURL.scheme, + completionHandler: completionHandler(callback)) } + #else + session = ASWebAuthenticationSession(url: url, + callbackURLScheme: redirectURL.scheme, + completionHandler: completionHandler(callback)) + #endif session.prefersEphemeralWebBrowserSession = ephemeralSession @@ -25,14 +38,31 @@ extension WebAuthentication { } } + static let completionHandler: (_ callback: @escaping WebAuthProviderCallback) -> ASHandler = { callback in + return { + guard let callbackURL = $0, $1 == nil else { + if let error = $1 as? NSError, + error.userInfo.isEmpty, + case ASWebAuthenticationSessionError.canceledLogin = error { + return callback(.failure(WebAuthError(code: .userCancelled))) + } else if let error = $1 { + return callback(.failure(WebAuthError(code: .other, cause: error))) + } + + return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed")))) + } + + _ = TransactionStore.shared.resume(callbackURL) + } + } } class ASUserAgent: NSObject, WebAuthUserAgent { let session: ASWebAuthenticationSession - let callback: (WebAuthResult) -> Void + let callback: WebAuthProviderCallback - init(session: ASWebAuthenticationSession, callback: @escaping (WebAuthResult) -> Void) { + init(session: ASWebAuthenticationSession, callback: @escaping WebAuthProviderCallback) { self.session = session self.callback = callback super.init() diff --git a/Auth0/Auth0WebAuth.swift b/Auth0/Auth0WebAuth.swift index f2fc7308..f211a38f 100644 --- a/Auth0/Auth0WebAuth.swift +++ b/Auth0/Auth0WebAuth.swift @@ -20,6 +20,7 @@ final class Auth0WebAuth: WebAuth { private let responseType = "code" private(set) var parameters: [String: String] = [:] + private(set) var https = false private(set) var ephemeralSession = false private(set) var issuer: String private(set) var leeway: Int = 60 * 1000 // Default leeway is 60 seconds @@ -35,15 +36,26 @@ final class Auth0WebAuth: WebAuth { } lazy var redirectURL: URL? = { - guard let bundleIdentifier = Bundle.main.bundleIdentifier, - let domain = self.url.host, - let baseURL = URL(string: "\(bundleIdentifier)://\(domain)") else { return nil } + guard let bundleID = Bundle.main.bundleIdentifier, let domain = self.url.host else { return nil } + let scheme: String + + #if compiler(>=5.10) + if #available(iOS 17.4, macOS 14.4, *) { + scheme = https ? "https" : bundleID + } else { + scheme = bundleID + } + #else + scheme = bundleID + #endif + guard let baseURL = URL(string: "\(scheme)://\(domain)") else { return nil } var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) + return components?.url? .appendingPathComponent(self.url.path) .appendingPathComponent(self.platform) - .appendingPathComponent(bundleIdentifier) + .appendingPathComponent(bundleID) .appendingPathComponent("callback") }() @@ -115,6 +127,11 @@ final class Auth0WebAuth: WebAuth { return self } + func useHTTPS() -> Self { + self.https = true + return self + } + func useEphemeralSession() -> Self { self.ephemeralSession = true return self @@ -141,7 +158,7 @@ final class Auth0WebAuth: WebAuth { } func start(_ callback: @escaping (WebAuthResult) -> Void) { - guard let redirectURL = self.redirectURL, let urlScheme = redirectURL.scheme else { + guard let redirectURL = self.redirectURL else { return callback(.failure(WebAuthError(code: .noBundleIdentifier))) } @@ -166,7 +183,7 @@ final class Auth0WebAuth: WebAuth { state: state, organization: organization, invitation: invitation) - let provider = self.provider ?? WebAuthentication.asProvider(urlScheme: urlScheme, + let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, ephemeralSession: ephemeralSession) let userAgent = provider(authorizeURL) { [storage, onCloseCallback] result in storage.clear() @@ -199,13 +216,11 @@ final class Auth0WebAuth: WebAuth { let queryItems = components?.queryItems ?? [] components?.queryItems = queryItems + [returnTo, clientId] - guard let logoutURL = components?.url, - let redirectURL = self.redirectURL, - let urlScheme = redirectURL.scheme else { + guard let logoutURL = components?.url, let redirectURL = self.redirectURL else { return callback(.failure(WebAuthError(code: .noBundleIdentifier))) } - let provider = self.provider ?? WebAuthentication.asProvider(urlScheme: urlScheme) + let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL) let userAgent = provider(logoutURL) { [storage] result in storage.clear() callback(result) diff --git a/Auth0/SafariProvider.swift b/Auth0/SafariProvider.swift index 9e6bd399..76616dd1 100644 --- a/Auth0/SafariProvider.swift +++ b/Auth0/SafariProvider.swift @@ -81,9 +81,9 @@ extension SFSafariViewController { class SafariUserAgent: NSObject, WebAuthUserAgent { let controller: SFSafariViewController - let callback: ((WebAuthResult) -> Void) + let callback: WebAuthProviderCallback - init(controller: SFSafariViewController, callback: @escaping (WebAuthResult) -> Void) { + init(controller: SFSafariViewController, callback: @escaping WebAuthProviderCallback) { self.controller = controller self.callback = callback super.init() diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index 5085b658..3fee5027 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -1,14 +1,19 @@ +// swiftlint:disable file_length + #if WEB_AUTH_PLATFORM import Foundation import Combine +/// Callback invoked by the ``WebAuthUserAgent`` when the web-based operation concludes. +public typealias WebAuthProviderCallback = (WebAuthResult) -> Void + /// Thunk that returns a function that creates and returns a ``WebAuthUserAgent`` to perform a web-based operation. /// The ``WebAuthUserAgent`` opens the URL in an external user agent and then invokes the callback when done. /// /// ## See Also /// /// - [Example](https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift) -public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping (WebAuthResult) -> Void) -> WebAuthUserAgent +public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping WebAuthProviderCallback) -> WebAuthUserAgent /// Web-based authentication using Auth0. /// @@ -127,6 +132,17 @@ public protocol WebAuth: Trackable, Loggable { /// - Returns: The same `WebAuth` instance to allow method chaining. func maxAge(_ maxAge: Int) -> Self + /// Use `https` as the scheme for the redirect URL on iOS 17.4+ and macOS 14.4+. On older versions of iOS and + /// macOS, the bundle identifier of the app will be used as a custom scheme. + /// + /// - Returns: The same `WebAuth` instance to allow method chaining. + /// - Requires: An Associated Domain configured with the `webcredentials` service type. For example, + /// `webcredentials:example.com`. If you're using a custom domain on your Auth0 tenant, use this domain as the + /// Associated Domain. Otherwise, use the domain of your Auth0 tenant. + /// - Note: Don't use this method along with ``provider(_:)``. Use either one or the other, because this + /// method will only work with the default `ASWebAuthenticationSession` implementation. + func useHTTPS() -> Self + /// Use a private browser session to avoid storing the session cookie in the shared cookie jar. /// Using this method will disable single sign-on (SSO). /// diff --git a/Auth0Tests/ASProviderSpec.swift b/Auth0Tests/ASProviderSpec.swift index 19d34270..acc3cda7 100644 --- a/Auth0Tests/ASProviderSpec.swift +++ b/Auth0Tests/ASProviderSpec.swift @@ -5,7 +5,9 @@ import Nimble @testable import Auth0 -private let Url = URL(string: "https://auth0.com")! +private let AuthorizeURL = URL(string: "https://auth0.com")! +private let HTTPSRedirectURL = URL(string: "https://auth0.com/callback")! +private let CustomSchemeRedirectURL = URL(string: "com.auth0.example://samples.auth0.com/callback")! private let Timeout: NimbleTimeInterval = .seconds(2) class ASProviderSpec: QuickSpec { @@ -16,7 +18,7 @@ class ASProviderSpec: QuickSpec { var userAgent: ASUserAgent! beforeEach { - session = ASWebAuthenticationSession(url: Url, callbackURLScheme: nil, completionHandler: { _, _ in }) + session = ASWebAuthenticationSession(url: AuthorizeURL, callbackURLScheme: nil, completionHandler: { _, _ in }) userAgent = ASUserAgent(session: session, callback: { _ in }) } @@ -27,20 +29,22 @@ class ASProviderSpec: QuickSpec { describe("WebAuthentication extension") { it("should create a web authentication session provider") { - let provider = WebAuthentication.asProvider(urlScheme: Url.scheme!) - expect(provider(Url, {_ in })).to(beAKindOf(ASUserAgent.self)) + let provider = WebAuthentication.asProvider(redirectURL: HTTPSRedirectURL) + expect(provider(AuthorizeURL, {_ in })).to(beAKindOf(ASUserAgent.self)) } it("should not use an ephemeral session by default") { - userAgent = WebAuthentication.asProvider(urlScheme: Url.scheme!)(Url, { _ in }) as? ASUserAgent + let provider = WebAuthentication.asProvider(redirectURL: CustomSchemeRedirectURL) + userAgent = provider(AuthorizeURL, { _ in }) as? ASUserAgent expect(userAgent.session.prefersEphemeralWebBrowserSession) == false } it("should use an ephemeral session") { - userAgent = WebAuthentication.asProvider(urlScheme: Url.scheme!, - ephemeralSession: true)(Url, { _ in }) as? ASUserAgent + let provider = WebAuthentication.asProvider(redirectURL: CustomSchemeRedirectURL, ephemeralSession: true) + userAgent = provider(AuthorizeURL, { _ in }) as? ASUserAgent expect(userAgent.session.prefersEphemeralWebBrowserSession) == true } + } describe("user agent") { diff --git a/Auth0Tests/SafariProviderSpec.swift b/Auth0Tests/SafariProviderSpec.swift index e8377bc3..b796b52b 100644 --- a/Auth0Tests/SafariProviderSpec.swift +++ b/Auth0Tests/SafariProviderSpec.swift @@ -7,10 +7,9 @@ import Nimble @testable import Auth0 -private let Url = URL(string: "https://auth0.com")! +private let RedirectURL = URL(string: "https://samples.auth0.com/callback")! private let Timeout: NimbleTimeInterval = .seconds(2) -@MainActor class SafariProviderSpec: QuickSpec { override func spec() { @@ -18,31 +17,34 @@ class SafariProviderSpec: QuickSpec { var safari: SFSafariViewController! var userAgent: SafariUserAgent! - beforeEach { - safari = SFSafariViewController(url: Url) + beforeEach { @MainActor in + safari = SFSafariViewController(url: RedirectURL) userAgent = SafariUserAgent(controller: safari, callback: { _ in }) } describe("WebAuthentication extension") { - it("should create a safari provider") { + it("should create a safari provider") { @MainActor in let provider = WebAuthentication.safariProvider() - expect(provider(Url, {_ in })).to(beAKindOf(SafariUserAgent.self)) + expect(provider(RedirectURL, { _ in })).to(beAKindOf(SafariUserAgent.self)) } - it("should use the fullscreen presentation style by default") { - let userAgent = WebAuthentication.safariProvider()(Url, {_ in }) as! SafariUserAgent + it("should use the fullscreen presentation style by default") { @MainActor in + let provider = WebAuthentication.safariProvider() + let userAgent = provider(RedirectURL, { _ in }) as! SafariUserAgent expect(userAgent.controller.modalPresentationStyle) == .fullScreen } - it("should set a custom presentation style") { + it("should set a custom presentation style") { @MainActor in let style = UIModalPresentationStyle.formSheet - let userAgent = WebAuthentication.safariProvider(style: style)(Url, {_ in }) as! SafariUserAgent + let provider = WebAuthentication.safariProvider(style: style) + let userAgent = provider(RedirectURL, { _ in }) as! SafariUserAgent expect(userAgent.controller.modalPresentationStyle) == style } - it("should use the cancel dismiss button style") { - let userAgent = WebAuthentication.safariProvider()(Url, {_ in }) as! SafariUserAgent + it("should use the cancel dismiss button style") { @MainActor in + let provider = WebAuthentication.safariProvider() + let userAgent = provider(RedirectURL, { _ in }) as! SafariUserAgent expect(userAgent.controller.dismissButtonStyle) == .cancel } @@ -52,33 +54,33 @@ class SafariProviderSpec: QuickSpec { var root: SpyViewController! - beforeEach { + beforeEach { @MainActor in root = SpyViewController() UIApplication.shared.windows.last(where: \.isKeyWindow)?.rootViewController = root } - it("should return nil when root is nil") { + it("should return nil when root is nil") { @MainActor in UIApplication.shared.windows.last(where: \.isKeyWindow)?.rootViewController = nil expect(safari.topViewController).to(beNil()) } - it("should return root when is top controller") { + it("should return root when is top controller") { @MainActor in expect(safari.topViewController) == root } - it("should return presented controller") { + it("should return presented controller") { @MainActor in let presented = UIViewController() root.presented = presented expect(safari.topViewController) == presented } - it("should return split view controller if contains nothing") { + it("should return split view controller if contains nothing") { @MainActor in let split = UISplitViewController() root.presented = split expect(safari.topViewController) == split } - it("should return last controller from split view controller") { + it("should return last controller from split view controller") { @MainActor in let split = UISplitViewController() let last = UIViewController() split.viewControllers = [UIViewController(), last] @@ -86,26 +88,26 @@ class SafariProviderSpec: QuickSpec { expect(safari.topViewController) == last } - it("should return navigation controller if contains nothing") { + it("should return navigation controller if contains nothing") { @MainActor in let navigation = UINavigationController() root.presented = navigation expect(safari.topViewController) == navigation } - it("should return top from navigation controller") { + it("should return top from navigation controller") { @MainActor in let top = UIViewController() let navigation = UINavigationController(rootViewController: top) root.presented = navigation expect(safari.topViewController) == top } - it("should return tab bar controller if contains nothing") { + it("should return tab bar controller if contains nothing") { @MainActor in let tabs = UITabBarController() root.presented = tabs expect(safari.topViewController) == tabs } - it("should return top from tab bar controller") { + it("should return top from tab bar controller") { @MainActor in let top = UIViewController() let tabs = UITabBarController() tabs.viewControllers = [top] @@ -116,23 +118,24 @@ class SafariProviderSpec: QuickSpec { describe("user agent") { - it("should have a custom description") { - let userAgent = SafariUserAgent(controller: SFSafariViewController(url: Url), callback: { _ in }) + it("should have a custom description") { @MainActor in + let safari = SFSafariViewController(url: RedirectURL) + let userAgent = SafariUserAgent(controller: safari, callback: { _ in }) expect(userAgent.description) == "SFSafariViewController" } - it("should be the safari view controller's delegate") { + it("should be the safari view controller's delegate") { @MainActor in expect(safari.delegate).to(be(userAgent)) } - it("should be the safari view controller's presentation delegate") { + it("should be the safari view controller's presentation delegate") { @MainActor in expect(safari.presentationController?.delegate).to(be(userAgent)) } - it("should present the safari view controller") { + it("should present the safari view controller") { @MainActor in let root = SpyViewController() UIApplication.shared.windows.last(where: \.isKeyWindow)?.rootViewController = root - let safari = SpySafariViewController(url: Url) + let safari = SpySafariViewController(url: RedirectURL) userAgent = SafariUserAgent(controller: safari, callback: { _ in }) root.presented = safari userAgent.start() @@ -141,8 +144,8 @@ class SafariProviderSpec: QuickSpec { it("should call the callback with an error when the user cancels the operation") { await waitUntil(timeout: Timeout) { done in - DispatchQueue.main.sync { - userAgent = SafariUserAgent(controller: safari, callback: { result in + DispatchQueue.main.async { [safari] in + let userAgent = SafariUserAgent(controller: safari!, callback: { result in expect(result).to(haveWebAuthError(WebAuthError(code: .userCancelled))) done() }) @@ -155,8 +158,8 @@ class SafariProviderSpec: QuickSpec { let expectedError = WebAuthError(code: .unknown("Cannot dismiss SFSafariViewController")) await waitUntil(timeout: Timeout) { done in - DispatchQueue.main.sync { - userAgent = SafariUserAgent(controller: safari, callback: { result in + DispatchQueue.main.async { [safari] in + let userAgent = SafariUserAgent(controller: safari!, callback: { result in expect(result).to(haveWebAuthError(expectedError)) done() }) @@ -165,7 +168,7 @@ class SafariProviderSpec: QuickSpec { } } - it("should call the callback with success") { + it("should call the callback with success") { @MainActor in let root = UIViewController() let window = UIWindow(frame: CGRect()) window.rootViewController = root @@ -173,8 +176,8 @@ class SafariProviderSpec: QuickSpec { root.present(safari, animated: false) await waitUntil(timeout: Timeout) { done in - DispatchQueue.main.sync { - userAgent = SafariUserAgent(controller: safari, callback: { result in + DispatchQueue.main.async { [safari] in + let userAgent = SafariUserAgent(controller: safari!, callback: { result in expect(result).to(beSuccessful()) done() }) @@ -183,19 +186,20 @@ class SafariProviderSpec: QuickSpec { } } - it("should cancel the transaction when the user cancels the operation") { + it("should cancel the transaction when the user cancels the operation") { @MainActor in let transaction = SpyTransaction() TransactionStore.shared.store(transaction) userAgent.safariViewControllerDidFinish(safari) expect(transaction.isCancelled) == true } - it("should cancel the transaction when the user dismisses the safari view controller") { + it("should cancel the transaction when the user dismisses the safari view controller") { @MainActor in let transaction = SpyTransaction() TransactionStore.shared.store(transaction) userAgent.presentationControllerDidDismiss(safari.presentationController!) expect(transaction.isCancelled) == true } + } } diff --git a/Auth0Tests/WebAuthSpec.swift b/Auth0Tests/WebAuthSpec.swift index 03785204..1b51dfc0 100644 --- a/Auth0Tests/WebAuthSpec.swift +++ b/Auth0Tests/WebAuthSpec.swift @@ -63,7 +63,6 @@ private func defaultQuery(withParameters parameters: [String: String] = [:]) -> private let defaults = ["response_type": "code"] -@MainActor class WebAuthSpec: QuickSpec { override func spec() { @@ -298,15 +297,40 @@ class WebAuthSpec: QuickSpec { } describe("redirect uri") { + let bundleId = Bundle.main.bundleIdentifier! + let platform: String + #if os(iOS) - let platform = "ios" + platform = "ios" #else - let platform = "macos" + platform = "macos" #endif - context("custom scheme") { + #if compiler(>=5.10) + if #available(iOS 17.4, macOS 14.4, *) { + context("https") { + it("should build with the domain") { + expect(newWebAuth().redirectURL?.absoluteString) == "https://\(Domain)/\(platform)/\(bundleId)/callback" + } + + it("should build with the domain and a subpath") { + let subpath = "foo" + let uri = "https://\(Domain)/\(subpath)/\(platform)/\(bundleId)/callback" + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL.appendingPathComponent(subpath)) + expect(webAuth.redirectURL?.absoluteString) == uri + } + + it("should build with the domain and subpaths") { + let subpaths = "foo/bar" + let uri = "https://\(Domain)/\(subpaths)/\(platform)/\(bundleId)/callback" + let webAuth = Auth0WebAuth(clientId: ClientId, url: DomainURL.appendingPathComponent(subpaths)) + expect(webAuth.redirectURL?.absoluteString) == uri + } + } + } + #endif - let bundleId = Bundle.main.bundleIdentifier! + context("custom scheme") { it("should build with the domain") { expect(newWebAuth().redirectURL?.absoluteString) == "\(bundleId)://\(Domain)/\(platform)/\(bundleId)/callback" @@ -336,6 +360,18 @@ class WebAuthSpec: QuickSpec { describe("other builder methods") { + context("https") { + + it("should not use https callbacks by default") { + expect(newWebAuth().https).to(beFalse()) + } + + it("should use https callbacks") { + expect(newWebAuth().useHTTPS().https).to(beTrue()) + } + + } + context("ephemeral session") { it("should not use ephemeral session by default") { @@ -413,7 +449,7 @@ class WebAuthSpec: QuickSpec { } it("should use a custom provider") { - expect(newWebAuth().provider(WebAuthentication.asProvider(urlScheme: "")).provider).toNot(beNil()) + expect(newWebAuth().provider(WebAuthentication.asProvider(redirectURL: RedirectURL)).provider).toNot(beNil()) } } @@ -452,11 +488,13 @@ class WebAuthSpec: QuickSpec { } it("should generate a state") { + _ = auth.provider({ url, _ in SpyUserAgent() }) auth.start { _ in } expect(auth.state).toNot(beNil()) } it("should generate different state on every start") { + _ = auth.provider({ url, _ in SpyUserAgent() }) auth.start { _ in } let state = auth.state auth.start { _ in } @@ -464,12 +502,14 @@ class WebAuthSpec: QuickSpec { } it("should use the supplied state") { + _ = auth.provider({ url, _ in SpyUserAgent() }) let state = UUID().uuidString auth.state(state).start { _ in } expect(auth.state) == state } it("should use the state supplied via parameters") { + _ = auth.provider({ url, _ in SpyUserAgent() }) let state = UUID().uuidString auth.parameters(["state": state]).start { _ in } expect(auth.state) == state @@ -536,13 +576,13 @@ class WebAuthSpec: QuickSpec { TransactionStore.shared.clear() } - it("should store a new transaction") { + it("should store a new transaction") { @MainActor in auth.start { _ in } expect(TransactionStore.shared.current).toNot(beNil()) TransactionStore.shared.cancel() } - it("should cancel the current transaction") { + it("should cancel the current transaction") { @MainActor in var result: WebAuthResult? auth.start { result = $0 } TransactionStore.shared.cancel() @@ -608,19 +648,19 @@ class WebAuthSpec: QuickSpec { TransactionStore.shared.clear() } - it("should store a new transaction") { + it("should store a new transaction") { @MainActor in auth.clearSession() { _ in } expect(TransactionStore.shared.current).toNot(beNil()) } - it("should cancel the current transaction") { + it("should cancel the current transaction") { @MainActor in auth.clearSession() { result = $0 } TransactionStore.shared.cancel() expect(result).to(haveWebAuthError(WebAuthError(code: .userCancelled))) expect(TransactionStore.shared.current).to(beNil()) } - it("should resume the current transaction") { + it("should resume the current transaction") { @MainActor in auth.clearSession() { result = $0 } _ = TransactionStore.shared.resume(URL(string: "http://fake.com")!) expect(result).to(beSuccessful())