Skip to content

Commit 4d9fe7b

Browse files
feat: Add configurable biometric authentication policies for CredentialsManager (#1019)
1 parent 3e33f6a commit 4d9fe7b

File tree

6 files changed

+653
-4
lines changed

6 files changed

+653
-4
lines changed

Auth0.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
5B7EE48D20FCA0F400367724 /* Auth0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F06DD851CC448C90011842B /* Auth0.framework */; };
3939
5B7EE48E20FCA0F400367724 /* Auth0.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 5F06DD851CC448C90011842B /* Auth0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4040
5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
41+
A4PF2C6UJAB9I6DXDJUTRG3B /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
4142
5B9262C31ECF0CC200F4F6D3 /* BioAuthenticationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */; };
4243
5BE65DCA1F7270DE00CADD3B /* Auth0.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 5F06DD781CC448B10011842B /* Auth0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
4344
5BEDE18A1EC21B040007300D /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1891EC21B040007300D /* CredentialsManager.swift */; };
@@ -124,6 +125,7 @@
124125
5C41F6C8244F969600252548 /* ASProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B16D88C1F7141A0009476A5 /* ASProvider.swift */; };
125126
5C41F6CA244F96AE00252548 /* LoginTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C41F6A3244DC94E00252548 /* LoginTransaction.swift */; };
126127
5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
128+
V17KI34D4PWP799FLD81GPVP /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
127129
5C41F6CC244F96F200252548 /* ClaimValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D7023D0BED200074024 /* ClaimValidators.swift */; };
128130
5C41F6CD244F96FD00252548 /* IDTokenSignatureValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3D23D0BA2C00074024 /* IDTokenSignatureValidator.swift */; };
129131
5C41F6CE244F970500252548 /* IDTokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D3E23D0BA2C00074024 /* IDTokenValidator.swift */; };
@@ -517,6 +519,7 @@
517519
C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB41D7023D0BED200074024 /* ClaimValidators.swift */; };
518520
C1B3B9FC2C24B6D4004A32A4 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B1748731EF2D3A40060E653 /* Shared.swift */; };
519521
C1B3B9FD2C24B6D4004A32A4 /* BioAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */; };
522+
2Z7FPPVSUUJRZC06V5RA8B2B /* BiometricPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */; };
520523
C1B3B9FE2C24B6D4004A32A4 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BEDE1891EC21B040007300D /* CredentialsManager.swift */; };
521524
C1B3B9FF2C24B6D4004A32A4 /* CredentialsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */; };
522525
C1B3BA002C24B6D4004A32A4 /* CredentialsManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */; };
@@ -860,6 +863,7 @@
860863
5B7EE48220FCA0A200367724 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
861864
5B7EE48420FCA0A200367724 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
862865
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BioAuthentication.swift; sourceTree = "<group>"; };
866+
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricPolicy.swift; sourceTree = "<group>"; };
863867
5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BioAuthenticationSpec.swift; path = Auth0Tests/BioAuthenticationSpec.swift; sourceTree = SOURCE_ROOT; };
864868
5BA58D33209081A700782DD1 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
865869
5BEDE1891EC21B040007300D /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
@@ -1025,7 +1029,6 @@
10251029
C107B5202CA27F76006B6BEA /* WebViewProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProviderSpec.swift; sourceTree = "<group>"; };
10261030
C160EE302CABD0DA005ACE8E /* UIWindow+TopViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+TopViewController.swift"; sourceTree = "<group>"; };
10271031
C160EE372CABD358005ACE8E /* UIWindow+TopViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+TopViewControllerSpec.swift"; sourceTree = "<group>"; };
1028-
C177D6C22C2ADDEB0094C657 /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Auth0.plist; path = ../Auth0.plist; sourceTree = "<group>"; };
10291032
C177D76F2C2BDFE40094C657 /* NetworkStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStub.swift; sourceTree = "<group>"; };
10301033
C177D7742C2BE00D0094C657 /* StubURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubURLProtocol.swift; sourceTree = "<group>"; };
10311034
C1B3B9A82C24B297004A32A4 /* OAuth2Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OAuth2Vision.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1048,6 +1051,7 @@
10481051
D4EDCFBF2E740295008E02F8 /* PreferredAuthenticationMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredAuthenticationMethod.swift; sourceTree = "<group>"; };
10491052
D581CF762757D773007327D1 /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
10501053
D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeGenerator.swift; sourceTree = "<group>"; };
1054+
C177D6C22C2ADDEB0094C657 /* Auth0.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Auth0.plist; path = ../Auth0.plist; sourceTree = "<group>"; };
10511055
/* End PBXFileReference section */
10521056

10531057
/* Begin PBXFrameworksBuildPhase section */
@@ -1211,6 +1215,7 @@
12111215
5CB41D3B23D0BA0300074024 /* Validators */,
12121216
5B1748731EF2D3A40060E653 /* Shared.swift */,
12131217
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */,
1218+
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */,
12141219
5BEDE1891EC21B040007300D /* CredentialsManager.swift */,
12151220
5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */,
12161221
5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */,
@@ -2481,6 +2486,7 @@
24812486
5FE2F8B21CCEAED8003628F4 /* Requestable.swift in Sources */,
24822487
5C4F551E23C8FB8E00C89615 /* Array+Encode.swift in Sources */,
24832488
5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */,
2489+
A4PF2C6UJAB9I6DXDJUTRG3B /* BiometricPolicy.swift in Sources */,
24842490
5CB41D7123D0BED200074024 /* ClaimValidators.swift in Sources */,
24852491
5FADB60C1CED7E0800D4BB50 /* UserPatchAttributes.swift in Sources */,
24862492
5FCAB1791D09124D00331C84 /* NSURL+Auth0.swift in Sources */,
@@ -2579,6 +2585,7 @@
25792585
5C505FAF2E216677005D0757 /* DPoPError.swift in Sources */,
25802586
5F74CB411CEFD5E600226823 /* JSONObjectPayload.swift in Sources */,
25812587
5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */,
2588+
V17KI34D4PWP799FLD81GPVP /* BiometricPolicy.swift in Sources */,
25822589
5C41F6C8244F969600252548 /* ASProvider.swift in Sources */,
25832590
5C38EA242DA4611B0085AC31 /* MyAccount.swift in Sources */,
25842591
5C41F6DF244FA1EE00252548 /* NSURLComponents+OAuth2.swift in Sources */,
@@ -2967,6 +2974,7 @@
29672974
C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */,
29682975
C1B3B9FC2C24B6D4004A32A4 /* Shared.swift in Sources */,
29692976
C1B3B9FD2C24B6D4004A32A4 /* BioAuthentication.swift in Sources */,
2977+
2Z7FPPVSUUJRZC06V5RA8B2B /* BiometricPolicy.swift in Sources */,
29702978
5C505FAE2E216677005D0757 /* DPoPError.swift in Sources */,
29712979
C1B3B9FE2C24B6D4004A32A4 /* CredentialsManager.swift in Sources */,
29722980
C1B3B9FF2C24B6D4004A32A4 /* CredentialsStorage.swift in Sources */,

Auth0/BioAuthentication.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ struct BioAuthentication {
88
private let evaluationPolicy: LAPolicy
99

1010
let title: String
11+
let policy: BiometricPolicy
12+
1113
var fallbackTitle: String? {
1214
get { return self.authContext.localizedFallbackTitle }
1315
set { self.authContext.localizedFallbackTitle = newValue }
@@ -22,10 +24,17 @@ struct BioAuthentication {
2224
return self.authContext.canEvaluatePolicy(evaluationPolicy, error: nil)
2325
}
2426

25-
init(authContext: LAContext, evaluationPolicy: LAPolicy, title: String, cancelTitle: String? = nil, fallbackTitle: String? = nil) {
27+
init(authContext: LAContext,
28+
evaluationPolicy: LAPolicy,
29+
title: String,
30+
cancelTitle: String? = nil,
31+
fallbackTitle: String? = nil,
32+
policy: BiometricPolicy = .always
33+
) {
2634
self.authContext = authContext
2735
self.evaluationPolicy = evaluationPolicy
2836
self.title = title
37+
self.policy = policy
2938
self.cancelTitle = cancelTitle
3039
self.fallbackTitle = fallbackTitle
3140
}

Auth0/BiometricPolicy.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#if WEB_AUTH_PLATFORM
2+
import Foundation
3+
4+
/// Defines the policy for when a biometric prompt should be shown when using the Credentials Manager.
5+
public enum BiometricPolicy {
6+
7+
/// Default behavior. A biometric prompt will be shown for every call to `credentials()`.
8+
case always
9+
10+
/// A biometric prompt will be shown only once within the specified timeout period.
11+
/// - Parameter timeoutInSeconds: The duration for which the session remains valid.
12+
case session(timeoutInSeconds: Int)
13+
14+
/// A biometric prompt will be shown only once while the app is in the foreground.
15+
/// The session is invalidated by calling `clearBiometricSession()` or after the default timeout.
16+
/// - Parameter timeoutInSeconds: The duration for which the session remains valid. Defaults to 3600 seconds (1 hour).
17+
case appLifecycle(timeoutInSeconds: Int = 3600)
18+
}
19+
#endif

Auth0/CredentialsManager.swift

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,
@@ -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

Comments
 (0)