@@ -32,6 +32,17 @@ public struct CredentialsManager {
3232 private let dispatchQueue = DispatchQueue ( label: " com.auth0.credentialsmanager.serial " )
3333 #if WEB_AUTH_PLATFORM
3434 var bioAuth : BioAuthentication ?
35+ // Biometric session management - using a class to allow mutation in non-mutating methods
36+ private final class BiometricSession {
37+ let noSession : TimeInterval = - 1
38+ var lastBiometricAuthTime : TimeInterval = - 1
39+ let lock = NSLock ( )
40+
41+ init ( ) {
42+ lastBiometricAuthTime = noSession
43+ }
44+ }
45+ private let biometricSession = BiometricSession ( )
3546 #endif
3647
3748 /// Creates a new `CredentialsManager` instance.
@@ -82,17 +93,27 @@ public struct CredentialsManager {
8293 /// evaluationPolicy: .deviceOwnerAuthentication)
8394 /// ```
8495 ///
96+ /// You can also configure a ``BiometricPolicy`` to control when biometric authentication is required:
97+ ///
98+ /// ```swift
99+ /// // Require authentication only once per 5-minute session
100+ /// credentialsManager.enableBiometrics(withTitle: "Unlock with Face ID",
101+ /// policy: .session(timeoutInSeconds: 300))
102+ /// ```
103+ ///
85104 /// - Parameters:
86105 /// - title: Main message to display when Face ID or Touch ID is used.
87106 /// - cancelTitle: Cancel message to display when Face ID or Touch ID is used.
88107 /// - fallbackTitle: Fallback message to display when Face ID or Touch ID is used after a failed match.
89108 /// - evaluationPolicy: Policy to be used for authentication policy evaluation.
109+ /// - policy: The ``BiometricPolicy`` that controls when biometric authentication is required. Defaults to `.always`.
90110 /// - Important: Access to the ``user`` property will not be protected by biometric authentication.
91111 public mutating func enableBiometrics( withTitle title: String ,
92112 cancelTitle: String ? = nil ,
93113 fallbackTitle: String ? = nil ,
94- evaluationPolicy: LAPolicy = . deviceOwnerAuthenticationWithBiometrics) {
95- self . bioAuth = BioAuthentication ( authContext: LAContext ( ) , evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle)
114+ evaluationPolicy: LAPolicy = . deviceOwnerAuthenticationWithBiometrics,
115+ policy: BiometricPolicy = . always) {
116+ self . bioAuth = BioAuthentication ( authContext: LAContext ( ) , evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle, policy: policy)
96117 }
97118 #endif
98119
@@ -125,6 +146,11 @@ public struct CredentialsManager {
125146 ///
126147 /// - Returns: If the credentials were removed.
127148 public func clear( ) -> Bool {
149+ #if WEB_AUTH_PLATFORM
150+ self . biometricSession. lock. lock ( )
151+ self . biometricSession. lastBiometricAuthTime = self . biometricSession. noSession
152+ self . biometricSession. lock. unlock ( )
153+ #endif
128154 return self . storage. deleteEntry ( forKey: self . storeKey)
129155 }
130156
@@ -142,6 +168,49 @@ public struct CredentialsManager {
142168 return self . storage. deleteEntry ( forKey: audience)
143169 }
144170
171+ #if WEB_AUTH_PLATFORM
172+ /// Checks if the current biometric session is valid based on the configured policy.
173+ ///
174+ /// ## Usage
175+ ///
176+ /// ```swift
177+ /// let isValid = credentialsManager.isBiometricSessionValid()
178+ /// ```
179+ ///
180+ /// - Returns: `true` if the session is valid and biometric authentication can be skipped, `false` otherwise.
181+ public func isBiometricSessionValid( ) -> Bool {
182+ guard let bioAuth = self . bioAuth else { return false }
183+
184+ self . biometricSession. lock. lock ( )
185+ defer { self . biometricSession. lock. unlock ( ) }
186+
187+ let lastAuth = self . biometricSession. lastBiometricAuthTime
188+ if lastAuth == self . biometricSession. noSession { return false }
189+
190+ switch bioAuth. policy {
191+ case . session( let timeoutInSeconds) , . appLifecycle( let timeoutInSeconds) :
192+ let timeoutInterval = TimeInterval ( timeoutInSeconds)
193+ return Date ( ) . timeIntervalSince1970 - lastAuth < timeoutInterval
194+ case . always:
195+ return false
196+ }
197+ }
198+
199+ /// Clears the in-memory biometric session timestamp. This will force biometric authentication on the next
200+ /// credential access.
201+ ///
202+ /// ## Usage
203+ ///
204+ /// ```swift
205+ /// credentialsManager.clearBiometricSession()
206+ /// ```
207+ public func clearBiometricSession( ) {
208+ self . biometricSession. lock. lock ( )
209+ defer { self . biometricSession. lock. unlock ( ) }
210+ self . biometricSession. lastBiometricAuthTime = self . biometricSession. noSession
211+ }
212+ #endif
213+
145214 /// Calls the `/oauth/revoke` endpoint to revoke the refresh token and then clears the credentials if the request
146215 /// was successful. Otherwise, the credentials will not be cleared and the callback will be called with a failure
147216 /// result containing a ``CredentialsManagerError/revokeFailed`` error.
@@ -314,11 +383,26 @@ public struct CredentialsManager {
314383 return callback ( . failure( error) )
315384 }
316385
386+ // Check if biometric session is valid based on policy
387+ if self . isBiometricSessionValid ( ) {
388+ // Session is valid, bypass biometric prompt
389+ self . retrieveCredentials ( scope: scope,
390+ minTTL: minTTL,
391+ parameters: parameters,
392+ headers: headers,
393+ forceRenewal: false ,
394+ callback: callback)
395+ return
396+ }
397+
317398 bioAuth. validateBiometric { error in
318399 guard error == nil else {
319400 return callback ( . failure( CredentialsManagerError ( code: . biometricsFailed, cause: error!) ) )
320401 }
321402
403+ // Update biometric session after successful authentication (only for session-based policies)
404+ self . updateBiometricSession ( for: bioAuth. policy)
405+
322406 self . retrieveCredentials ( scope: scope,
323407 minTTL: minTTL,
324408 parameters: parameters,
@@ -610,7 +694,7 @@ public struct CredentialsManager {
610694 callback: callback)
611695 }
612696
613- func store( apiCredentials: APICredentials , forAudience audience: String ) -> Bool {
697+ public func store( apiCredentials: APICredentials , forAudience audience: String ) -> Bool {
614698 guard let data = try ? apiCredentials. encode ( ) else {
615699 return false
616700 }
@@ -1504,5 +1588,21 @@ public extension CredentialsManager {
15041588 }
15051589 }
15061590
1591+ #if WEB_AUTH_PLATFORM
1592+ /// Updates the biometric session timestamp to the current time.
1593+ /// Only updates for session-based policies (Session and AppLifecycle).
1594+ private func updateBiometricSession( for policy: BiometricPolicy ) {
1595+ // Don't update session for "Always" policy
1596+ switch policy {
1597+ case . always:
1598+ return
1599+ case . session, . appLifecycle:
1600+ self . biometricSession. lock. lock ( )
1601+ defer { self . biometricSession. lock. unlock ( ) }
1602+ self . biometricSession. lastBiometricAuthTime = Date ( ) . timeIntervalSince1970
1603+ }
1604+ }
1605+ #endif
1606+
15071607}
15081608#endif
0 commit comments