Skip to content

Commit f814a9a

Browse files
feat: add BiometricPolicy to manage biometric authentication requirements and update CredentialsManager to support it
1 parent 1a8c782 commit f814a9a

File tree

6 files changed

+641
-5
lines changed

6 files changed

+641
-5
lines changed

Auth0.xcodeproj/project.pbxproj

Lines changed: 12 additions & 2 deletions
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 */; };
@@ -679,6 +682,7 @@
679682
D581CF792757D773007327D1 /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D581CF762757D773007327D1 /* RequestSpec.swift */; };
680683
D5E9E317273ACCA5000CDB0A /* ChallengeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */; };
681684
D5E9E318273ACCA5000CDB0A /* ChallengeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */; };
685+
E929B27B2EBDCEC70071DC9F /* Auth0.plist in Resources */ = {isa = PBXBuildFile; fileRef = E929B27A2EBDCEC70071DC9F /* Auth0.plist */; };
682686
/* End PBXBuildFile section */
683687

684688
/* Begin PBXContainerItemProxy section */
@@ -860,6 +864,7 @@
860864
5B7EE48220FCA0A200367724 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
861865
5B7EE48420FCA0A200367724 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
862866
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BioAuthentication.swift; sourceTree = "<group>"; };
867+
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricPolicy.swift; sourceTree = "<group>"; };
863868
5B9262C11ECF0CBA00F4F6D3 /* BioAuthenticationSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BioAuthenticationSpec.swift; path = Auth0Tests/BioAuthenticationSpec.swift; sourceTree = SOURCE_ROOT; };
864869
5BA58D33209081A700782DD1 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
865870
5BEDE1891EC21B040007300D /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = "<group>"; };
@@ -1025,7 +1030,6 @@
10251030
C107B5202CA27F76006B6BEA /* WebViewProviderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProviderSpec.swift; sourceTree = "<group>"; };
10261031
C160EE302CABD0DA005ACE8E /* UIWindow+TopViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+TopViewController.swift"; sourceTree = "<group>"; };
10271032
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>"; };
10291033
C177D76F2C2BDFE40094C657 /* NetworkStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStub.swift; sourceTree = "<group>"; };
10301034
C177D7742C2BE00D0094C657 /* StubURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubURLProtocol.swift; sourceTree = "<group>"; };
10311035
C1B3B9A82C24B297004A32A4 /* OAuth2Vision.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OAuth2Vision.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1048,6 +1052,7 @@
10481052
D4EDCFBF2E740295008E02F8 /* PreferredAuthenticationMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredAuthenticationMethod.swift; sourceTree = "<group>"; };
10491053
D581CF762757D773007327D1 /* RequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestSpec.swift; sourceTree = "<group>"; };
10501054
D5E9E316273ACCA5000CDB0A /* ChallengeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeGenerator.swift; sourceTree = "<group>"; };
1055+
E929B27A2EBDCEC70071DC9F /* Auth0.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Auth0.plist; sourceTree = "<group>"; };
10511056
/* End PBXFileReference section */
10521057

10531058
/* Begin PBXFrameworksBuildPhase section */
@@ -1211,6 +1216,7 @@
12111216
5CB41D3B23D0BA0300074024 /* Validators */,
12121217
5B1748731EF2D3A40060E653 /* Shared.swift */,
12131218
5B9262BF1ECF0CA800F4F6D3 /* BioAuthentication.swift */,
1219+
KDG54I4VDN4TZII38JVUS4AP /* BiometricPolicy.swift */,
12141220
5BEDE1891EC21B040007300D /* CredentialsManager.swift */,
12151221
5C80980A275A7B8600DC0A76 /* CredentialsStorage.swift */,
12161222
5B5E93F81EC45C22002A37F9 /* CredentialsManagerError.swift */,
@@ -1492,7 +1498,7 @@
14921498
5F3965D01CF67DD800CDE7C0 /* Assets.xcassets */,
14931499
5F3965D21CF67DD800CDE7C0 /* LaunchScreen.storyboard */,
14941500
5F3965CD1CF67DD800CDE7C0 /* Main.storyboard */,
1495-
C177D6C22C2ADDEB0094C657 /* Auth0.plist */,
1501+
E929B27A2EBDCEC70071DC9F /* Auth0.plist */,
14961502
5F3965D51CF67DD800CDE7C0 /* Info.plist */,
14971503
);
14981504
path = App;
@@ -2212,6 +2218,7 @@
22122218
5F3965D41CF67DD800CDE7C0 /* LaunchScreen.storyboard in Resources */,
22132219
5F3965D11CF67DD800CDE7C0 /* Assets.xcassets in Resources */,
22142220
5F3965CF1CF67DD800CDE7C0 /* Main.storyboard in Resources */,
2221+
E929B27B2EBDCEC70071DC9F /* Auth0.plist in Resources */,
22152222
);
22162223
runOnlyForDeploymentPostprocessing = 0;
22172224
};
@@ -2481,6 +2488,7 @@
24812488
5FE2F8B21CCEAED8003628F4 /* Requestable.swift in Sources */,
24822489
5C4F551E23C8FB8E00C89615 /* Array+Encode.swift in Sources */,
24832490
5B9262C01ECF0CA800F4F6D3 /* BioAuthentication.swift in Sources */,
2491+
A4PF2C6UJAB9I6DXDJUTRG3B /* BiometricPolicy.swift in Sources */,
24842492
5CB41D7123D0BED200074024 /* ClaimValidators.swift in Sources */,
24852493
5FADB60C1CED7E0800D4BB50 /* UserPatchAttributes.swift in Sources */,
24862494
5FCAB1791D09124D00331C84 /* NSURL+Auth0.swift in Sources */,
@@ -2579,6 +2587,7 @@
25792587
5C505FAF2E216677005D0757 /* DPoPError.swift in Sources */,
25802588
5F74CB411CEFD5E600226823 /* JSONObjectPayload.swift in Sources */,
25812589
5C41F6CB244F96E300252548 /* BioAuthentication.swift in Sources */,
2590+
V17KI34D4PWP799FLD81GPVP /* BiometricPolicy.swift in Sources */,
25822591
5C41F6C8244F969600252548 /* ASProvider.swift in Sources */,
25832592
5C38EA242DA4611B0085AC31 /* MyAccount.swift in Sources */,
25842593
5C41F6DF244FA1EE00252548 /* NSURLComponents+OAuth2.swift in Sources */,
@@ -2967,6 +2976,7 @@
29672976
C1B3B9FB2C24B6D4004A32A4 /* ClaimValidators.swift in Sources */,
29682977
C1B3B9FC2C24B6D4004A32A4 /* Shared.swift in Sources */,
29692978
C1B3B9FD2C24B6D4004A32A4 /* BioAuthentication.swift in Sources */,
2979+
2Z7FPPVSUUJRZC06V5RA8B2B /* BiometricPolicy.swift in Sources */,
29702980
5C505FAE2E216677005D0757 /* DPoPError.swift in Sources */,
29712981
C1B3B9FE2C24B6D4004A32A4 /* CredentialsManager.swift in Sources */,
29722982
C1B3B9FF2C24B6D4004A32A4 /* CredentialsStorage.swift in Sources */,

Auth0/BioAuthentication.swift

Lines changed: 4 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,11 @@ 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, evaluationPolicy: LAPolicy, title: String, cancelTitle: String? = nil, fallbackTitle: String? = nil, policy: BiometricPolicy = .always) {
2628
self.authContext = authContext
2729
self.evaluationPolicy = evaluationPolicy
2830
self.title = title
31+
self.policy = policy
2932
self.cancelTitle = cancelTitle
3033
self.fallbackTitle = fallbackTitle
3134
}

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: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ 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
36+
private static let noSession: TimeInterval = -1
37+
private static var lastBiometricAuthTime: TimeInterval = noSession
38+
private static let sessionLock = NSLock()
3539
#endif
3640

3741
/// Creates a new `CredentialsManager` instance.
@@ -82,17 +86,27 @@ public struct CredentialsManager {
8286
/// evaluationPolicy: .deviceOwnerAuthentication)
8387
/// ```
8488
///
89+
/// You can also configure a ``BiometricPolicy`` to control when biometric authentication is required:
90+
///
91+
/// ```swift
92+
/// // Require authentication only once per 5-minute session
93+
/// credentialsManager.enableBiometrics(withTitle: "Unlock with Face ID",
94+
/// policy: .session(timeoutInSeconds: 300))
95+
/// ```
96+
///
8597
/// - Parameters:
8698
/// - title: Main message to display when Face ID or Touch ID is used.
8799
/// - cancelTitle: Cancel message to display when Face ID or Touch ID is used.
88100
/// - fallbackTitle: Fallback message to display when Face ID or Touch ID is used after a failed match.
89101
/// - evaluationPolicy: Policy to be used for authentication policy evaluation.
102+
/// - policy: The ``BiometricPolicy`` that controls when biometric authentication is required. Defaults to `.always`.
90103
/// - Important: Access to the ``user`` property will not be protected by biometric authentication.
91104
public mutating func enableBiometrics(withTitle title: String,
92105
cancelTitle: String? = nil,
93106
fallbackTitle: String? = nil,
94-
evaluationPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics) {
95-
self.bioAuth = BioAuthentication(authContext: LAContext(), evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle)
107+
evaluationPolicy: LAPolicy = .deviceOwnerAuthenticationWithBiometrics,
108+
policy: BiometricPolicy = .always) {
109+
self.bioAuth = BioAuthentication(authContext: LAContext(), evaluationPolicy: evaluationPolicy, title: title, cancelTitle: cancelTitle, fallbackTitle: fallbackTitle, policy: policy)
96110
}
97111
#endif
98112

@@ -125,6 +139,9 @@ public struct CredentialsManager {
125139
///
126140
/// - Returns: If the credentials were removed.
127141
public func clear() -> Bool {
142+
#if WEB_AUTH_PLATFORM
143+
Self.clearBiometricSession()
144+
#endif
128145
return self.storage.deleteEntry(forKey: self.storeKey)
129146
}
130147

@@ -142,6 +159,49 @@ public struct CredentialsManager {
142159
return self.storage.deleteEntry(forKey: audience)
143160
}
144161

162+
#if WEB_AUTH_PLATFORM
163+
/// Checks if the current biometric session is valid based on the configured policy.
164+
///
165+
/// ## Usage
166+
///
167+
/// ```swift
168+
/// let isValid = credentialsManager.isBiometricSessionValid()
169+
/// ```
170+
///
171+
/// - Returns: `true` if the session is valid and biometric authentication can be skipped, `false` otherwise.
172+
public func isBiometricSessionValid() -> Bool {
173+
guard let bioAuth = self.bioAuth else { return false }
174+
175+
Self.sessionLock.lock()
176+
defer { Self.sessionLock.unlock() }
177+
178+
let lastAuth = Self.lastBiometricAuthTime
179+
if lastAuth == Self.noSession { return false }
180+
181+
switch bioAuth.policy {
182+
case .session(let timeoutInSeconds), .appLifecycle(let timeoutInSeconds):
183+
let timeoutInterval = TimeInterval(timeoutInSeconds)
184+
return Date().timeIntervalSince1970 - lastAuth < timeoutInterval
185+
case .always:
186+
return false
187+
}
188+
}
189+
190+
/// Clears the in-memory biometric session timestamp. This will force biometric authentication on the next
191+
/// credential access.
192+
///
193+
/// ## Usage
194+
///
195+
/// ```swift
196+
/// CredentialsManager.clearBiometricSession()
197+
/// ```
198+
public static func clearBiometricSession() {
199+
sessionLock.lock()
200+
defer { sessionLock.unlock() }
201+
lastBiometricAuthTime = noSession
202+
}
203+
#endif
204+
145205
/// Calls the `/oauth/revoke` endpoint to revoke the refresh token and then clears the credentials if the request
146206
/// was successful. Otherwise, the credentials will not be cleared and the callback will be called with a failure
147207
/// result containing a ``CredentialsManagerError/revokeFailed`` error.
@@ -314,11 +374,26 @@ public struct CredentialsManager {
314374
return callback(.failure(error))
315375
}
316376

377+
// Check if biometric session is valid based on policy
378+
if self.isBiometricSessionValid() {
379+
// Session is valid, bypass biometric prompt
380+
self.retrieveCredentials(scope: scope,
381+
minTTL: minTTL,
382+
parameters: parameters,
383+
headers: headers,
384+
forceRenewal: false,
385+
callback: callback)
386+
return
387+
}
388+
317389
bioAuth.validateBiometric { error in
318390
guard error == nil else {
319391
return callback(.failure(CredentialsManagerError(code: .biometricsFailed, cause: error!)))
320392
}
321393

394+
// Update biometric session after successful authentication (only for session-based policies)
395+
Self.updateBiometricSession(for: bioAuth.policy)
396+
322397
self.retrieveCredentials(scope: scope,
323398
minTTL: minTTL,
324399
parameters: parameters,
@@ -1504,5 +1579,21 @@ public extension CredentialsManager {
15041579
}
15051580
}
15061581

1582+
#if WEB_AUTH_PLATFORM
1583+
/// Updates the biometric session timestamp to the current time.
1584+
/// Only updates for session-based policies (Session and AppLifecycle).
1585+
private static func updateBiometricSession(for policy: BiometricPolicy) {
1586+
// Don't update session for "Always" policy
1587+
switch policy {
1588+
case .always:
1589+
return
1590+
case .session, .appLifecycle:
1591+
sessionLock.lock()
1592+
defer { sessionLock.unlock() }
1593+
lastBiometricAuthTime = Date().timeIntervalSince1970
1594+
}
1595+
}
1596+
#endif
1597+
15071598
}
15081599
#endif

0 commit comments

Comments
 (0)