Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions App/ViewController.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
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 { [weak self] in
guard let self = self else { return }
await self.testFetchCredentials()
}
}
print($0)
print(result)
}
}

Expand All @@ -34,6 +59,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)")
}
}

}

Expand Down
2 changes: 1 addition & 1 deletion Auth0/Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
4 changes: 2 additions & 2 deletions Auth0/BioAuthentication.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#if WEB_AUTH_PLATFORM
import Foundation
import LocalAuthentication
@preconcurrency import LocalAuthentication

struct BioAuthentication {
struct BioAuthentication: Sendable {

private let authContext: LAContext
private let evaluationPolicy: LAPolicy

let title: String
let policy: BiometricPolicy

Check warning on line 12 in Auth0/BioAuthentication.swift

View workflow job for this annotation

GitHub Actions / Lint code with SwiftLint

Lines should not have trailing whitespace (trailing_whitespace)
var fallbackTitle: String? {
get { return self.authContext.localizedFallbackTitle }
set { self.authContext.localizedFallbackTitle = newValue }
Expand Down
2 changes: 1 addition & 1 deletion Auth0/BiometricPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 11 additions & 4 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,23 @@ import LocalAuthentication
///
/// - ``CredentialsManagerError``
/// - <doc:RefreshTokens>
public struct CredentialsManager {
public struct CredentialsManager: Sendable {

// storage is inherently sendable as it uses Keychain under the hood and is stateless
private let sendableStorage: SendableBox<CredentialsStorage>

private var storage: CredentialsStorage {
sendableStorage.value
}

private let storage: CredentialsStorage
private let storeKey: String
private let authentication: Authentication
private let dispatchQueue = DispatchQueue(label: "com.auth0.credentialsmanager.serial")
#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 across threads.
private final class BiometricSession: @unchecked Sendable {
let noSession: TimeInterval = -1
var lastBiometricAuthTime: TimeInterval = -1
let lock = NSLock()
Expand All @@ -56,7 +63,7 @@ public struct CredentialsManager {
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.
Expand Down
2 changes: 1 addition & 1 deletion Auth0/IDTokenValidator.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#if WEB_AUTH_PLATFORM
import Foundation
import JWTDecode
@preconcurrency import JWTDecode

protocol JWTValidator {
func validate(_ jwt: JWT) -> Auth0Error?
Expand Down
4 changes: 2 additions & 2 deletions Auth0/Logger.swift
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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()
}
Expand Down
1 change: 0 additions & 1 deletion Auth0/Requestable.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import Combine

public protocol Requestable {
associatedtype ResultType
Expand Down
6 changes: 6 additions & 0 deletions Auth0/Shared.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>: @unchecked Sendable {
let value: T
}
2 changes: 1 addition & 1 deletion Auth0/Telemetry.swift
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
22 changes: 22 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,28 @@ 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`, 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)
> 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: [:])
> }
> }
> ```

### 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.
Expand Down
Loading