Skip to content

Commit 55e8d3a

Browse files
committed
Added dpop validation check before token refresh
1 parent 7e6b32e commit 55e8d3a

3 files changed

Lines changed: 80 additions & 0 deletions

File tree

auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,63 @@ public abstract class BaseCredentialsManager internal constructor(
194194
}
195195
}
196196

197+
/**
198+
* Validates DPoP key/token alignment before attempting a refresh.
199+
*
200+
* Uses two signals to detect DPoP-bound credentials:
201+
* - tokenType == "DPoP"
202+
* - KEY_DPOP_THUMBPRINT exists
203+
*
204+
* @param tokenType the token_type value from storage (or decrypted credentials for migration)
205+
* @return null if validation passes, or a CredentialsManagerException if it fails
206+
*/
207+
protected fun validateDPoPState(tokenType: String?): CredentialsManagerException? {
208+
val storedThumbprint = storage.retrieveString(KEY_DPOP_THUMBPRINT)
209+
val isDPoPBound = (tokenType?.equals("DPoP", ignoreCase = true) == true)
210+
|| (storedThumbprint != null)
211+
if (!isDPoPBound) return null
212+
213+
// Check 1: Does the DPoP key still exist in KeyStore?
214+
val hasKey = try {
215+
DPoPUtil.hasKeyPair()
216+
} catch (e: DPoPException) {
217+
Log.e(this::class.java.simpleName, "Failed to check DPoP key existence", e)
218+
false
219+
}
220+
if (!hasKey) {
221+
Log.w(this::class.java.simpleName, "DPoP key missing from KeyStore. Clearing stale credentials.")
222+
clearCredentials()
223+
return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING)
224+
}
225+
226+
// Check 2: Is the AuthenticationAPIClient configured with DPoP?
227+
if (!authenticationClient.isDPoPEnabled) {
228+
return CredentialsManagerException(CredentialsManagerException.Code.DPOP_NOT_CONFIGURED)
229+
}
230+
231+
// Check 3: Does the current key match the one used when credentials were saved?
232+
val currentThumbprint = try {
233+
DPoPUtil.getPublicKeyJWK()
234+
} catch (e: DPoPException) {
235+
Log.e(this::class.java.simpleName, "Failed to read DPoP key thumbprint", e)
236+
null
237+
}
238+
239+
if (storedThumbprint != null) {
240+
if (currentThumbprint != storedThumbprint) {
241+
Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. Clearing stale credentials.")
242+
clearCredentials()
243+
return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING)
244+
}
245+
} else if (currentThumbprint != null) {
246+
// Migration: existing DPoP user upgraded — no thumbprint stored yet.
247+
// Backfill so future checks can detect key rotation.
248+
storage.store(KEY_DPOP_THUMBPRINT, currentThumbprint)
249+
}
250+
251+
return null
252+
}
253+
197254
/**
198255
* Checks if the stored scope is the same as the requested one.
199256
*

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
483483
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
484484
return@execute
485485
}
486+
validateDPoPState(tokenType)?.let { dpopError ->
487+
callback.onFailure(dpopError)
488+
return@execute
489+
}
486490
val request = authenticationClient.renewAuth(refreshToken)
487491
request.addParameters(parameters)
488492
if (scope != null) {
@@ -593,8 +597,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
593597
//Check if existing api credentials are present and valid
594598
val key = getAPICredentialsKey(audience, scope)
595599
val apiCredentialsJson = storage.retrieveString(key)
600+
var apiCredentialType: String? = null
596601
apiCredentialsJson?.let {
597602
val apiCredentials = gson.fromJson(it, APICredentials::class.java)
603+
apiCredentialType = apiCredentials.type
598604
val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong())
599605

600606
val scopeChanged = hasScopeChanged(
@@ -617,6 +623,11 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
617623
return@execute
618624
}
619625

626+
validateDPoPState(apiCredentialType)?.let { dpopError ->
627+
callback.onFailure(dpopError)
628+
return@execute
629+
}
630+
620631
val request = authenticationClient.renewAuth(refreshToken, audience, scope)
621632
request.addParameters(parameters)
622633

auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
848848
callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN)
849849
return@execute
850850
}
851+
val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) ?: credentials.type
852+
validateDPoPState(tokenType)?.let { dpopError ->
853+
callback.onFailure(dpopError)
854+
return@execute
855+
}
851856
Log.d(TAG, "Credentials have expired. Renewing them now...")
852857
val request = authenticationClient.renewAuth(
853858
credentials.refreshToken
@@ -963,6 +968,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
963968
val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope))
964969
//Check if existing api credentials are present and valid
965970

971+
var apiCredentialType: String? = null
966972
encryptedEncodedJson?.let { encryptedEncoded ->
967973
val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT)
968974
val json: String = try {
@@ -987,6 +993,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
987993
}
988994

989995
val apiCredentials = gson.fromJson(json, APICredentials::class.java)
996+
apiCredentialType = apiCredentials.type
990997

991998
val expiresAt = apiCredentials.expiresAt.time
992999
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
@@ -1014,6 +1021,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
10141021
return@execute
10151022
}
10161023

1024+
validateDPoPState(apiCredentialType)?.let { dpopError ->
1025+
callback.onFailure(dpopError)
1026+
return@execute
1027+
}
1028+
10171029
val request = authenticationClient.renewAuth(refreshToken, audience, scope)
10181030
request.addParameters(parameters)
10191031
for (header in headers) {

0 commit comments

Comments
 (0)