Skip to content
Merged
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
10 changes: 9 additions & 1 deletion Auth0.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
5B7EE48D20FCA0F400367724 /* Auth0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F06DD851CC448C90011842B /* Auth0.framework */; };
5B7EE48E20FCA0F400367724 /* Auth0.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 5F06DD851CC448C90011842B /* Auth0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
A4PF2C6UJAB9I6DXDJUTRG3B /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
5B9262C31ECF0CC200F4F6D3 /* BioAuthenticationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */; };
5BE65DCA1F7270DE00CADD3B /* Auth0.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 5F06DD781CC448B10011842B /* Auth0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5BEDE18A1EC21B040007300D /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1891EC21B040007300D /* CredentialsManager.swift */; };
Expand Down Expand Up @@ -124,6 +125,7 @@
5C41F6C8244F969600252548 /* ASProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B16D88C1F7141A0009476A5 /* ASProvider.swift */; };
5C41F6CA244F96AE00252548 /* LoginTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C41F6A3244DC94E00252548 /* LoginTransaction.swift */; };
5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
V17KI34D4PWP799FLD81GPVP /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
5C41F6CC244F96F200252548 /* ClaimValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D7023D0BED200074024 /* ClaimValidators.swift */; };
5C41F6CD244F96FD00252548 /* IDTokenSignatureValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3D23D0BA2C00074024 /* IDTokenSignatureValidator.swift */; };
5C41F6CE244F970500252548 /* IDTokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3E23D0BA2C00074024 /* IDTokenValidator.swift */; };
Expand Down Expand Up @@ -517,6 +519,7 @@
C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D7023D0BED200074024 /* ClaimValidators.swift */; };
C1B3B9FC2C24B6D4004A32A4 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1748731EF2D3A40060E653 /* Shared.swift */; };
C1B3B9FD2C24B6D4004A32A4 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
2Z7FPPVSUUJRZC06V5RA8B2B /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
C1B3B9FE2C24B6D4004A32A4 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1891EC21B040007300D /* CredentialsManager.swift */; };
C1B3B9FF2C24B6D4004A32A4 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; };
C1B3BA002C24B6D4004A32A4 /* CredentialsManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */; };
Expand Down Expand Up @@ -860,6 +863,7 @@
5B7EE48220FCA0A200367724 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
5B7EE48420FCA0A200367724 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BioAuthentication.swift; sourceTree = "<group>"; };
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricPolicy.swift; sourceTree = "<group>"; };
5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BioAuthenticationSpec.swift; path = Auth0Tests/BioAuthenticationSpec.swift; sourceTree = SOURCE_ROOT; };
5BA58D33209081A700782DD1 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
5BEDE1891EC21B040007300D /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1025,7 +1029,6 @@
C107B5202CA27F76006B6BEA /* WebViewProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProviderSpec.swift; sourceTree = "<group>"; };
C160EE302CABD0DA005ACE8E /* UIWindow+TopViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+TopViewController.swift"; sourceTree = "<group>"; };
C160EE372CABD358005ACE8E /* UIWindow+TopViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+TopViewControllerSpec.swift"; sourceTree = "<group>"; };
C177D6C22C2ADDEB0094C657 /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Auth0.plist; path = ../Auth0.plist; sourceTree = "<group>"; };
C177D76F2C2BDFE40094C657 /* NetworkStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStub.swift; sourceTree = "<group>"; };
C177D7742C2BE00D0094C657 /* StubURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubURLProtocol.swift; sourceTree = "<group>"; };
C1B3B9A82C24B297004A32A4 /* OAuth2Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OAuth2Vision.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -1048,6 +1051,7 @@
D4EDCFBF2E740295008E02F8 /* PreferredAuthenticationMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredAuthenticationMethod.swift; sourceTree = "<group>"; };
D581CF762757D773007327D1 /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeGenerator.swift; sourceTree = "<group>"; };
C177D6C22C2ADDEB0094C657 /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Auth0.plist; path = ../Auth0.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1211,6 +1215,7 @@
5CB41D3B23D0BA0300074024 /* Validators */,
5B1748731EF2D3A40060E653 /* Shared.swift */,
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */,
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */,
5BEDE1891EC21B040007300D /* CredentialsManager.swift */,
5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */,
5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */,
Expand Down Expand Up @@ -2481,6 +2486,7 @@
5FE2F8B21CCEAED8003628F4 /* Requestable.swift in Sources */,
5C4F551E23C8FB8E00C89615 /* Array+Encode.swift in Sources */,
5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */,
A4PF2C6UJAB9I6DXDJUTRG3B /* BiometricPolicy.swift in Sources */,
5CB41D7123D0BED200074024 /* ClaimValidators.swift in Sources */,
5FADB60C1CED7E0800D4BB50 /* UserPatchAttributes.swift in Sources */,
5FCAB1791D09124D00331C84 /* NSURL+Auth0.swift in Sources */,
Expand Down Expand Up @@ -2579,6 +2585,7 @@
5C505FAF2E216677005D0757 /* DPoPError.swift in Sources */,
5F74CB411CEFD5E600226823 /* JSONObjectPayload.swift in Sources */,
5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */,
V17KI34D4PWP799FLD81GPVP /* BiometricPolicy.swift in Sources */,
5C41F6C8244F969600252548 /* ASProvider.swift in Sources */,
5C38EA242DA4611B0085AC31 /* MyAccount.swift in Sources */,
5C41F6DF244FA1EE00252548 /* NSURLComponents+OAuth2.swift in Sources */,
Expand Down Expand Up @@ -2967,6 +2974,7 @@
C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */,
C1B3B9FC2C24B6D4004A32A4 /* Shared.swift in Sources */,
C1B3B9FD2C24B6D4004A32A4 /* BioAuthentication.swift in Sources */,
2Z7FPPVSUUJRZC06V5RA8B2B /* BiometricPolicy.swift in Sources */,
5C505FAE2E216677005D0757 /* DPoPError.swift in Sources */,
C1B3B9FE2C24B6D4004A32A4 /* CredentialsManager.swift in Sources */,
C1B3B9FF2C24B6D4004A32A4 /* CredentialsStorage.swift in Sources */,
Expand Down
11 changes: 10 additions & 1 deletion Auth0/BioAuthentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct BioAuthentication {
private let evaluationPolicy: LAPolicy

let title: String
let policy: BiometricPolicy

var fallbackTitle: String? {
get { return self.authContext.localizedFallbackTitle }
set { self.authContext.localizedFallbackTitle = newValue }
Expand All @@ -22,10 +24,17 @@ struct BioAuthentication {
return self.authContext.canEvaluatePolicy(evaluationPolicy, error: nil)
}

init(authContext: LAContext, evaluationPolicy: LAPolicy, title: String, cancelTitle: String? = nil, fallbackTitle: String? = nil) {
init(authContext: LAContext,
evaluationPolicy: LAPolicy,
title: String,
cancelTitle: String? = nil,
fallbackTitle: String? = nil,
policy: BiometricPolicy = .always
) {
self.authContext = authContext
self.evaluationPolicy = evaluationPolicy
self.title = title
self.policy = policy
self.cancelTitle = cancelTitle
self.fallbackTitle = fallbackTitle
}
Expand Down
19 changes: 19 additions & 0 deletions Auth0/BiometricPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if WEB_AUTH_PLATFORM
import Foundation

/// Defines the policy for when a biometric prompt should be shown when using the Credentials Manager.
public enum BiometricPolicy {

/// Default behavior. A biometric prompt will be shown for every call to `credentials()`.
case always

/// A biometric prompt will be shown only once within the specified timeout period.
/// - Parameter timeoutInSeconds: The duration for which the session remains valid.
case session(timeoutInSeconds: Int)

/// A biometric prompt will be shown only once while the app is in the foreground.
/// The session is invalidated by calling `clearBiometricSession()` or after the default timeout.
/// - Parameter timeoutInSeconds: The duration for which the session remains valid. Defaults to 3600 seconds (1 hour).
case appLifecycle(timeoutInSeconds: Int = 3600)
}
#endif
104 changes: 102 additions & 2 deletions Auth0/CredentialsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ public struct CredentialsManager {
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 {
let noSession: TimeInterval = -1
var lastBiometricAuthTime: TimeInterval = -1
let lock = NSLock()

init() {
lastBiometricAuthTime = noSession
}
}
private let biometricSession = BiometricSession()
#endif

/// Creates a new `CredentialsManager` instance.
Expand Down Expand Up @@ -82,17 +93,27 @@ public struct CredentialsManager {
/// evaluationPolicy: .deviceOwnerAuthentication)
/// ```
///
/// You can also configure a ``BiometricPolicy`` to control when biometric authentication is required:
///
/// ```swift
/// // Require authentication only once per 5-minute session
/// credentialsManager.enableBiometrics(withTitle: "Unlock with Face ID",
/// policy: .session(timeoutInSeconds: 300))
/// ```
///
/// - Parameters:
/// - title: Main message to display when Face ID or Touch ID is used.
/// - cancelTitle: Cancel message to display when Face ID or Touch ID is used.
/// - fallbackTitle: Fallback message to display when Face ID or Touch ID is used after a failed match.
/// - evaluationPolicy: Policy to be used for authentication policy evaluation.
/// - policy: The ``BiometricPolicy`` that controls when biometric authentication is required. Defaults to `.always`.
/// - Important: Access to the ``user`` property will not be protected by biometric authentication.
public mutating func enableBiometrics(withTitle title: String,
cancelTitle: String? = nil,
fallbackTitle: String? = nil,
evaluationPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics) {
self.bioAuth = BioAuthentication(authContext: LAContext(), evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle)
evaluationPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics,
policy: BiometricPolicy = .always) {
self.bioAuth = BioAuthentication(authContext: LAContext(), evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle, policy: policy)
}
#endif

Expand Down Expand Up @@ -125,6 +146,11 @@ public struct CredentialsManager {
///
/// - Returns: If the credentials were removed.
public func clear() -> Bool {
#if WEB_AUTH_PLATFORM
self.biometricSession.lock.lock()
self.biometricSession.lastBiometricAuthTime = self.biometricSession.noSession
self.biometricSession.lock.unlock()
#endif
return self.storage.deleteEntry(forKey: self.storeKey)
}

Expand All @@ -142,6 +168,49 @@ public struct CredentialsManager {
return self.storage.deleteEntry(forKey: audience)
}

#if WEB_AUTH_PLATFORM
/// Checks if the current biometric session is valid based on the configured policy.
///
/// ## Usage
///
/// ```swift
/// let isValid = credentialsManager.isBiometricSessionValid()
/// ```
///
/// - Returns: `true` if the session is valid and biometric authentication can be skipped, `false` otherwise.
public func isBiometricSessionValid() -> Bool {
guard let bioAuth = self.bioAuth else { return false }

self.biometricSession.lock.lock()
defer { self.biometricSession.lock.unlock() }

let lastAuth = self.biometricSession.lastBiometricAuthTime
if lastAuth == self.biometricSession.noSession { return false }

switch bioAuth.policy {
case .session(let timeoutInSeconds), .appLifecycle(let timeoutInSeconds):
let timeoutInterval = TimeInterval(timeoutInSeconds)
return Date().timeIntervalSince1970 - lastAuth < timeoutInterval
case .always:
return false
}
}

/// Clears the in-memory biometric session timestamp. This will force biometric authentication on the next
/// credential access.
///
/// ## Usage
///
/// ```swift
/// credentialsManager.clearBiometricSession()
/// ```
public func clearBiometricSession() {
self.biometricSession.lock.lock()
defer { self.biometricSession.lock.unlock() }
self.biometricSession.lastBiometricAuthTime = self.biometricSession.noSession
}
#endif

/// Calls the `/oauth/revoke` endpoint to revoke the refresh token and then clears the credentials if the request
/// was successful. Otherwise, the credentials will not be cleared and the callback will be called with a failure
/// result containing a ``CredentialsManagerError/revokeFailed`` error.
Expand Down Expand Up @@ -314,11 +383,26 @@ public struct CredentialsManager {
return callback(.failure(error))
}

// Check if biometric session is valid based on policy
if self.isBiometricSessionValid() {
// Session is valid, bypass biometric prompt
self.retrieveCredentials(scope: scope,
minTTL: minTTL,
parameters: parameters,
headers: headers,
forceRenewal: false,
callback: callback)
return
}

bioAuth.validateBiometric { error in
guard error == nil else {
return callback(.failure(CredentialsManagerError(code: .biometricsFailed, cause: error!)))
}

// Update biometric session after successful authentication (only for session-based policies)
self.updateBiometricSession(for: bioAuth.policy)

self.retrieveCredentials(scope: scope,
minTTL: minTTL,
parameters: parameters,
Expand Down Expand Up @@ -1504,5 +1588,21 @@ public extension CredentialsManager {
}
}

#if WEB_AUTH_PLATFORM
/// Updates the biometric session timestamp to the current time.
/// Only updates for session-based policies (Session and AppLifecycle).
private func updateBiometricSession(for policy: BiometricPolicy) {
// Don't update session for "Always" policy
switch policy {
case .always:
return
case .session, .appLifecycle:
self.biometricSession.lock.lock()
defer { self.biometricSession.lock.unlock() }
self.biometricSession.lastBiometricAuthTime = Date().timeIntervalSince1970
}
}
#endif

}
#endif
Loading
Loading