@@ -37,6 +37,7 @@ public struct CredentialsManager: Sendable {
3737 private let dpopThumbprintKey : String
3838 private let authentication : Authentication
3939 private let maxRetries : Int
40+ private let dpopValidator : DPoPCredentialValidator
4041 #if WEB_AUTH_PLATFORM
4142 var bioAuth : BioAuthentication ?
4243 // Biometric session management - using a class to allow mutation in non-mutating methods
@@ -71,6 +72,9 @@ public struct CredentialsManager: Sendable {
7172 self . authentication = authentication
7273 self . sendableStorage = SendableBox ( value: storage)
7374 self . maxRetries = max ( 0 , maxRetries)
75+ self . dpopValidator = DPoPCredentialValidator ( authentication: authentication,
76+ storage: storage,
77+ thumbprintKey: dpopThumbprintKey)
7478 }
7579
7680 /// Retrieves the user information from the Keychain synchronously, without checking if the credentials are expired.
@@ -153,7 +157,7 @@ public struct CredentialsManager: Sendable {
153157 let data = try NSKeyedArchiver . archivedData ( withRootObject: credentials,
154158 requiringSecureCoding: true )
155159 try self . storage. setEntry ( data, forKey: self . storeKey)
156- try ? saveDPoPThumbprint ( for: credentials)
160+ dpopValidator . saveThumbprint ( for: credentials)
157161 }
158162
159163 /// Clears credentials stored in the Keychain.
@@ -172,7 +176,7 @@ public struct CredentialsManager: Sendable {
172176 self . biometricSession. lock. unlock ( )
173177 #endif
174178 try self . storage. deleteEntry ( forKey: self . storeKey)
175- try ? self . storage . deleteEntry ( forKey : self . dpopThumbprintKey )
179+ dpopValidator . clearThumbprint ( )
176180 }
177181
178182 /// Clears API credentials stored in the Keychain for a given audience value.
@@ -772,55 +776,6 @@ public struct CredentialsManager: Sendable {
772776 return try NSKeyedUnarchiver . unarchivedObject ( ofClass: Credentials . self, from: data)
773777 }
774778
775- private func validateDPoPState( for credentials: Credentials ) throws {
776- let storedThumbprint = try ? self . storage. getEntry ( forKey: self . dpopThumbprintKey)
777- let storedThumbPrintValue = storedThumbprint. flatMap { String ( data: $0, encoding: . utf8) }
778-
779- let isDPoPBound = credentials. tokenType. caseInsensitiveCompare ( " DPoP " ) == . orderedSame
780- || storedThumbPrintValue != nil
781-
782- guard isDPoPBound else { return }
783-
784- guard let dpop = self . authentication. dpop else {
785- throw CredentialsManagerError . dpopNotConfigured
786- }
787-
788- let hasKeyPair = try ? dpop. hasKeypair ( )
789-
790- guard hasKeyPair == true else {
791- try self . clear ( )
792- throw CredentialsManagerError . dpopKeyMissing
793- }
794-
795- // Hash the current thumbprint to compare against the stored hash
796- let currentThumbprint = try dpop. jkt ( )
797- if let stored = storedThumbPrintValue {
798- if stored != currentThumbprint {
799- try self . clear ( )
800- throw CredentialsManagerError . dpopKeyMismatch
801- }
802- } else {
803- try self . storage. setEntry ( Data ( currentThumbprint. utf8) , forKey: dpopThumbprintKey)
804- }
805- }
806-
807- private func saveDPoPThumbprint( for credentials: Credentials ) throws {
808- // token type must be DPoP and authentication must have non nil dpop property
809- guard credentials. tokenType. caseInsensitiveCompare ( " DPoP " ) == . orderedSame ||
810- self . authentication. dpop != nil else {
811- try self . storage. deleteEntry ( forKey: self . dpopThumbprintKey)
812- return
813- }
814-
815- if let dpop = authentication. dpop,
816- let thumbprint = try ? dpop. jkt ( ) {
817- // Store a SHA-256 hash of the thumbprint to avoid persisting the raw key thumbprint in device storage
818- try self . storage. setEntry ( Data ( thumbprint. utf8) , forKey: self . dpopThumbprintKey)
819- } else {
820- try self . storage. deleteEntry ( forKey: self . dpopThumbprintKey)
821- }
822- }
823-
824779 private func retrieveAPICredentials( audience: String , scope: String ? ) throws -> APICredentials ? {
825780 let key = getAPICredentialsStorageKey ( audience: audience, scope: scope)
826781 let data = try self . storage. getEntry ( forKey: key)
@@ -883,7 +838,7 @@ public struct CredentialsManager: Sendable {
883838 return callback ( . failure( . noRefreshToken) )
884839 }
885840
886- try self . validateDPoPState ( for: credentials)
841+ try self . dpopValidator . validate ( for: credentials)
887842
888843 self . authentication
889844 . renew ( withRefreshToken: refreshToken, scope: scope)
@@ -958,7 +913,7 @@ public struct CredentialsManager: Sendable {
958913 complete ( )
959914 return callback ( . failure( . noRefreshToken) )
960915 }
961- try self . validateDPoPState ( for: credentials)
916+ try self . dpopValidator . validate ( for: credentials)
962917
963918 self . authentication
964919 . ssoExchange ( withRefreshToken: refreshToken)
@@ -1018,7 +973,7 @@ public struct CredentialsManager: Sendable {
1018973 complete ( )
1019974 return callback ( . failure( . noRefreshToken) )
1020975 }
1021- try self . validateDPoPState ( for: currentCredentials)
976+ try self . dpopValidator . validate ( for: currentCredentials)
1022977
1023978 self . authentication
1024979 . renew ( withRefreshToken: refreshToken, audience: audience, scope: scope)
0 commit comments