From d8f787b3e20962fff5b98d879ce646509b9cdc1e Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Mon, 8 Dec 2025 15:14:18 +0530 Subject: [PATCH 1/9] Github issue: #890 Conforming Credential Manager to Sendable --- Auth0/Authentication.swift | 2 +- Auth0/BioAuthentication.swift | 4 ++-- Auth0/BiometricPolicy.swift | 2 +- Auth0/CredentialsManager.swift | 5 +++-- Auth0/CredentialsStorage.swift | 2 +- Auth0/IDTokenValidator.swift | 2 +- Auth0/Requestable.swift | 1 - Auth0/Telemetry.swift | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index 5db071f8a..9b687a58a 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: SenderConstraining, Trackable, Loggable { +public protocol Authentication: SenderConstraining, Trackable, Loggable, Sendable { /// The Auth0 Client ID. var clientId: String { get } diff --git a/Auth0/BioAuthentication.swift b/Auth0/BioAuthentication.swift index 1575fa6d7..a6acab6ea 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 diff --git a/Auth0/BiometricPolicy.swift b/Auth0/BiometricPolicy.swift index c8b278bcc..1847bb5ba 100644 --- a/Auth0/BiometricPolicy.swift +++ b/Auth0/BiometricPolicy.swift @@ -2,7 +2,7 @@ import Foundation /// Defines the policy for when a biometric prompt should be shown when using the Credentials Manager. -public enum BiometricPolicy { +public enum BiometricPolicy: Sendable { /// Default behavior. Uses the same LAContext instance, allowing the system to manage biometric prompts. /// The system may skip the prompt if biometric authentication was recently successful. diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 6142dedb1..4ee09057b 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 @@ -33,7 +33,8 @@ public struct CredentialsManager { #if WEB_AUTH_PLATFORM var bioAuth: BioAuthentication? // Biometric session management - using a class to allow mutation in non-mutating methods - private final class BiometricSession { + // @unchecked Sendable is fine here as we are using lock to read and update lastBiometricAuthTime which is safe acorss threads. + private final class BiometricSession: @unchecked Sendable { let noSession: TimeInterval = -1 var lastBiometricAuthTime: TimeInterval = -1 let lock = NSLock() diff --git a/Auth0/CredentialsStorage.swift b/Auth0/CredentialsStorage.swift index abc22501d..2762f9809 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/IDTokenValidator.swift b/Auth0/IDTokenValidator.swift index b5ae54fec..fcb805080 100644 --- a/Auth0/IDTokenValidator.swift +++ b/Auth0/IDTokenValidator.swift @@ -1,6 +1,6 @@ #if WEB_AUTH_PLATFORM import Foundation -import JWTDecode +@preconcurrency import JWTDecode protocol JWTValidator { func validate(_ jwt: JWT) -> Auth0Error? diff --git a/Auth0/Requestable.swift b/Auth0/Requestable.swift index 970d44463..bba1638bf 100644 --- a/Auth0/Requestable.swift +++ b/Auth0/Requestable.swift @@ -1,5 +1,4 @@ import Foundation -import Combine public protocol Requestable { associatedtype ResultType diff --git a/Auth0/Telemetry.swift b/Auth0/Telemetry.swift index 4d62618b9..dc434675f 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" From 8d9ffa22ad3dc4d61419516db9fdddc32b1e4866 Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Mon, 8 Dec 2025 17:39:03 +0530 Subject: [PATCH 2/9] update docs --- EXAMPLES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/EXAMPLES.md b/EXAMPLES.md index ca4e12773..7d4251eef 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -367,6 +367,28 @@ The Credentials Manager utility allows you to securely store and retrieve the us let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) ``` +> [!NOTE] +> **Swift 6 Concurrency Support**: The Credentials Manager conforms to `Sendable` and can be safely used across concurrency contexts, including within actors. +> +> ```swift +> // Example: Using CredentialsManager in an Actor (Swift 6) +> actor AuthService { +> let credentialsManager: CredentialsManager +> +> init() { +> self.credentialsManager = CredentialsManager(authentication: Auth0.authentication()) +> } +> +> func fetchCredentials() async throws -> Credentials { +> // Safe to call from within an actor +> return try await credentialsManager.credentials(withScope: "openid profile email", +> minTTL: 60, +> parameters: [:], +> headers: [:]) +> } +> } +> ``` + > [!CAUTION] > The Credentials Manager is not thread-safe, except for the following methods: > From c39dddb28be1cd5f64efdf34c492b55f07af02b3 Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Tue, 9 Dec 2025 13:56:40 +0530 Subject: [PATCH 3/9] Update Documentation and testing it in App Target --- App/AppDelegate.swift | 2 +- App/ViewController.swift | 44 +++++++++++++++++++++++++++++---- Auth0.xcodeproj/project.pbxproj | 4 +-- Auth0/CredentialsManager.swift | 2 +- EXAMPLES.md | 20 +++++++-------- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/App/AppDelegate.swift b/App/AppDelegate.swift index bba2d4a00..739fcd365 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? diff --git a/App/ViewController.swift b/App/ViewController.swift index 859e71e86..b88a4c5a0 100644 --- a/App/ViewController.swift +++ b/App/ViewController.swift @@ -1,25 +1,49 @@ import UIKit import Auth0 +// MARK: - Swift 6 Sendability Test: CredentialsManager in Actor +actor AuthService { + let credentialsManager: CredentialsManager + + init() { + self.credentialsManager = CredentialsManager(authentication: Auth0.authentication()) + } + + func fetchCredentials() async throws -> Credentials { + // This method can be called across concurrency contexts eg. Actor + return try await credentialsManager.credentials(withScope: "openid profile email", + minTTL: 60, + parameters: [:], + headers: [:]) + } +} + class ViewController: UIViewController { + + // Swift 6 test: CredentialsManager can be used within actors + private let authService = AuthService() @IBAction func login(_ sender: Any) { Auth0 .webAuth() .logging(enabled: true) - .start { - switch $0 { + .start { [weak self] result in + switch result { case .failure(let error): DispatchQueue.main.async { - self.alert(title: "Error", message: "\(error)") + self?.alert(title: "Error", message: "\(error)") } case .success(let credentials): DispatchQueue.main.async { - self.alert(title: "Success", + self?.alert(title: "Success", message: "Authorized and got a token \(credentials.accessToken)") } + // Test: Fetch credentials from actor with custom scope + Task { + await self?.testFetchCredentials() + } } - print($0) + print(result) } } @@ -34,6 +58,16 @@ class ViewController: UIViewController { } } } + + // Additional test method to fetch credentials from actor + func testFetchCredentials() async { + do { + let credentials = try await authService.fetchCredentials() + print("Successfully fetched credentials within actor: \(credentials.accessToken)") + } catch { + print("Failed to fetch credentials: \(error)") + } + } } diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index 35b7eaf13..4a4f659c7 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -3891,7 +3891,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -3913,7 +3913,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.auth0.OAuth2; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index fe871765e..e025009d7 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -33,7 +33,7 @@ public struct CredentialsManager: Sendable { #if WEB_AUTH_PLATFORM var bioAuth: BioAuthentication? // Biometric session management - using a class to allow mutation in non-mutating methods - // @unchecked Sendable is fine here as we are using lock to read and update lastBiometricAuthTime which is safe acorss threads. + // @unchecked Sendable is fine here as we are using lock to read and update lastBiometricAuthTime which is safe across threads. private final class BiometricSession: @unchecked Sendable { let noSession: TimeInterval = -1 var lastBiometricAuthTime: TimeInterval = -1 diff --git a/EXAMPLES.md b/EXAMPLES.md index 7d4251eef..bbc3088fb 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -367,6 +367,16 @@ The Credentials Manager utility allows you to securely store and retrieve the us let credentialsManager = CredentialsManager(authentication: Auth0.authentication()) ``` +> [!CAUTION] +> The Credentials Manager is not thread-safe, except for the following methods: +> +> - `credentials()` +> - `apiCredentials()` +> - `ssoCredentials()` +> - `renew()` +> +> To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. + > [!NOTE] > **Swift 6 Concurrency Support**: The Credentials Manager conforms to `Sendable` and can be safely used across concurrency contexts, including within actors. > @@ -389,16 +399,6 @@ let credentialsManager = CredentialsManager(authentication: Auth0.authentication > } > ``` -> [!CAUTION] -> The Credentials Manager is not thread-safe, except for the following methods: -> -> - `credentials()` -> - `apiCredentials()` -> - `ssoCredentials()` -> - `renew()` -> -> To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. - ### Store credentials When your users log in, store their credentials securely in the Keychain. You can then check if their credentials are still valid when they open your app again. From f95038b7966e3a59f92415a21ffb1acb73151abd Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Tue, 9 Dec 2025 14:15:38 +0530 Subject: [PATCH 4/9] update doc --- EXAMPLES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index bbc3088fb..780d1bb12 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -378,7 +378,7 @@ let credentialsManager = CredentialsManager(authentication: Auth0.authentication > To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. > [!NOTE] -> **Swift 6 Concurrency Support**: The Credentials Manager conforms to `Sendable` and can be safely used across concurrency contexts, including within actors. +> **Swift 6 Sendability Support**: The Credentials Manager conforms to `Sendable` and can be called within actors. > > ```swift > // Example: Using CredentialsManager in an Actor (Swift 6) From 1ae38a2f9859abcc0fc7a361cf0f5bee9c902141 Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Tue, 9 Dec 2025 15:54:37 +0530 Subject: [PATCH 5/9] backward compatibility --- Auth0/CredentialsManager.swift | 10 +++++++++- Auth0/CredentialsStorage.swift | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index e025009d7..e3225ceff 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -26,7 +26,15 @@ import LocalAuthentication /// - public struct CredentialsManager: Sendable { - private let storage: CredentialsStorage + private let sendableStorage: SendableBox + private var storage: CredentialsStorage { + sendableStorage.value + } + + struct SendableBox: @unchecked Sendable { + let value: T + } + private let storeKey: String private let authentication: Authentication private let dispatchQueue = DispatchQueue(label: "com.auth0.credentialsmanager.serial") diff --git a/Auth0/CredentialsStorage.swift b/Auth0/CredentialsStorage.swift index 2762f9809..abc22501d 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: Sendable { +public protocol CredentialsStorage { /// Retrieves a storage entry. /// From c26a8020720f3e9053318e4755301cebb4bf7d1d Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Wed, 10 Dec 2025 09:28:44 +0530 Subject: [PATCH 6/9] docs --- Auth0/CredentialsManager.swift | 8 +++----- Auth0/Shared.swift | 6 ++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index e3225ceff..9a496905b 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -25,15 +25,13 @@ import LocalAuthentication /// - ``CredentialsManagerError`` /// - public struct CredentialsManager: Sendable { - + + // storage is inherently sendable as it uses Keychain under the hood and is stateless private let sendableStorage: SendableBox + private var storage: CredentialsStorage { sendableStorage.value } - - struct SendableBox: @unchecked Sendable { - let value: T - } private let storeKey: String private let authentication: Authentication diff --git a/Auth0/Shared.swift b/Auth0/Shared.swift index a90251803..5a3034fec 100644 --- a/Auth0/Shared.swift +++ b/Auth0/Shared.swift @@ -30,3 +30,9 @@ func extractRedirectURL(from url: URL) -> URL? { return nil } + +/// Wrapper for non-Sendable types that need to be used in Sendable contexts. +/// Use only when thread-safety is guaranteed through synchronization (locks, serial queues, etc.). +struct SendableBox: @unchecked Sendable { + let value: T +} From baff721a0d997c2ecff26e01afe738a7bc41b05c Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Wed, 10 Dec 2025 12:37:49 +0530 Subject: [PATCH 7/9] address claud --- App/ViewController.swift | 5 +++-- Auth0/CredentialsManager.swift | 2 +- EXAMPLES.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/App/ViewController.swift b/App/ViewController.swift index b88a4c5a0..61c1e873f 100644 --- a/App/ViewController.swift +++ b/App/ViewController.swift @@ -39,8 +39,9 @@ class ViewController: UIViewController { message: "Authorized and got a token \(credentials.accessToken)") } // Test: Fetch credentials from actor with custom scope - Task { - await self?.testFetchCredentials() + Task { [weak self] in + guard let self = self else { return } + await self.testFetchCredentials() } } print(result) diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 9a496905b..c32f11bf3 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -63,7 +63,7 @@ public struct CredentialsManager: Sendable { storage: CredentialsStorage = SimpleKeychain()) { self.storeKey = storeKey self.authentication = authentication - self.storage = storage + self.sendableStorage = SendableBox(value: storage) } /// Retrieves the user information from the Keychain synchronously, without checking if the credentials are expired. diff --git a/EXAMPLES.md b/EXAMPLES.md index 780d1bb12..591b4da8c 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -378,7 +378,7 @@ let credentialsManager = CredentialsManager(authentication: Auth0.authentication > To avoid concurrency issues, do not call its non thread-safe methods and properties from different threads without proper synchronization. > [!NOTE] -> **Swift 6 Sendability Support**: The Credentials Manager conforms to `Sendable` and can be called within actors. +> **Swift 6 Sendability Support**: The Credentials Manager conforms to `Sendable`, which allows it to be passed across concurrency boundaries (like into actors). However, this does **not** make all its methods thread-safe. Only the methods listed above (`credentials()`, `apiCredentials()`, `ssoCredentials()`, `renew()`) are thread-safe. Other methods and properties still require proper synchronization when called from multiple threads. > > ```swift > // Example: Using CredentialsManager in an Actor (Swift 6) From 54d6564463d03707347dd8d3cd4708797743617d Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Wed, 10 Dec 2025 21:27:38 +0530 Subject: [PATCH 8/9] sendability --- Auth0/Logger.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Auth0/Logger.swift b/Auth0/Logger.swift index c1dee060d..6e48bca64 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) @@ -16,7 +16,7 @@ public protocol Logger { private let networkTraceQueue = DispatchQueue(label: "com.auth0.networkTrace", qos: .utility) -protocol LoggerOutput { +protocol LoggerOutput: Sendable { func log(message: String) func newLine() } From 8db68bb2211629378954223eb99a53f8a368eef6 Mon Sep 17 00:00:00 2001 From: sanchitmehta94 Date: Thu, 11 Dec 2025 14:57:09 +0530 Subject: [PATCH 9/9] test --- App/AppDelegate.swift | 2 +- Auth0.xcodeproj/project.pbxproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/App/AppDelegate.swift b/App/AppDelegate.swift index 739fcd365..bba2d4a00 100644 --- a/App/AppDelegate.swift +++ b/App/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Auth0 -@main +@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? diff --git a/Auth0.xcodeproj/project.pbxproj b/Auth0.xcodeproj/project.pbxproj index 4a4f659c7..35b7eaf13 100644 --- a/Auth0.xcodeproj/project.pbxproj +++ b/Auth0.xcodeproj/project.pbxproj @@ -3891,7 +3891,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -3913,7 +3913,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.auth0.OAuth2; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; }; name = Release; };