diff --git a/App/AppDelegate.swift b/App/AppDelegate.swift index bba2d4a0..79145fc6 100644 --- a/App/AppDelegate.swift +++ b/App/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Auth0 -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? @@ -9,9 +9,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } - + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - return WebAuthentication.resume(with: url) + Task { + await WebAuthentication.resume(with: url) + } + return true } } diff --git a/App/ViewController.swift b/App/ViewController.swift index 859e71e8..a01f6e82 100644 --- a/App/ViewController.swift +++ b/App/ViewController.swift @@ -4,23 +4,25 @@ import Auth0 class ViewController: UIViewController { @IBAction func login(_ sender: Any) { - Auth0 - .webAuth() - .logging(enabled: true) - .start { - switch $0 { - case .failure(let error): - DispatchQueue.main.async { - self.alert(title: "Error", message: "\(error)") - } - case .success(let credentials): - DispatchQueue.main.async { - self.alert(title: "Success", - message: "Authorized and got a token \(credentials.accessToken)") + Task { + await Auth0 + .webAuth() + .logging(enabled: true) + .start { + switch $0 { + case .failure(let error): + DispatchQueue.main.async { + self.alert(title: "Error", message: "\(error)") + } + case .success(let credentials): + DispatchQueue.main.async { + self.alert(title: "Success", + message: "Authorized and got a token \(credentials.accessToken)") + } } + print($0) } - print($0) - } + } } @IBAction func logout(_ sender: Any) { diff --git a/Auth0.podspec b/Auth0.podspec index 47a88367..4514e5f3 100644 --- a/Auth0.podspec +++ b/Auth0.podspec @@ -12,7 +12,6 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/auth0' s.source_files = 'Auth0/**/*.swift' s.resource_bundles = { s.name => 'Auth0/PrivacyInfo.xcprivacy' } - s.swift_versions = ['5.0'] s.dependency 'SimpleKeychain', '1.3.0' s.dependency 'JWTDecode', '3.3.0' @@ -26,6 +25,8 @@ Pod::Spec.new do |s| s.osx.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'WEB_AUTH_PLATFORM PASSKEYS_PLATFORM' } + + s.swift_versions = ['6.0', '6.1'] s.tvos.deployment_target = '14.0' s.watchos.deployment_target = '7.0' diff --git a/Auth0/APICredentials.swift b/Auth0/APICredentials.swift index 72681269..8dd22a77 100644 --- a/Auth0/APICredentials.swift +++ b/Auth0/APICredentials.swift @@ -8,7 +8,7 @@ private struct _A0APICredentials { } /// User's credentials obtained from Auth0 for a specific API as the result of exchanging a refresh token. -public struct APICredentials: CustomStringConvertible { +public struct APICredentials: CustomStringConvertible, Sendable { /// Token that can be used to make authenticated requests to the API. /// diff --git a/Auth0/ASProvider.swift b/Auth0/ASProvider.swift index fd67df7f..e88e8322 100644 --- a/Auth0/ASProvider.swift +++ b/Auth0/ASProvider.swift @@ -8,76 +8,86 @@ extension WebAuthentication { static func asProvider(redirectURL: URL, ephemeralSession: Bool = false, headers: [String: String]? = nil) -> WebAuthProvider { - return { url, callback in - let session: ASWebAuthenticationSession - - if #available(iOS 17.4, macOS 14.4, visionOS 1.2, *) { - if redirectURL.scheme == "https" { - session = ASWebAuthenticationSession(url: url, - callback: .https(host: redirectURL.host!, - path: redirectURL.path), - completionHandler: completionHandler(callback)) + return { url, callback -> WebAuthUserAgent in + + return await Task { + let session: ASWebAuthenticationSession + + if #available(iOS 17.4, macOS 14.4, visionOS 1.2, *) { + 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)) + } + + session.additionalHeaderFields = headers } else { session = ASWebAuthenticationSession(url: url, - callback: .customScheme(redirectURL.scheme!), + callbackURLScheme: redirectURL.scheme, completionHandler: completionHandler(callback)) } - session.additionalHeaderFields = headers - } else { - session = ASWebAuthenticationSession(url: url, - callbackURLScheme: redirectURL.scheme, - completionHandler: completionHandler(callback)) - } - - session.prefersEphemeralWebBrowserSession = ephemeralSession + session.prefersEphemeralWebBrowserSession = ephemeralSession - return ASUserAgent(session: session, callback: callback) + return await ASUserAgent(session: session, callback: callback) + }.value + } } - 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))) + static let completionHandler: @Sendable (_ callback: @escaping WebAuthProviderCallback) -> ASHandler = { callback in + return { url,error in + Task { + guard let callbackURL = url, error == nil else { + if let error = error as? NSError, + error.userInfo.isEmpty, + case ASWebAuthenticationSessionError.canceledLogin = error { + return callback(.failure(WebAuthError(code: .userCancelled))) + } else if let error = error { + return callback(.failure(WebAuthError(code: .other, cause: error))) + } + + return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed")))) } - - return callback(.failure(WebAuthError(code: .unknown("ASWebAuthenticationSession failed")))) + + _ = await TransactionStore.shared.resume(callbackURL) } - - _ = TransactionStore.shared.resume(callbackURL) } } } -class ASUserAgent: NSObject, WebAuthUserAgent { +@MainActor final class ASUserAgent: NSObject, WebAuthUserAgent { private(set) static var currentSession: ASWebAuthenticationSession? let callback: WebAuthProviderCallback - init(session: ASWebAuthenticationSession, callback: @escaping WebAuthProviderCallback) { + init(session: ASWebAuthenticationSession, callback: @escaping @Sendable WebAuthProviderCallback) async { self.callback = callback super.init() session.presentationContextProvider = self + await initializeSession(session: session) + } + + func initializeSession(session: ASWebAuthenticationSession) async { ASUserAgent.currentSession = session } - func start() { + func start() async { _ = ASUserAgent.currentSession?.start() } - func finish(with result: WebAuthResult) { + func finish(with result: WebAuthResult) async { ASUserAgent.currentSession?.cancel() self.callback(result) } - public override var description: String { + public nonisolated override var description: String { return String(describing: ASWebAuthenticationSession.self) } diff --git a/Auth0/Auth0WebAuth.swift b/Auth0/Auth0WebAuth.swift index 2cd6cc56..9c424c79 100644 --- a/Auth0/Auth0WebAuth.swift +++ b/Auth0/Auth0WebAuth.swift @@ -2,7 +2,7 @@ import Foundation import Combine -final class Auth0WebAuth: WebAuth { +actor Auth0WebAuth: @preconcurrency WebAuth { let clientId: String let url: URL @@ -34,7 +34,7 @@ final class Auth0WebAuth: WebAuth { private(set) var invitationURL: URL? private(set) var overrideAuthorizeURL: URL? private(set) var provider: WebAuthProvider? - private(set) var onCloseCallback: (() -> Void)? + private(set) var onCloseCallback: (@Sendable () -> Void)? var state: String { return self.parameters["state"] ?? self.generateDefaultState() @@ -75,38 +75,38 @@ final class Auth0WebAuth: WebAuth { self.issuer = url.absoluteString } - func connection(_ connection: String) -> Self { + func connection(_ connection: String) async -> Self { self.parameters["connection"] = connection return self } - func scope(_ scope: String) -> Self { + func scope(_ scope: String) async -> Self { self.parameters["scope"] = scope return self } - func connectionScope(_ connectionScope: String) -> Self { + func connectionScope(_ connectionScope: String) async -> Self { self.parameters["connection_scope"] = connectionScope return self } - func state(_ state: String) -> Self { + func state(_ state: String) async -> Self { self.parameters["state"] = state return self } - func parameters(_ parameters: [String: String]) -> Self { + func parameters(_ parameters: [String: String]) async -> Self { parameters.forEach { self.parameters[$0] = $1 } return self } @available(iOS 17.4, macOS 14.4, visionOS 1.2, *) - func headers(_ headers: [String: String]) -> Self { + func headers(_ headers: [String: String]) async -> Self { headers.forEach { self.headers[$0] = $1 } return self } - func redirectURL(_ redirectURL: URL) -> Self { + func redirectURL(_ redirectURL: URL) async -> Self { self.redirectURL = redirectURL return self } @@ -116,119 +116,123 @@ final class Auth0WebAuth: WebAuth { return self } - func nonce(_ nonce: String) -> Self { + func nonce(_ nonce: String) async -> Self { self.nonce = nonce return self } - func audience(_ audience: String) -> Self { + func audience(_ audience: String) async -> Self { self.parameters["audience"] = audience return self } - func issuer(_ issuer: String) -> Self { + func issuer(_ issuer: String) async -> Self { self.issuer = issuer return self } - func leeway(_ leeway: Int) -> Self { + func leeway(_ leeway: Int) async -> Self { self.leeway = leeway return self } - func maxAge(_ maxAge: Int) -> Self { + func maxAge(_ maxAge: Int) async -> Self { self.maxAge = maxAge return self } - func useHTTPS() -> Self { + func useHTTPS() async -> Self { self.https = true return self } - func useEphemeralSession() -> Self { + func useEphemeralSession() async -> Self { self.ephemeralSession = true return self } - func invitationURL(_ invitationURL: URL) -> Self { + func invitationURL(_ invitationURL: URL) async -> Self { self.invitationURL = invitationURL return self } - func organization(_ organization: String) -> Self { + func organization(_ organization: String) async -> Self { self.organization = organization return self } - func provider(_ provider: @escaping WebAuthProvider) -> Self { + func provider(_ provider: @escaping WebAuthProvider) async -> Self { self.provider = provider return self } - func onClose(_ callback: (() -> Void)?) -> Self { + func onClose(_ callback: (@Sendable () -> Void)?) async -> Self { self.onCloseCallback = callback return self } - func start(_ callback: @escaping (WebAuthResult) -> Void) { - guard barrier.raise() else { - return callback(.failure(WebAuthError(code: .transactionActiveAlready))) - } - - guard let redirectURL = self.redirectURL else { - return callback(.failure(WebAuthError(code: .noBundleIdentifier))) - } - - let handler = self.handler(redirectURL) - let state = self.state - var organization: String? = self.organization - var invitation: String? - - if let invitationURL = self.invitationURL { - guard let queryItems = URLComponents(url: invitationURL, resolvingAgainstBaseURL: false)?.queryItems, - let organizationId = queryItems.first(where: { $0.name == "organization" })?.value, - let invitationId = queryItems.first(where: { $0.name == "invitation" })?.value else { - return callback(.failure(WebAuthError(code: .invalidInvitationURL(invitationURL.absoluteString)))) + func start(_ callback: @escaping @Sendable (WebAuthResult) -> Void) { + Task { + guard await barrier.raise() else { + return callback(.failure(WebAuthError(code: .transactionActiveAlready))) } - - organization = organizationId - invitation = invitationId - } - - let authorizeURL = self.buildAuthorizeURL(withRedirectURL: redirectURL, - defaults: handler.defaults, - state: state, - organization: organization, - invitation: invitation) - - let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, - ephemeralSession: ephemeralSession, - headers: headers) - let userAgent = provider(authorizeURL) { [storage, barrier, onCloseCallback] result in - storage.clear() - barrier.lower() - - switch result { - case .success: - onCloseCallback?() - case .failure(let error): - callback(.failure(error)) + + guard let redirectURL = self.redirectURL else { + return callback(.failure(WebAuthError(code: .noBundleIdentifier))) + } + + let handler = self.handler(redirectURL) + let state = self.state + var organization: String? = self.organization + var invitation: String? + + if let invitationURL = self.invitationURL { + guard let queryItems = URLComponents(url: invitationURL, resolvingAgainstBaseURL: false)?.queryItems, + let organizationId = queryItems.first(where: { $0.name == "organization" })?.value, + let invitationId = queryItems.first(where: { $0.name == "invitation" })?.value else { + return callback(.failure(WebAuthError(code: .invalidInvitationURL(invitationURL.absoluteString)))) + } + + organization = organizationId + invitation = invitationId + } + + let authorizeURL = self.buildAuthorizeURL(withRedirectURL: redirectURL, + defaults: handler.defaults, + state: state, + organization: organization, + invitation: invitation) + + let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, + ephemeralSession: ephemeralSession, + headers: headers) + let userAgent = await provider(authorizeURL) { [storage, barrier, onCloseCallback] result in + Task { + await storage.clear() + await barrier.lower() + + switch result { + case .success: + onCloseCallback?() + case .failure(let error): + callback(.failure(error)) + } + } } + let transaction = LoginTransaction(redirectURL: redirectURL, + state: state, + userAgent: userAgent, + handler: handler, + logger: self.logger, + callback: callback) + await self.storage.store(transaction) + await userAgent.start() + logger?.trace(url: authorizeURL, source: String(describing: userAgent.self)) } - let transaction = LoginTransaction(redirectURL: redirectURL, - state: state, - userAgent: userAgent, - handler: handler, - logger: self.logger, - callback: callback) - self.storage.store(transaction) - userAgent.start() - logger?.trace(url: authorizeURL, source: String(describing: userAgent.self)) } - func clearSession(federated: Bool, callback: @escaping (WebAuthResult) -> Void) { - guard barrier.raise() else { + func clearSession(federated: Bool, callback: @escaping @Sendable (WebAuthResult) -> Void) async { + guard await barrier.raise() else { return callback(.failure(WebAuthError(code: .transactionActiveAlready))) } @@ -246,14 +250,16 @@ final class Auth0WebAuth: WebAuth { } let provider = self.provider ?? WebAuthentication.asProvider(redirectURL: redirectURL, headers: headers) - let userAgent = provider(logoutURL) { [storage, barrier] result in - storage.clear() - barrier.lower() - callback(result) + let userAgent = await provider(logoutURL) { [storage, barrier] result in + Task { + await storage.clear() + await barrier.lower() + callback(result) + } } let transaction = ClearSessionTransaction(userAgent: userAgent) - self.storage.store(transaction) - userAgent.start() + await self.storage.store(transaction) + await userAgent.start() } func buildAuthorizeURL(withRedirectURL redirectURL: URL, @@ -322,12 +328,25 @@ final class Auth0WebAuth: WebAuth { // MARK: - Combine extension Auth0WebAuth { - - public func start() -> AnyPublisher { - return Deferred { Future(self.start) }.eraseToAnyPublisher() + nonisolated public func start() -> AnyPublisher { + return Deferred { + Future { promise in + let wrapper = FutureResultWrapper(promise) + Task { + await self.start { result in + switch result { + case .success(let credentials): + wrapper.completionResult(.success(credentials)) + case .failure(let error): + wrapper.completionResult(.failure(error)) + } + } + } + } + }.eraseToAnyPublisher() } - public func clearSession(federated: Bool) -> AnyPublisher { + nonisolated public func clearSession(federated: Bool) -> AnyPublisher { return Deferred { Future { callback in self.clearSession(federated: federated) { result in @@ -346,8 +365,8 @@ extension Auth0WebAuth { func start() async throws -> Credentials { return try await withCheckedThrowingContinuation { continuation in - Task { @MainActor in - self.start { result in + Task { + await self.start { result in continuation.resume(with: result) } } @@ -356,8 +375,8 @@ extension Auth0WebAuth { func clearSession(federated: Bool) async throws { return try await withCheckedThrowingContinuation { continuation in - Task { @MainActor in - self.clearSession(federated: federated) { result in + Task { + await self.clearSession(federated: federated) { result in continuation.resume(with: result) } } @@ -367,3 +386,30 @@ extension Auth0WebAuth { } #endif #endif + +fileprivate final class FutureResultWrapper: @unchecked Sendable { + fileprivate typealias Promise = (Result) -> Void + + fileprivate let completionResult: Promise + + /// Creates a publisher that invokes a promise closure when the publisher emits an element. + /// + /// - Parameter attemptToFulfill: A ``Future/Promise`` that the publisher invokes when the publisher emits an element or terminates with an error. + fileprivate init(_ attemptToFulfill: @escaping Promise) { + self.completionResult = attemptToFulfill + } +} +//and then use it like this: +// +// let publisher = Future { [weak self] completionResult in +// guard let self = self else { +// completionResult(.success(object)) +// return +// } +// +// let wrapper = FutureResultWrapper(completionResult) +// +// Task.detached { [weak self] in +// await self?.persist(object) +// wrapper.completionResult(.success(object)) +// } diff --git a/Auth0/AuthTransaction.swift b/Auth0/AuthTransaction.swift index acbf0c91..99f9ce80 100644 --- a/Auth0/AuthTransaction.swift +++ b/Auth0/AuthTransaction.swift @@ -11,7 +11,7 @@ OS can open it on success/failure. - Important: Only one ``AuthTransaction`` can be active at a given time for Auth0.swift, if you start a new one before finishing the current one it will be cancelled. */ -protocol AuthTransaction { +protocol AuthTransaction: Sendable { /** Resumes the transaction when the third party application notifies the application using a URL with a custom @@ -20,12 +20,12 @@ protocol AuthTransaction { - Parameter url: The URL sent by the third party application that contains the result of the auth. - Returns: If the URL was expected and properly formatted. Otherwise, it will return `false`. */ - func resume(_ url: URL) -> Bool + func resume(_ url: URL) async -> Bool /** Terminates the operation and reports back that it was cancelled. */ - func cancel() + func cancel() async } #endif diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index ce712e24..d8e9dd37 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -13,7 +13,7 @@ public typealias DatabaseUser = (email: String, username: String?, verified: Boo - ``AuthenticationError`` */ -public protocol Authentication: Trackable, Loggable { +public protocol Authentication: Trackable, Loggable, Sendable { /// The Auth0 Client ID. var clientId: String { get } diff --git a/Auth0/Barrier.swift b/Auth0/Barrier.swift index 276dc394..07c888b3 100644 --- a/Auth0/Barrier.swift +++ b/Auth0/Barrier.swift @@ -1,11 +1,11 @@ import Foundation -protocol Barrier: AnyObject { - func raise() -> Bool - func lower() +protocol Barrier: Sendable { + func raise() async -> Bool + func lower() async } -final class QueueBarrier: Barrier { +actor QueueBarrier: Barrier { static let shared = QueueBarrier() private let queue = DispatchQueue(label: "com.auth0.webauth.barrier.serial") @@ -13,17 +13,17 @@ final class QueueBarrier: Barrier { private init() {} - func raise() -> Bool { - self.queue.sync { + func raise() async -> Bool { +// self.queue.sync { guard !self.isRaised else { return false } self.isRaised = true return self.isRaised - } +// } } - func lower() { - self.queue.sync { + func lower() async { +// self.queue.sync { self.isRaised = false - } +// } } } diff --git a/Auth0/BioAuthentication.swift b/Auth0/BioAuthentication.swift index 20d3adcb..c8198431 100644 --- a/Auth0/BioAuthentication.swift +++ b/Auth0/BioAuthentication.swift @@ -1,8 +1,8 @@ #if WEB_AUTH_PLATFORM import Foundation -import LocalAuthentication +@preconcurrency import LocalAuthentication -struct BioAuthentication { +struct BioAuthentication: Sendable { private let authContext: LAContext private let evaluationPolicy: LAPolicy @@ -30,7 +30,7 @@ struct BioAuthentication { self.fallbackTitle = fallbackTitle } - func validateBiometric(callback: @escaping (Error?) -> Void) { + func validateBiometric(callback: @escaping @Sendable (Error?) -> Void) { self.authContext.evaluatePolicy(evaluationPolicy, localizedReason: self.title) { guard $1 == nil else { return callback($1) } callback($0 ? nil : LAError(.authenticationFailed)) diff --git a/Auth0/ClearSessionTransaction.swift b/Auth0/ClearSessionTransaction.swift index e34b09b6..d64e421e 100644 --- a/Auth0/ClearSessionTransaction.swift +++ b/Auth0/ClearSessionTransaction.swift @@ -1,7 +1,7 @@ #if WEB_AUTH_PLATFORM import Foundation -class ClearSessionTransaction: NSObject, AuthTransaction { +actor ClearSessionTransaction: NSObject, AuthTransaction { private(set) var userAgent: WebAuthUserAgent? @@ -10,20 +10,20 @@ class ClearSessionTransaction: NSObject, AuthTransaction { super.init() } - func cancel() { + func cancel() async { // The user agent can handle the error - self.finishUserAgent(with: .failure(WebAuthError(code: .userCancelled))) + await self.finishUserAgent(with: .failure(WebAuthError(code: .userCancelled))) } - func resume(_ url: URL) -> Bool { + func resume(_ url: URL) async -> Bool { // The user agent can close itself - self.finishUserAgent(with: .success(())) + await self.finishUserAgent(with: .success(())) return true } - private func finishUserAgent(with result: WebAuthResult) { - self.userAgent?.finish(with: result) - self.userAgent = nil + private func finishUserAgent(with result: WebAuthResult) async { + await userAgent?.finish(with: result) + userAgent = nil } } diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 3f024ecb..e1dbf63c 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -24,7 +24,7 @@ import LocalAuthentication /// /// - ``CredentialsManagerError`` /// - -public struct CredentialsManager { +public struct CredentialsManager: Sendable { private let storage: CredentialsStorage private let storeKey: String @@ -304,9 +304,9 @@ public struct CredentialsManager { /// - public func credentials(withScope scope: String? = nil, minTTL: Int = 0, - parameters: [String: Any] = [:], + parameters: [String: any Sendable] = [:], headers: [String: String] = [:], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { if let bioAuth = self.bioAuth { guard bioAuth.available else { let error = CredentialsManagerError(code: .biometricsFailed, @@ -476,7 +476,7 @@ public struct CredentialsManager { minTTL: Int = 0, parameters: [String: Any] = [:], headers: [String: String] = [:], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { self.retrieveAPICredentials(audience: audience, scope: scope, minTTL: minTTL, @@ -556,7 +556,7 @@ public struct CredentialsManager { /// - public func ssoCredentials(parameters: [String: Any] = [:], headers: [String: String] = [:], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { self.retrieveSSOCredentials(parameters: parameters, headers: headers, callback: callback) } @@ -601,7 +601,7 @@ public struct CredentialsManager { /// - public func renew(parameters: [String: Any] = [:], headers: [String: String] = [:], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { self.retrieveCredentials(scope: nil, minTTL: 0, parameters: parameters, @@ -634,7 +634,7 @@ public struct CredentialsManager { parameters: [String: Any], headers: [String: String], forceRenewal: Bool, - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { let dispatchGroup = DispatchGroup() self.dispatchQueue.async { @@ -691,7 +691,7 @@ public struct CredentialsManager { private func retrieveSSOCredentials(parameters: [String: Any], headers: [String: String], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { let dispatchGroup = DispatchGroup() self.dispatchQueue.async { @@ -741,7 +741,7 @@ public struct CredentialsManager { minTTL: Int, parameters: [String: Any], headers: [String: String], - callback: @escaping (CredentialsManagerResult) -> Void) { + callback: @escaping @Sendable (CredentialsManagerResult) -> Void) { let dispatchGroup = DispatchGroup() self.dispatchQueue.async { @@ -945,20 +945,20 @@ public extension CredentialsManager { /// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens) /// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication/refresh-token/refresh-token) /// - - func credentials(withScope scope: String? = nil, - minTTL: Int = 0, - parameters: [String: Any] = [:], - headers: [String: String] = [:]) -> AnyPublisher { - return Deferred { - Future { callback in - return self.credentials(withScope: scope, - minTTL: minTTL, - parameters: parameters, - headers: headers, - callback: callback) - } - }.eraseToAnyPublisher() - } +// func credentials(withScope scope: String? = nil, +// minTTL: Int = 0, +// parameters: [String: Any] = [:], +// headers: [String: String] = [:]) -> AnyPublisher { +// return Deferred { +// Future { callback in +// return self.credentials(withScope: scope, +// minTTL: minTTL, +// parameters: parameters, +// headers: headers, +// callback: callback) +// } +// }.eraseToAnyPublisher() +// } /// Retrieves API credentials from the Keychain and automatically renews them using the refresh token if the access /// token is expired. Otherwise, the subscription will complete with the retrieved API credentials as they are @@ -1036,22 +1036,22 @@ public extension CredentialsManager { /// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens) /// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication/refresh-token/refresh-token) /// - - func apiCredentials(forAudience audience: String, - scope: String? = nil, - minTTL: Int = 0, - parameters: [String: Any] = [:], - headers: [String: String] = [:]) -> AnyPublisher { - return Deferred { - Future { callback in - return self.apiCredentials(forAudience: audience, - scope: scope, - minTTL: minTTL, - parameters: parameters, - headers: headers, - callback: callback) - } - }.eraseToAnyPublisher() - } +// func apiCredentials(forAudience audience: String, +// scope: String? = nil, +// minTTL: Int = 0, +// parameters: [String: Any] = [:], +// headers: [String: String] = [:]) -> AnyPublisher { +// return Deferred { +// Future { callback in +// return self.apiCredentials(forAudience: audience, +// scope: scope, +// minTTL: minTTL, +// parameters: parameters, +// headers: headers, +// callback: callback) +// } +// }.eraseToAnyPublisher() +// } /// Exchanges the refresh token for a session transfer token that can be used to perform web single sign-on (SSO). /// **This method is thread-safe**. @@ -1127,14 +1127,14 @@ public extension CredentialsManager { /// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens) /// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication#refresh-token) /// - - func ssoCredentials(parameters: [String: Any] = [:], - headers: [String: String] = [:]) -> AnyPublisher { - return Deferred { - Future { callback in - return self.ssoCredentials(parameters: parameters, headers: headers, callback: callback) - } - }.eraseToAnyPublisher() - } +// func ssoCredentials(parameters: [String: Any] = [:], +// headers: [String: String] = [:]) -> AnyPublisher { +// return Deferred { +// Future { callback in +// return self.ssoCredentials(parameters: parameters, headers: headers, callback: callback) +// } +// }.eraseToAnyPublisher() +// } /// Renews credentials using the refresh token and stores them in the Keychain. **This method is thread-safe**. /// @@ -1181,16 +1181,16 @@ public extension CredentialsManager { /// - [Refresh Tokens](https://auth0.com/docs/secure/tokens/refresh-tokens) /// - [Authentication API Endpoint](https://auth0.com/docs/api/authentication/refresh-token/refresh-token) /// - - func renew(parameters: [String: Any] = [:], - headers: [String: String] = [:]) -> AnyPublisher { - return Deferred { - Future { callback in - return self.renew(parameters: parameters, - headers: headers, - callback: callback) - } - }.eraseToAnyPublisher() - } +// func renew(parameters: [String: Any] = [:], +// headers: [String: String] = [:]) -> AnyPublisher { +// return Deferred { +// Future { callback in +// return self.renew(parameters: parameters, +// headers: headers, +// callback: callback) +// } +// }.eraseToAnyPublisher() +// } } @@ -1295,7 +1295,7 @@ public extension CredentialsManager { /// - func credentials(withScope scope: String? = nil, minTTL: Int = 0, - parameters: [String: Any] = [:], + parameters: [String: any Sendable] = [:], headers: [String: String] = [:]) async throws -> Credentials { return try await withCheckedThrowingContinuation { continuation in self.credentials(withScope: scope, diff --git a/Auth0/CredentialsStorage.swift b/Auth0/CredentialsStorage.swift index abc22501..2762f980 100644 --- a/Auth0/CredentialsStorage.swift +++ b/Auth0/CredentialsStorage.swift @@ -2,7 +2,7 @@ import SimpleKeychain import Foundation /// Generic storage API for storing credentials. -public protocol CredentialsStorage { +public protocol CredentialsStorage: Sendable { /// Retrieves a storage entry. /// diff --git a/Auth0/IDTokenSignatureValidator.swift b/Auth0/IDTokenSignatureValidator.swift index 449e5d08..44c61ecf 100644 --- a/Auth0/IDTokenSignatureValidator.swift +++ b/Auth0/IDTokenSignatureValidator.swift @@ -2,7 +2,7 @@ import Foundation import JWTDecode -protocol IDTokenSignatureValidatorContext { +protocol IDTokenSignatureValidatorContext: Sendable { var issuer: String { get } var audience: String { get } var jwksRequest: Request { get } diff --git a/Auth0/IDTokenValidator.swift b/Auth0/IDTokenValidator.swift index b5ae54fe..9fecccbe 100644 --- a/Auth0/IDTokenValidator.swift +++ b/Auth0/IDTokenValidator.swift @@ -1,13 +1,13 @@ #if WEB_AUTH_PLATFORM import Foundation -import JWTDecode +@preconcurrency import JWTDecode -protocol JWTValidator { +protocol JWTValidator: Sendable { func validate(_ jwt: JWT) -> Auth0Error? } -protocol JWTAsyncValidator { - func validate(_ jwt: JWT, callback: @escaping (Auth0Error?) -> Void) +protocol JWTAsyncValidator: Sendable { + func validate(_ jwt: JWT, callback: @escaping @Sendable (Auth0Error?) -> Void) } struct IDTokenValidator: JWTAsyncValidator { @@ -23,7 +23,7 @@ struct IDTokenValidator: JWTAsyncValidator { self.context = context } - func validate(_ jwt: JWT, callback: @escaping (Auth0Error?) -> Void) { + func validate(_ jwt: JWT, callback: @escaping @Sendable (Auth0Error?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { self.signatureValidator.validate(jwt) { error in if let error = error { return callback(error) } @@ -50,7 +50,7 @@ func validate(idToken: String, with context: IDTokenValidatorContext, signatureValidator: JWTAsyncValidator? = nil, // for testing claimsValidator: JWTValidator? = nil, - callback: @escaping (Auth0Error?) -> Void) { + callback: @escaping @Sendable (Auth0Error?) -> Void) { guard let jwt = try? decode(jwt: idToken) else { return callback(IDTokenDecodingError.cannotDecode) } var claimValidators: [JWTValidator] = [IDTokenIssValidator(issuer: context.issuer), IDTokenSubValidator(), diff --git a/Auth0/IDTokenValidatorContext.swift b/Auth0/IDTokenValidatorContext.swift index 91c03a74..c064e67f 100644 --- a/Auth0/IDTokenValidatorContext.swift +++ b/Auth0/IDTokenValidatorContext.swift @@ -1,7 +1,7 @@ #if WEB_AUTH_PLATFORM import Foundation -struct IDTokenValidatorContext: IDTokenSignatureValidatorContext, IDTokenClaimsValidatorContext { +struct IDTokenValidatorContext: IDTokenSignatureValidatorContext, IDTokenClaimsValidatorContext, Sendable { let issuer: String let audience: String let jwksRequest: Request diff --git a/Auth0/Loggable.swift b/Auth0/Loggable.swift index acaf0cdf..77fd9a19 100644 --- a/Auth0/Loggable.swift +++ b/Auth0/Loggable.swift @@ -1,7 +1,7 @@ import Foundation /// A type that can log statements for debugging purposes. -public protocol Loggable { +public protocol Loggable: Sendable { /// Logger used to print log statements. var logger: Logger? { get set } diff --git a/Auth0/Logger.swift b/Auth0/Logger.swift index 08322629..5b4d500c 100644 --- a/Auth0/Logger.swift +++ b/Auth0/Logger.swift @@ -1,7 +1,7 @@ import Foundation /// Logger for debugging purposes. -public protocol Logger { +public protocol Logger: Sendable { /// Log an HTTP request. func trace(request: URLRequest, session: URLSession) @@ -14,7 +14,7 @@ public protocol Logger { } -protocol LoggerOutput { +protocol LoggerOutput: Sendable { func log(message: String) func newLine() } diff --git a/Auth0/LoginTransaction.swift b/Auth0/LoginTransaction.swift index ed29c586..8e97e565 100644 --- a/Auth0/LoginTransaction.swift +++ b/Auth0/LoginTransaction.swift @@ -1,9 +1,9 @@ #if WEB_AUTH_PLATFORM import Foundation -class LoginTransaction: NSObject, AuthTransaction { +actor LoginTransaction: NSObject, AuthTransaction { - typealias FinishTransaction = (WebAuthResult) -> Void + typealias FinishTransaction = @Sendable (WebAuthResult) -> Void private(set) var userAgent: WebAuthUserAgent? @@ -28,32 +28,32 @@ class LoginTransaction: NSObject, AuthTransaction { super.init() } - func cancel() { - self.finishUserAgent(with: .failure(WebAuthError(code: .userCancelled))) + func cancel() async { + await self.finishUserAgent(with: .failure(WebAuthError(code: .userCancelled))) } - func resume(_ url: URL) -> Bool { + func resume(_ url: URL) async -> Bool { self.logger?.trace(url: url, source: "Callback URL") - return self.handleURL(url) + return await self.handleURL(url) } - private func handleURL(_ url: URL) -> Bool { + private func handleURL(_ url: URL) async -> Bool { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), case let items = self.handler.values(fromComponents: components), has(state: self.state, inItems: items) else { let error = WebAuthError(code: .unknown("Invalid callback URL: \(url.absoluteString)")) // The user agent can handle the error - self.finishUserAgent(with: .failure(error)) + await self.finishUserAgent(with: .failure(error)) return false } if items["error"] != nil { let error = WebAuthError(code: .other, cause: AuthenticationError(info: items)) // The user agent can handle the error - self.finishUserAgent(with: .failure(error)) + await self.finishUserAgent(with: .failure(error)) } else { // The user agent can close itself - self.finishUserAgent(with: .success(())) + await self.finishUserAgent(with: .success(())) // Continue with code exchange self.handler.credentials(from: items, callback: self.callback) } @@ -61,9 +61,9 @@ class LoginTransaction: NSObject, AuthTransaction { return true } - private func finishUserAgent(with result: WebAuthResult) { - self.userAgent?.finish(with: result) - self.userAgent = nil + private func finishUserAgent(with result: WebAuthResult) async { + await userAgent?.finish(with: result) + userAgent = nil } private func has(state: String?, inItems items: [String: String]) -> Bool { diff --git a/Auth0/MobileWebAuth.swift b/Auth0/MobileWebAuth.swift index d6f347f5..64682741 100644 --- a/Auth0/MobileWebAuth.swift +++ b/Auth0/MobileWebAuth.swift @@ -13,13 +13,13 @@ extension UIApplication { extension ASUserAgent: ASWebAuthenticationPresentationContextProviding { #if os(iOS) - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return UIApplication.shared()?.windows.last(where: \.isKeyWindow) ?? ASPresentationAnchor() } #endif #if os(visionOS) - func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + nonisolated func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { if let windowScene = UIApplication.shared()?.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { return windowScene.windows.last(where: \.isKeyWindow) ?? ASPresentationAnchor() } diff --git a/Auth0/OAuth2Grant.swift b/Auth0/OAuth2Grant.swift index d878ebbd..eb95c328 100644 --- a/Auth0/OAuth2Grant.swift +++ b/Auth0/OAuth2Grant.swift @@ -1,9 +1,9 @@ #if WEB_AUTH_PLATFORM import Foundation -protocol OAuth2Grant { +protocol OAuth2Grant: Sendable { var defaults: [String: String] { get } - func credentials(from values: [String: String], callback: @escaping (WebAuthResult) -> Void) + func credentials(from values: [String: String], callback: @escaping @Sendable (WebAuthResult) -> Void) func values(fromComponents components: URLComponents) -> [String: String] } @@ -63,7 +63,7 @@ struct PKCE: OAuth2Grant { self.defaults = newDefaults } - func credentials(from values: [String: String], callback: @escaping (WebAuthResult) -> Void) { + func credentials(from values: [String: String], callback: @escaping @Sendable (WebAuthResult) -> Void) { guard let code = values["code"] else { return callback(.failure(WebAuthError(code: .noAuthorizationCode(values)))) } diff --git a/Auth0/ParameterValue.swift b/Auth0/ParameterValue.swift new file mode 100644 index 00000000..7771e0ff --- /dev/null +++ b/Auth0/ParameterValue.swift @@ -0,0 +1,6 @@ +public enum ParameterValue: Sendable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) +} diff --git a/Auth0/Request.swift b/Auth0/Request.swift index f790618c..3604c31c 100644 --- a/Auth0/Request.swift +++ b/Auth0/Request.swift @@ -18,7 +18,7 @@ let parameterPropertyKey = "com.auth0.parameter" } ``` */ -public struct Request: Requestable { +public struct Request: Requestable, @unchecked Sendable { /** The callback closure type for the request. */ @@ -27,13 +27,13 @@ public struct Request: Requestable { let session: URLSession let url: URL let method: String - let handle: (Response, Callback) -> Void + let handle: @Sendable (Response, Callback) -> Void let parameters: [String: Any] let headers: [String: String] let logger: Logger? let telemetry: Telemetry - init(session: URLSession, url: URL, method: String, handle: @escaping (Response, Callback) -> Void, parameters: [String: Any] = [:], headers: [String: String] = [:], logger: Logger?, telemetry: Telemetry) { + init(session: URLSession, url: URL, method: String, handle: @escaping @Sendable (Response, Callback) -> Void, parameters: [String: Any] = [:], headers: [String: String] = [:], logger: Logger?, telemetry: Telemetry) { self.session = session self.url = url self.method = method @@ -73,7 +73,7 @@ public struct Request: Requestable { - Parameter callback: Callback that receives the result of the request when it completes. */ - public func start(_ callback: @escaping Callback) { + public func start(_ callback: @escaping @Sendable Callback) { let handler = self.handle let request = self.request let logger = self.logger @@ -132,20 +132,20 @@ public extension Request { // MARK: - Async/Await #if canImport(_Concurrency) -public extension Request { - - /** - Performs the request. - - - Throws: An error that conforms to ``Auth0APIError``; either an ``AuthenticationError`` or a ``ManagementError``. - */ - func start() async throws -> T { - return try await withCheckedThrowingContinuation { continuation in - self.start { result in - continuation.resume(with: result) - } - } - } - -} +//public extension Request { +// +// /** +// Performs the request. +// +// - Throws: An error that conforms to ``Auth0APIError``; either an ``AuthenticationError`` or a ``ManagementError``. +// */ +// func start() async throws -> T { +// return try await withCheckedThrowingContinuation { continuation in +// self.start { result in +// continuation.resume(with: result) +// } +// } +// } +// +//} #endif diff --git a/Auth0/Response.swift b/Auth0/Response.swift index 06c7c933..139fc195 100644 --- a/Auth0/Response.swift +++ b/Auth0/Response.swift @@ -1,8 +1,33 @@ import Foundation -func json(_ data: Data?) -> Any? { +func json(_ data: Data?) -> Sendable? { guard let data = data else { return nil } - return try? JSONSerialization.jsonObject(with: data, options: []) + do { + let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) + + if let dict = jsonObject as? [String: Any] { + let sendableDict: [String: Sendable] = dict.compactMapValues { value in + if let s = value as? String { return s } + if let i = value as? Int { return i } + if let d = value as? Double { return d } + if let b = value as? Bool { return b } + return nil + } + return sendableDict + } else if let array = jsonObject as? [Any] { + let sendableArray: [Sendable] = array.compactMap { value in + if let s = value as? String { return s } + if let i = value as? Int { return i } + if let d = value as? Double { return d } + if let b = value as? Bool { return b } + return nil + } + return sendableArray + } + return nil + } catch { + return nil + } } func string(_ data: Data?) -> String? { @@ -10,9 +35,9 @@ func string(_ data: Data?) -> String? { return String(data: data, encoding: .utf8) } -typealias JSONResponse = (headers: [String: Any], body: Any, data: Data) +typealias JSONResponse = (headers: [String: any Sendable], body: any Sendable, data: Data) -struct Response { +struct Response: Sendable { let data: Data? let response: HTTPURLResponse? let error: Error? @@ -44,13 +69,28 @@ struct Response { } } -private extension Dictionary where Key == AnyHashable, Value == Any { +extension Dictionary where Key == AnyHashable, Value == Any { - var stringDictionary: [String: Any] { - var result: [String: Any] = [:] + var stringDictionary: [String: any Sendable] { + var result: [String: any Sendable] = [:] for (key, value) in self { - if let stringKey = key as? String { - result[stringKey] = value + guard let stringKey = key as? String else { + continue + } + if let str = value as? String { + result[stringKey] = str + } else if let num = value as? Int { + result[stringKey] = num + } else if let boolean = value as? Bool { + result[stringKey] = boolean + } else if let double = value as? Double { + result[stringKey] = double + } else if let array = value as? [any Sendable] { + result[stringKey] = array + } else if let dict = value as? [String: any Sendable] { + result[stringKey] = dict + } else { + continue } } return result diff --git a/Auth0/SSOCredentials.swift b/Auth0/SSOCredentials.swift index 2741d340..6580a1c4 100644 --- a/Auth0/SSOCredentials.swift +++ b/Auth0/SSOCredentials.swift @@ -9,7 +9,7 @@ private struct _A0SSOCredentials { } /// Credentials obtained from Auth0 to perform web single sign-on (SSO). -public struct SSOCredentials: CustomStringConvertible { +public struct SSOCredentials: CustomStringConvertible, Sendable { /// Token that can be used to request a web session. public let sessionTransferToken: String diff --git a/Auth0/SafariProvider.swift b/Auth0/SafariProvider.swift index 45767d11..d8356002 100644 --- a/Auth0/SafariProvider.swift +++ b/Auth0/SafariProvider.swift @@ -42,17 +42,29 @@ public extension WebAuthentication { /// /// - static func safariProvider(style: UIModalPresentationStyle = .fullScreen) -> WebAuthProvider { - return { url, callback in - let safari = SFSafariViewController(url: url) - safari.dismissButtonStyle = .cancel - safari.modalPresentationStyle = style - return SafariUserAgent(controller: safari, callback: callback) + return { url, callback -> WebAuthUserAgent in + return await Task { + let safari = await SFSafariViewController(url: url) + await safari.setDismissButtonStyle() + await safari.setModelPresentationStyle(style: style) + return await SafariUserAgent(controller: safari, callback: callback) + }.value } } } -class SafariUserAgent: NSObject, WebAuthUserAgent { +extension SFSafariViewController { + func setDismissButtonStyle(style: DismissButtonStyle = .cancel) async { + dismissButtonStyle = style + } + + func setModelPresentationStyle(style: UIModalPresentationStyle = .fullScreen) async { + modalPresentationStyle = style + } +} + +@MainActor final class SafariUserAgent: NSObject, WebAuthUserAgent { let controller: SFSafariViewController let callback: WebAuthProviderCallback @@ -65,11 +77,11 @@ class SafariUserAgent: NSObject, WebAuthUserAgent { self.controller.presentationController?.delegate = self } - func start() { + func start() async { UIWindow.topViewController?.present(controller, animated: true, completion: nil) } - func finish(with result: WebAuthResult) { + func finish(with result: WebAuthResult) async { if case .failure(let cause) = result, case .userCancelled = cause { DispatchQueue.main.async { [callback] in callback(result) @@ -95,20 +107,24 @@ class SafariUserAgent: NSObject, WebAuthUserAgent { extension SafariUserAgent: SFSafariViewControllerDelegate { - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + nonisolated func safariViewControllerDidFinish(_ controller: SFSafariViewController) { // If you are developing a custom Web Auth provider, call WebAuthentication.cancel() instead // TransactionStore is internal - TransactionStore.shared.cancel() + Task { + await TransactionStore.shared.cancel() + } } - + } extension SafariUserAgent: UIAdaptivePresentationControllerDelegate { - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { // If you are developing a custom Web Auth provider, call WebAuthentication.cancel() instead - // TransactionStore is internal - TransactionStore.shared.cancel() + Task { + // TransactionStore is internal + await TransactionStore.shared.cancel() + } } } diff --git a/Auth0/Telemetry.swift b/Auth0/Telemetry.swift index 4d62618b..26602245 100644 --- a/Auth0/Telemetry.swift +++ b/Auth0/Telemetry.swift @@ -1,7 +1,7 @@ import Foundation /// Generates and sets the `Auth0-Client` header. -public struct Telemetry { +public struct Telemetry: Sendable { static let NameKey = "name" static let VersionKey = "version" @@ -98,7 +98,7 @@ public struct Telemetry { } /// A type that can send the `Auth0-Client` header on every request to Auth0. -public protocol Trackable { +public protocol Trackable: Sendable { /// The ``Telemetry`` instance. var telemetry: Telemetry { get set } diff --git a/Auth0/TransactionStore.swift b/Auth0/TransactionStore.swift index 260cae8b..1edc1657 100644 --- a/Auth0/TransactionStore.swift +++ b/Auth0/TransactionStore.swift @@ -2,28 +2,28 @@ import Foundation /// Keeps track of the current Auth Transaction. -class TransactionStore { +actor TransactionStore { static let shared = TransactionStore() private(set) var current: AuthTransaction? - func resume(_ url: URL) -> Bool { - let isResumed = self.current?.resume(url) ?? false - self.clear() + func resume(_ url: URL) async -> Bool { + let isResumed = await self.current?.resume(url) ?? false + await self.clear() return isResumed } - func store(_ transaction: AuthTransaction) { + func store(_ transaction: AuthTransaction) async { self.current = transaction } - func cancel() { - self.current?.cancel() - self.clear() + func cancel() async { + await self.current?.cancel() + await self.clear() } - func clear() { + func clear() async { self.current = nil } diff --git a/Auth0/WebAuth.swift b/Auth0/WebAuth.swift index 67c1258a..eb3ffefb 100644 --- a/Auth0/WebAuth.swift +++ b/Auth0/WebAuth.swift @@ -5,7 +5,7 @@ import Foundation import Combine /// Callback invoked by the ``WebAuthUserAgent`` when the web-based operation concludes. -public typealias WebAuthProviderCallback = (WebAuthResult) -> Void +public typealias WebAuthProviderCallback = @Sendable (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. @@ -13,7 +13,7 @@ public typealias WebAuthProviderCallback = (WebAuthResult) -> Void /// ## See Also /// /// - [Example](https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift) -public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping WebAuthProviderCallback) -> WebAuthUserAgent +public typealias WebAuthProvider = (_ url: URL, _ callback: @escaping @Sendable WebAuthProviderCallback) async -> WebAuthUserAgent /// Web-based authentication using Auth0. /// @@ -41,7 +41,7 @@ public protocol WebAuth: Trackable, Loggable { - Parameter connection: Name of the connection. For example, `github`. - Returns: The same `WebAuth` instance to allow method chaining. */ - func connection(_ connection: String) -> Self + func connection(_ connection: String) async -> Self /** Specify the scopes that will be requested during authentication. @@ -54,7 +54,7 @@ public protocol WebAuth: Trackable, Loggable { - [Scopes](https://auth0.com/docs/get-started/apis/scopes) */ - func scope(_ scope: String) -> Self + func scope(_ scope: String) async -> Self /** Specify provider scopes for OAuth2/social connections, such as GitHub or Google. @@ -67,7 +67,7 @@ public protocol WebAuth: Trackable, Loggable { - [Connection Scopes](https://auth0.com/docs/authenticate/identity-providers/adding-scopes-for-an-external-idp) */ - func connectionScope(_ connectionScope: String) -> Self + func connectionScope(_ connectionScope: String) async -> Self /** Specify a `state` parameter that will be sent back after authentication to verify that the response @@ -77,7 +77,7 @@ public protocol WebAuth: Trackable, Loggable { - Parameter state: State value. - Returns: The same `WebAuth` instance to allow method chaining. */ - func state(_ state: String) -> Self + func state(_ state: String) async -> Self /** Specify additional parameters for authentication. @@ -85,7 +85,7 @@ public protocol WebAuth: Trackable, Loggable { - Parameter parameters: Additional authentication parameters. - Returns: The same `WebAuth` instance to allow method chaining. */ - func parameters(_ parameters: [String: String]) -> Self + func parameters(_ parameters: [String: String]) async -> Self /// Specify additional headers for `ASWebAuthenticationSession`. /// @@ -98,19 +98,19 @@ public protocol WebAuth: Trackable, Loggable { /// /// - [additionalHeaderFields](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/additionalheaderfields) @available(iOS 17.4, macOS 14.4, visionOS 1.2, *) - func headers(_ headers: [String: String]) -> Self + func headers(_ headers: [String: String]) async -> Self /// Specify a custom redirect URL to be used. /// /// - Parameter redirectURL: Custom redirect URL. /// - Returns: The same `WebAuth` instance to allow method chaining. - func redirectURL(_ redirectURL: URL) -> Self + func redirectURL(_ redirectURL: URL) async -> Self /// Specify a custom authorize URL to be used. /// /// - Parameter authorizeURL: Custom authorize URL. /// - Returns: The same `WebAuth` instance to allow method chaining. - func authorizeURL(_ authorizeURL: URL) -> Self + func authorizeURL(_ authorizeURL: URL) async -> Self /// Specify an audience name for the API that your application will call using the access token returned after /// authentication. @@ -122,34 +122,34 @@ public protocol WebAuth: Trackable, Loggable { /// ## See Also /// /// - [Audience](https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience) - func audience(_ audience: String) -> Self + func audience(_ audience: String) async -> Self /// Specify a `nonce` parameter for ID token validation. /// /// - Parameter nonce: Nonce value. /// - Returns: The same `WebAuth` instance to allow method chaining. - func nonce(_ nonce: String) -> Self + func nonce(_ nonce: String) async -> Self /// Specify a custom issuer for ID token validation. /// This value will be used instead of the Auth0 Domain. /// /// - Parameter issuer: Custom issuer value. For example, `https://example.com/`. /// - Returns: The same `WebAuth` instance to allow method chaining. - func issuer(_ issuer: String) -> Self + func issuer(_ issuer: String) async -> Self /// Specify a leeway amount for ID token validation. /// This value represents the clock skew for the validation of date claims, for example `exp`. /// /// - Parameter leeway: Number of milliseconds. Defaults to `60_000` (1 minute). /// - Returns: The same `WebAuth` instance to allow method chaining. - func leeway(_ leeway: Int) -> Self + func leeway(_ leeway: Int) async -> Self /// Specify a `max_age` parameter for authentication. /// Sending this parameter will require the presence of the `auth_time` claim in the ID token. /// /// - Parameter maxAge: Number of milliseconds. /// - Returns: The same `WebAuth` instance to allow method chaining. - func maxAge(_ maxAge: Int) -> Self + func maxAge(_ maxAge: Int) async -> 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. @@ -160,7 +160,7 @@ public protocol WebAuth: Trackable, Loggable { /// 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 + func useHTTPS() async -> 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). @@ -176,19 +176,19 @@ public protocol WebAuth: Trackable, Loggable { /// - /// - [FAQ](https://github.com/auth0/Auth0.swift/blob/master/FAQ.md) /// - [prefersEphemeralWebBrowserSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession/3237231-prefersephemeralwebbrowsersessio) - func useEphemeralSession() -> Self + func useEphemeralSession() async -> Self /// Specify an invitation URL to join an organization. /// /// - Parameter invitationURL: Invitation URL for the organization. /// - Returns: The same `WebAuth` instance to allow method chaining. - func invitationURL(_ invitationURL: URL) -> Self + func invitationURL(_ invitationURL: URL) async -> Self /// Specify an organization identifier to log in to. /// /// - Parameter organization: ID of the organization. /// - Returns: The same `WebAuth` instance to allow method chaining. - func organization(_ organization: String) -> Self + func organization(_ organization: String) async -> Self /// Specify a custom Web Auth provider to use instead of the default `ASWebAuthenticationSession` implementation. /// @@ -201,13 +201,13 @@ public protocol WebAuth: Trackable, Loggable { /// /// - /// - ``WebAuthProvider`` - func provider(_ provider: @escaping WebAuthProvider) -> Self + func provider(_ provider: @escaping WebAuthProvider) async -> Self /// Specify a callback to be called when the ``WebAuthUserAgent`` closes, while the flow continues with the code exchange. /// /// - Parameter callback: A callback to be executed /// - Returns: The same `WebAuth` instance to allow method chaining. - func onClose(_ callback: (() -> Void)?) -> Self + func onClose(_ callback: (@Sendable () -> Void)?) async -> Self // MARK: - Methods @@ -236,7 +236,7 @@ public protocol WebAuth: Trackable, Loggable { - Requires: The **Callback URL** to have been added to the **Allowed Callback URLs** field of your Auth0 application settings in the [Dashboard](https://manage.auth0.com/#/applications/). */ - func start(_ callback: @escaping (WebAuthResult) -> Void) + func start(_ callback: @escaping @Sendable (WebAuthResult) -> Void) #if canImport(_Concurrency) /** diff --git a/Auth0/WebAuthUserAgent.swift b/Auth0/WebAuthUserAgent.swift index 7b8755b0..77217217 100644 --- a/Auth0/WebAuthUserAgent.swift +++ b/Auth0/WebAuthUserAgent.swift @@ -4,10 +4,10 @@ /// ## See Also /// /// - [Example](https://github.com/auth0/Auth0.swift/blob/master/Auth0/SafariProvider.swift) -public protocol WebAuthUserAgent { +public protocol WebAuthUserAgent: Sendable { /// Starts the external user agent. - func start() + func start() async /// Tears down the external user agent after the web-based operation completed, if needed. /// Auth0.swift will call this method after the callback URL was received and processed, or after the user @@ -15,7 +15,7 @@ public protocol WebAuthUserAgent { /// /// - Parameter result: The outcome of the web-based operation, containing either an empty success case or an /// error. - func finish(with result: WebAuthResult) + func finish(with result: WebAuthResult) async } #endif diff --git a/Auth0/WebAuthentication.swift b/Auth0/WebAuthentication.swift index 19f8ea2d..44149222 100644 --- a/Auth0/WebAuthentication.swift +++ b/Auth0/WebAuthentication.swift @@ -51,15 +51,15 @@ public struct WebAuthentication { /// - Parameter url: The URL sent by the external user agent that contains the result of the web-based operation. /// - Returns: If the URL was expected and properly formatted. @discardableResult - public static func resume(with url: URL) -> Bool { - return TransactionStore.shared.resume(url) + public static func resume(with url: URL) async -> Bool { + return await TransactionStore.shared.resume(url) } /// Terminates the ongoing web-based operation and reports back that it was cancelled. /// You need to call this method within your custom Web Auth provider implementation whenever the operation is /// cancelled by the user. - public static func cancel() { - TransactionStore.shared.cancel() + public static func cancel() async { + await TransactionStore.shared.cancel() } } diff --git a/Auth0/WebViewProvider.swift b/Auth0/WebViewProvider.swift index 13e9831d..5ddc6e0d 100644 --- a/Auth0/WebViewProvider.swift +++ b/Auth0/WebViewProvider.swift @@ -52,17 +52,18 @@ public extension WebAuthentication { static func webViewProvider(style: UIModalPresentationStyle = .fullScreen) -> WebAuthProvider { return { url, callback in let redirectURL = extractRedirectURL(from: url)! - - return WebViewUserAgent(authorizeURL: url, - redirectURL: redirectURL, - modalPresentationStyle: style, - callback: callback) + return await Task { + return await WebViewUserAgent(authorizeURL: url, + redirectURL: redirectURL, + modalPresentationStyle: style, + callback: callback) + }.value } } } -class WebViewUserAgent: NSObject, WebAuthUserAgent { +@MainActor class WebViewUserAgent: NSObject, WebAuthUserAgent { static let customSchemeRedirectionSuccessMessage = "com.auth0.webview.redirection_success" static let customSchemeRedirectionFailureMessage = "com.auth0.webview.redirection_failure" @@ -74,7 +75,11 @@ class WebViewUserAgent: NSObject, WebAuthUserAgent { let redirectURL: URL let callback: WebAuthProviderCallback - init(authorizeURL: URL, redirectURL: URL, viewController: UIViewController = UIViewController(), modalPresentationStyle: UIModalPresentationStyle = .fullScreen, callback: @escaping WebAuthProviderCallback) { + init(authorizeURL: URL, + redirectURL: URL, + viewController: UIViewController = UIViewController(), + modalPresentationStyle: UIModalPresentationStyle = .fullScreen, + callback: @escaping WebAuthProviderCallback) async { self.request = URLRequest(url: authorizeURL) self.redirectURL = redirectURL self.callback = callback @@ -129,13 +134,14 @@ class WebViewUserAgent: NSObject, WebAuthUserAgent { /// Handling of Custom Scheme callbacks. extension WebViewUserAgent: WKURLSchemeHandler { - func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { - _ = TransactionStore.shared.resume(urlSchemeTask.request.url!) - let error = NSError(domain: WebViewUserAgent.customSchemeRedirectionSuccessMessage, code: 200, userInfo: [ - NSLocalizedDescriptionKey: "WebViewProvider: WKURLSchemeHandler: Succesfully redirected back to the app" - ]) - urlSchemeTask.didFailWithError(error) + Task { + _ = await TransactionStore.shared.resume(urlSchemeTask.request.url!) + let error = NSError(domain: WebViewUserAgent.customSchemeRedirectionSuccessMessage, code: 200, userInfo: [ + NSLocalizedDescriptionKey: "WebViewProvider: WKURLSchemeHandler: Succesfully redirected back to the app" + ]) + urlSchemeTask.didFailWithError(error) + } } func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { @@ -152,11 +158,13 @@ extension WebViewUserAgent: WKURLSchemeHandler { extension WebViewUserAgent: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let callbackUrl = navigationAction.request.url, callbackUrl.absoluteString.starts(with: redirectURL.absoluteString), let scheme = callbackUrl.scheme, scheme == "https" { - _ = TransactionStore.shared.resume(callbackUrl) - decisionHandler(.cancel) - } else { - decisionHandler(.allow) + Task { + if let callbackUrl = navigationAction.request.url, callbackUrl.absoluteString.starts(with: redirectURL.absoluteString), let scheme = callbackUrl.scheme, scheme == "https" { + _ = await TransactionStore.shared.resume(callbackUrl) + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } } } diff --git a/Package.swift b/Package.swift index dc386f1c..081ee0ec 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,6 @@ import PackageDescription let webAuthPlatforms: [Platform] = [.iOS, .macOS, .macCatalyst, .visionOS] let swiftSettings: [SwiftSetting] = [ - .swiftLanguageMode(.v5), .define("WEB_AUTH_PLATFORM", .when(platforms: webAuthPlatforms)), .define("PASSKEYS_PLATFORM", .when(platforms: webAuthPlatforms)) ]