From e2c65d4092eda6d580f05d688c9ed6921697ecec Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Mon, 23 Mar 2026 14:27:01 +0530 Subject: [PATCH 01/12] Added two new exceptions DPOP_KEY_MISSING and DPOP_NOT_CONFIGURED to CredentialsManager class --- .../authentication/AuthenticationAPIClient.kt | 7 ++++++ .../storage/BaseCredentialsManager.kt | 4 ++++ .../storage/CredentialsManagerException.kt | 9 ++++++++ .../main/java/com/auth0/android/dpop/DPoP.kt | 23 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt index 572ecc513..f33aee9ca 100755 --- a/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt @@ -55,6 +55,13 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe private var dPoP: DPoP? = null + /** + * Returns whether DPoP (Demonstrating Proof of Possession) is enabled on this client. + * DPoP is enabled by calling [useDPoP]. + */ + public val isDPoPEnabled: Boolean + get() = dPoP != null + /** * Creates a new API client instance providing Auth0 account info. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index d3ac32d59..c7237586a 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -20,6 +20,10 @@ public abstract class BaseCredentialsManager internal constructor( protected val storage: Storage, private val jwtDecoder: JWTDecoder ) { + + internal companion object { + internal const val KEY_DPOP_THUMBPRINT = "com.auth0.dpop_key_thumbprint" + } private var _clock: Clock = ClockImpl() /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index 9796dbe64..eb52a4543 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -48,6 +48,8 @@ public class CredentialsManagerException : API_ERROR, SSO_EXCHANGE_FAILED, MFA_REQUIRED, + DPOP_KEY_MISSING, + DPOP_NOT_CONFIGURED, UNKNOWN_ERROR } @@ -159,6 +161,11 @@ public class CredentialsManagerException : public val MFA_REQUIRED: CredentialsManagerException = CredentialsManagerException(Code.MFA_REQUIRED) + public val DPOP_KEY_MISSING: CredentialsManagerException = + CredentialsManagerException(Code.DPOP_KEY_MISSING) + public val DPOP_NOT_CONFIGURED: CredentialsManagerException = + CredentialsManagerException(Code.DPOP_NOT_CONFIGURED) + public val UNKNOWN_ERROR: CredentialsManagerException = CredentialsManagerException(Code.UNKNOWN_ERROR) @@ -207,6 +214,8 @@ public class CredentialsManagerException : Code.API_ERROR -> "An error occurred while processing the request." Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed." Code.MFA_REQUIRED -> "Multi-factor authentication is required to complete the credential renewal." + Code.DPOP_KEY_MISSING -> "The stored credentials are DPoP-bound but the DPoP key pair is no longer available in the Android KeyStore. This can happen after a device backup/restore, factory reset, or biometric enrollment change. Re-authentication is required." + Code.DPOP_NOT_CONFIGURED -> "The stored credentials are DPoP-bound but the AuthenticationAPIClient used by this CredentialsManager was not configured with useDPoP(context). Call AuthenticationAPIClient(auth0).useDPoP(context) and pass the configured client to CredentialsManager." Code.UNKNOWN_ERROR -> "An unknown error has occurred while fetching the token. Please check the error cause for more details." } } diff --git a/auth0/src/main/java/com/auth0/android/dpop/DPoP.kt b/auth0/src/main/java/com/auth0/android/dpop/DPoP.kt index d84f7f610..b0490575e 100644 --- a/auth0/src/main/java/com/auth0/android/dpop/DPoP.kt +++ b/auth0/src/main/java/com/auth0/android/dpop/DPoP.kt @@ -198,6 +198,29 @@ public class DPoP(context: Context) { return HeaderData(token, proof) } + /** + * Returns whether a DPoP key pair currently exists in the Android KeyStore. + * + * This can be used to check if DPoP credentials are still available after events + * like device backup/restore or factory reset, which do not preserve KeyStore entries. + * + * ```kotlin + * + * if (!DPoP.hasKeyPair()) { + * // Key was lost — clear stored credentials and re-authenticate + * } + * + * ``` + * + * @return true if a DPoP key pair exists in the KeyStore, false otherwise. + * @throws DPoPException if there is an error accessing the KeyStore. + */ + @Throws(DPoPException::class) + @JvmStatic + public fun hasKeyPair(): Boolean { + return DPoPUtil.hasKeyPair() + } + /** * Method to clear the DPoP key pair from the keystore. It must be called when the user logs out * to prevent reuse of the key pair in subsequent sessions. From 7145784497532e2d50ed7a5cb5baaedee4728057 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Mon, 23 Mar 2026 16:45:56 +0530 Subject: [PATCH 02/12] Saving the Dpop thumbprint --- .../storage/BaseCredentialsManager.kt | 25 +++++++++++++++++++ .../storage/CredentialsManager.kt | 2 ++ .../storage/SecureCredentialsManager.kt | 7 ++++++ 3 files changed, 34 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index c7237586a..e6be3340d 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -4,6 +4,8 @@ import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback +import com.auth0.android.dpop.DPoPException +import com.auth0.android.dpop.DPoPUtil import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials import com.auth0.android.result.SSOCredentials @@ -159,6 +161,29 @@ public abstract class BaseCredentialsManager internal constructor( internal val currentTimeInMillis: Long get() = _clock.getCurrentTimeMillis() + /** + * Stores the DPoP key thumbprint if DPoP was used for this credential set. + * Uses a dual strategy to store the thumbprint: + * - credentials.type == "DPoP" when server confirms DPoP but client lacks useDPoP() + * - isDPoPEnabled catches the case where client used DPoP, server returned token_type: "Bearer" + */ + protected fun saveDPoPThumbprint(credentials: Credentials) { + val dpopUsed = credentials.type.equals("DPoP", ignoreCase = true) + || authenticationClient.isDPoPEnabled + if (dpopUsed && DPoPUtil.hasKeyPair()) { + try { + val thumbprint = DPoPUtil.getPublicKeyJWK() + if (thumbprint != null) { + storage.store(KEY_DPOP_THUMBPRINT, thumbprint) + } + } catch (e: DPoPException) { + Log.w(this::class.java.simpleName, "Failed to store DPoP key thumbprint", e) + } + } else { + storage.remove(KEY_DPOP_THUMBPRINT) + } + } + /** * Checks if the stored scope is the same as the requested one. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 0cc5c61fa..a658f9b83 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -75,6 +75,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage.store(KEY_EXPIRES_AT, credentials.expiresAt.time) storage.store(KEY_SCOPE, credentials.scope) storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time) + saveDPoPThumbprint(credentials) } /** @@ -714,6 +715,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage.remove(KEY_EXPIRES_AT) storage.remove(KEY_SCOPE) storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) + storage.remove(KEY_DPOP_THUMBPRINT) } /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 4a367e778..41c05447c 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -189,6 +189,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time) storage.store(KEY_CAN_REFRESH, canRefresh) + storage.store(KEY_TOKEN_TYPE, credentials.type) + saveDPoPThumbprint(credentials) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e @@ -735,6 +737,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage.remove(KEY_EXPIRES_AT) storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) storage.remove(KEY_CAN_REFRESH) + storage.remove(KEY_TOKEN_TYPE) + storage.remove(KEY_DPOP_THUMBPRINT) clearBiometricSession() Log.d(TAG, "Credentials were just removed from the storage") } @@ -1251,6 +1255,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val KEY_ALIAS = "com.auth0.key" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_TOKEN_TYPE = "com.auth0.token_type" + // Using NO_SESSION to represent "no session" (uninitialized state) private const val NO_SESSION = -1L } From dac4dce02b5f0f3a4a2ce3cbcaf89f02245862d1 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Mon, 23 Mar 2026 16:45:56 +0530 Subject: [PATCH 03/12] Saving the Dpop thumbprint --- .../storage/BaseCredentialsManager.kt | 25 +++++++++++++++++++ .../storage/CredentialsManager.kt | 2 ++ .../storage/SecureCredentialsManager.kt | 7 ++++++ 3 files changed, 34 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index c7237586a..e6be3340d 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -4,6 +4,8 @@ import android.util.Log import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.callback.Callback +import com.auth0.android.dpop.DPoPException +import com.auth0.android.dpop.DPoPUtil import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials import com.auth0.android.result.SSOCredentials @@ -159,6 +161,29 @@ public abstract class BaseCredentialsManager internal constructor( internal val currentTimeInMillis: Long get() = _clock.getCurrentTimeMillis() + /** + * Stores the DPoP key thumbprint if DPoP was used for this credential set. + * Uses a dual strategy to store the thumbprint: + * - credentials.type == "DPoP" when server confirms DPoP but client lacks useDPoP() + * - isDPoPEnabled catches the case where client used DPoP, server returned token_type: "Bearer" + */ + protected fun saveDPoPThumbprint(credentials: Credentials) { + val dpopUsed = credentials.type.equals("DPoP", ignoreCase = true) + || authenticationClient.isDPoPEnabled + if (dpopUsed && DPoPUtil.hasKeyPair()) { + try { + val thumbprint = DPoPUtil.getPublicKeyJWK() + if (thumbprint != null) { + storage.store(KEY_DPOP_THUMBPRINT, thumbprint) + } + } catch (e: DPoPException) { + Log.w(this::class.java.simpleName, "Failed to store DPoP key thumbprint", e) + } + } else { + storage.remove(KEY_DPOP_THUMBPRINT) + } + } + /** * Checks if the stored scope is the same as the requested one. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 0cc5c61fa..a658f9b83 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -75,6 +75,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage.store(KEY_EXPIRES_AT, credentials.expiresAt.time) storage.store(KEY_SCOPE, credentials.scope) storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time) + saveDPoPThumbprint(credentials) } /** @@ -714,6 +715,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage.remove(KEY_EXPIRES_AT) storage.remove(KEY_SCOPE) storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) + storage.remove(KEY_DPOP_THUMBPRINT) } /** diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 4a367e778..41c05447c 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -189,6 +189,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT ) storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time) storage.store(KEY_CAN_REFRESH, canRefresh) + storage.store(KEY_TOKEN_TYPE, credentials.type) + saveDPoPThumbprint(credentials) } catch (e: IncompatibleDeviceException) { throw CredentialsManagerException( CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e @@ -735,6 +737,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT storage.remove(KEY_EXPIRES_AT) storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT) storage.remove(KEY_CAN_REFRESH) + storage.remove(KEY_TOKEN_TYPE) + storage.remove(KEY_DPOP_THUMBPRINT) clearBiometricSession() Log.d(TAG, "Credentials were just removed from the storage") } @@ -1251,6 +1255,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val KEY_ALIAS = "com.auth0.key" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_TOKEN_TYPE = "com.auth0.token_type" + // Using NO_SESSION to represent "no session" (uninitialized state) private const val NO_SESSION = -1L } From 5875c0ce2507e6872c14ea4ea5871d2997ef5a7c Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 24 Mar 2026 15:15:16 +0530 Subject: [PATCH 04/12] Addressed review comments --- .../storage/BaseCredentialsManager.kt | 24 ++++++++++++------- .../storage/CredentialsManagerException.kt | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index e6be3340d..0d34432f3 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -170,15 +170,21 @@ public abstract class BaseCredentialsManager internal constructor( protected fun saveDPoPThumbprint(credentials: Credentials) { val dpopUsed = credentials.type.equals("DPoP", ignoreCase = true) || authenticationClient.isDPoPEnabled - if (dpopUsed && DPoPUtil.hasKeyPair()) { - try { - val thumbprint = DPoPUtil.getPublicKeyJWK() - if (thumbprint != null) { - storage.store(KEY_DPOP_THUMBPRINT, thumbprint) - } - } catch (e: DPoPException) { - Log.w(this::class.java.simpleName, "Failed to store DPoP key thumbprint", e) - } + + if (!dpopUsed) { + storage.remove(KEY_DPOP_THUMBPRINT) + return + } + + val thumbprint = try { + if (DPoPUtil.hasKeyPair()) DPoPUtil.getPublicKeyJWK() else null + } catch (e: DPoPException) { + Log.w(this::class.java.simpleName, "Failed to fetch DPoP key thumbprint", e) + null + } + + if (thumbprint != null) { + storage.store(KEY_DPOP_THUMBPRINT, thumbprint) } else { storage.remove(KEY_DPOP_THUMBPRINT) } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index d1fa9a5e7..c141ae83d 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -215,7 +215,7 @@ public class CredentialsManagerException : Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed." Code.MFA_REQUIRED -> "Multi-factor authentication is required to complete the credential renewal." Code.DPOP_KEY_MISSING -> "The stored credentials are DPoP-bound but the DPoP key pair is no longer available in the Android KeyStore. Re-authentication is required." - Code.DPOP_NOT_CONFIGURED -> "The stored credentials are DPoP-bound but the AuthenticationAPIClient used by this CredentialsManager was not configured with useDPoP(context). Call AuthenticationAPIClient(auth0).useDPoP(context) and pass the configured client to CredentialsManager." + Code.DPOP_NOT_CONFIGURED -> "The stored credentials are DPoP-bound but the AuthenticationAPIClient used by this credentials manager was not configured with useDPoP(context). Call AuthenticationAPIClient(auth0).useDPoP(context) and pass the configured client to the credentials manager." Code.UNKNOWN_ERROR -> "An unknown error has occurred while fetching the token. Please check the error cause for more details." } } From 97fd1b199b0eb05f8eb7c3c3e3353500ea6e44ab Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 24 Mar 2026 21:47:14 +0530 Subject: [PATCH 05/12] Moved the token type key to BaseCredentialsManager class --- .../authentication/storage/BaseCredentialsManager.kt | 6 +++++- .../android/authentication/storage/CredentialsManager.kt | 1 - .../authentication/storage/SecureCredentialsManager.kt | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 0d34432f3..384265cc9 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -25,7 +25,11 @@ public abstract class BaseCredentialsManager internal constructor( internal companion object { internal const val KEY_DPOP_THUMBPRINT = "com.auth0.dpop_key_thumbprint" + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + internal const val KEY_TOKEN_TYPE = "com.auth0.token_type" } + private var _clock: Clock = ClockImpl() /** @@ -169,7 +173,7 @@ public abstract class BaseCredentialsManager internal constructor( */ protected fun saveDPoPThumbprint(credentials: Credentials) { val dpopUsed = credentials.type.equals("DPoP", ignoreCase = true) - || authenticationClient.isDPoPEnabled + || authenticationClient.isDPoPEnabled if (!dpopUsed) { storage.remove(KEY_DPOP_THUMBPRINT) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index a658f9b83..4cc3144b7 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -763,7 +763,6 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting private const val KEY_ACCESS_TOKEN = "com.auth0.access_token" private const val KEY_REFRESH_TOKEN = "com.auth0.refresh_token" private const val KEY_ID_TOKEN = "com.auth0.id_token" - private const val KEY_TOKEN_TYPE = "com.auth0.token_type" private const val KEY_EXPIRES_AT = "com.auth0.expires_at" private const val KEY_SCOPE = "com.auth0.scope" diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index 41c05447c..adcb883ae 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -897,7 +897,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message + ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) @@ -1055,7 +1056,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message + ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) @@ -1255,8 +1257,6 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal const val KEY_ALIAS = "com.auth0.key" - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal const val KEY_TOKEN_TYPE = "com.auth0.token_type" // Using NO_SESSION to represent "no session" (uninitialized state) private const val NO_SESSION = -1L From 298b10f7cab1197b03a84cc8e870507475fa542b Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 24 Mar 2026 18:20:26 +0530 Subject: [PATCH 06/12] Added dpop validation check before token refresh --- .../storage/BaseCredentialsManager.kt | 57 +++++++++++++++++++ .../storage/CredentialsManager.kt | 11 ++++ .../storage/SecureCredentialsManager.kt | 12 ++++ 3 files changed, 80 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 384265cc9..7b6fd1350 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -194,6 +194,63 @@ public abstract class BaseCredentialsManager internal constructor( } } + /** + * Validates DPoP key/token alignment before attempting a refresh. + * + * Uses two signals to detect DPoP-bound credentials: + * - tokenType == "DPoP" + * - KEY_DPOP_THUMBPRINT exists + * + * @param tokenType the token_type value from storage (or decrypted credentials for migration) + * @return null if validation passes, or a CredentialsManagerException if it fails + */ + protected fun validateDPoPState(tokenType: String?): CredentialsManagerException? { + val storedThumbprint = storage.retrieveString(KEY_DPOP_THUMBPRINT) + val isDPoPBound = (tokenType?.equals("DPoP", ignoreCase = true) == true) + || (storedThumbprint != null) + if (!isDPoPBound) return null + + // Check 1: Does the DPoP key still exist in KeyStore? + val hasKey = try { + DPoPUtil.hasKeyPair() + } catch (e: DPoPException) { + Log.e(this::class.java.simpleName, "Failed to check DPoP key existence", e) + false + } + if (!hasKey) { + Log.w(this::class.java.simpleName, "DPoP key missing from KeyStore. Clearing stale credentials.") + clearCredentials() + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) + } + + // Check 2: Is the AuthenticationAPIClient configured with DPoP? + if (!authenticationClient.isDPoPEnabled) { + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_NOT_CONFIGURED) + } + + // Check 3: Does the current key match the one used when credentials were saved? + val currentThumbprint = try { + DPoPUtil.getPublicKeyJWK() + } catch (e: DPoPException) { + Log.e(this::class.java.simpleName, "Failed to read DPoP key thumbprint", e) + null + } + + if (storedThumbprint != null) { + if (currentThumbprint != storedThumbprint) { + Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. Clearing stale credentials.") + clearCredentials() + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) + } + } else if (currentThumbprint != null) { + // Migration: existing DPoP user upgraded — no thumbprint stored yet. + // Backfill so future checks can detect key rotation. + storage.store(KEY_DPOP_THUMBPRINT, currentThumbprint) + } + + return null + } + /** * Checks if the stored scope is the same as the requested one. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 4cc3144b7..437b623f0 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -483,6 +483,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } val request = authenticationClient.renewAuth(refreshToken) request.addParameters(parameters) if (scope != null) { @@ -593,8 +597,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting //Check if existing api credentials are present and valid val key = getAPICredentialsKey(audience, scope) val apiCredentialsJson = storage.retrieveString(key) + var apiCredentialType: String? = null apiCredentialsJson?.let { val apiCredentials = gson.fromJson(it, APICredentials::class.java) + apiCredentialType = apiCredentials.type val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong()) val scopeChanged = hasScopeChanged( @@ -617,6 +623,11 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } + validateDPoPState(apiCredentialType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.renewAuth(refreshToken, audience, scope) request.addParameters(parameters) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index adcb883ae..e3b432f29 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -848,6 +848,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) ?: credentials.type + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } Log.d(TAG, "Credentials have expired. Renewing them now...") val request = authenticationClient.renewAuth( credentials.refreshToken @@ -963,6 +968,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope)) //Check if existing api credentials are present and valid + var apiCredentialType: String? = null encryptedEncodedJson?.let { encryptedEncoded -> val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) val json: String = try { @@ -987,6 +993,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } val apiCredentials = gson.fromJson(json, APICredentials::class.java) + apiCredentialType = apiCredentials.type val expiresAt = apiCredentials.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) @@ -1014,6 +1021,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } + validateDPoPState(apiCredentialType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.renewAuth(refreshToken, audience, scope) request.addParameters(parameters) for (header in headers) { From 464e52be57148f05cf967c41ceb73d760be6641f Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Wed, 25 Mar 2026 11:37:58 +0530 Subject: [PATCH 07/12] Added the validation in SSOCredentials flow also --- .../android/authentication/storage/CredentialsManager.kt | 9 ++++++++- .../authentication/storage/SecureCredentialsManager.kt | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 437b623f0..f9a56919b 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -134,6 +134,12 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.ssoExchange(refreshToken) try { if (parameters.isNotEmpty()) { @@ -623,7 +629,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } - validateDPoPState(apiCredentialType)?.let { dpopError -> + val tokenType = apiCredentialType ?: storage.retrieveString(KEY_TOKEN_TYPE) + validateDPoPState(tokenType)?.let { dpopError -> callback.onFailure(dpopError) return@execute } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index e3b432f29..ef51b4d7a 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -282,6 +282,12 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) ?: existingCredentials.type + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.ssoExchange(existingCredentials.refreshToken) try { @@ -1021,7 +1027,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } - validateDPoPState(apiCredentialType)?.let { dpopError -> + val tokenType = apiCredentialType ?: storage.retrieveString(KEY_TOKEN_TYPE) ?: existingCredentials.type + validateDPoPState(tokenType)?.let { dpopError -> callback.onFailure(dpopError) return@execute } From efba1e3bf20f7632089afac6e44424f30fb1c86f Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Mon, 23 Mar 2026 16:45:56 +0530 Subject: [PATCH 08/12] Saving the Dpop thumbprint --- .../authentication/storage/SecureCredentialsManager.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index adcb883ae..c24aee641 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -897,8 +897,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message - ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) @@ -1056,8 +1055,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message - ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) From 7e6b32efd55a3b2a869dfd2e3094fc28e9ca23a4 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 24 Mar 2026 21:47:14 +0530 Subject: [PATCH 09/12] Moved the token type key to BaseCredentialsManager class --- .../authentication/storage/SecureCredentialsManager.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index c24aee641..adcb883ae 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -897,7 +897,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message + ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) @@ -1055,7 +1056,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure( CredentialsManagerException( CredentialsManagerException.Code.MFA_REQUIRED, - error.message ?: "Multi-factor authentication is required to complete the credential renewal.", + error.message + ?: "Multi-factor authentication is required to complete the credential renewal.", error, error.mfaRequiredErrorPayload ) From 55e8d3ae85ad69ceb94eef14b9e9b97253f882de Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Tue, 24 Mar 2026 18:20:26 +0530 Subject: [PATCH 10/12] Added dpop validation check before token refresh --- .../storage/BaseCredentialsManager.kt | 57 +++++++++++++++++++ .../storage/CredentialsManager.kt | 11 ++++ .../storage/SecureCredentialsManager.kt | 12 ++++ 3 files changed, 80 insertions(+) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 384265cc9..7b6fd1350 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -194,6 +194,63 @@ public abstract class BaseCredentialsManager internal constructor( } } + /** + * Validates DPoP key/token alignment before attempting a refresh. + * + * Uses two signals to detect DPoP-bound credentials: + * - tokenType == "DPoP" + * - KEY_DPOP_THUMBPRINT exists + * + * @param tokenType the token_type value from storage (or decrypted credentials for migration) + * @return null if validation passes, or a CredentialsManagerException if it fails + */ + protected fun validateDPoPState(tokenType: String?): CredentialsManagerException? { + val storedThumbprint = storage.retrieveString(KEY_DPOP_THUMBPRINT) + val isDPoPBound = (tokenType?.equals("DPoP", ignoreCase = true) == true) + || (storedThumbprint != null) + if (!isDPoPBound) return null + + // Check 1: Does the DPoP key still exist in KeyStore? + val hasKey = try { + DPoPUtil.hasKeyPair() + } catch (e: DPoPException) { + Log.e(this::class.java.simpleName, "Failed to check DPoP key existence", e) + false + } + if (!hasKey) { + Log.w(this::class.java.simpleName, "DPoP key missing from KeyStore. Clearing stale credentials.") + clearCredentials() + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) + } + + // Check 2: Is the AuthenticationAPIClient configured with DPoP? + if (!authenticationClient.isDPoPEnabled) { + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_NOT_CONFIGURED) + } + + // Check 3: Does the current key match the one used when credentials were saved? + val currentThumbprint = try { + DPoPUtil.getPublicKeyJWK() + } catch (e: DPoPException) { + Log.e(this::class.java.simpleName, "Failed to read DPoP key thumbprint", e) + null + } + + if (storedThumbprint != null) { + if (currentThumbprint != storedThumbprint) { + Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. Clearing stale credentials.") + clearCredentials() + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) + } + } else if (currentThumbprint != null) { + // Migration: existing DPoP user upgraded — no thumbprint stored yet. + // Backfill so future checks can detect key rotation. + storage.store(KEY_DPOP_THUMBPRINT, currentThumbprint) + } + + return null + } + /** * Checks if the stored scope is the same as the requested one. * diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 4cc3144b7..437b623f0 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -483,6 +483,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } val request = authenticationClient.renewAuth(refreshToken) request.addParameters(parameters) if (scope != null) { @@ -593,8 +597,10 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting //Check if existing api credentials are present and valid val key = getAPICredentialsKey(audience, scope) val apiCredentialsJson = storage.retrieveString(key) + var apiCredentialType: String? = null apiCredentialsJson?.let { val apiCredentials = gson.fromJson(it, APICredentials::class.java) + apiCredentialType = apiCredentials.type val willTokenExpire = willExpire(apiCredentials.expiresAt.time, minTtl.toLong()) val scopeChanged = hasScopeChanged( @@ -617,6 +623,11 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } + validateDPoPState(apiCredentialType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.renewAuth(refreshToken, audience, scope) request.addParameters(parameters) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index adcb883ae..e3b432f29 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -848,6 +848,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT callback.onFailure(CredentialsManagerException.NO_REFRESH_TOKEN) return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) ?: credentials.type + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } Log.d(TAG, "Credentials have expired. Renewing them now...") val request = authenticationClient.renewAuth( credentials.refreshToken @@ -963,6 +968,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT val encryptedEncodedJson = storage.retrieveString(getAPICredentialsKey(audience, scope)) //Check if existing api credentials are present and valid + var apiCredentialType: String? = null encryptedEncodedJson?.let { encryptedEncoded -> val encrypted = Base64.decode(encryptedEncoded, Base64.DEFAULT) val json: String = try { @@ -987,6 +993,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT } val apiCredentials = gson.fromJson(json, APICredentials::class.java) + apiCredentialType = apiCredentials.type val expiresAt = apiCredentials.expiresAt.time val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong()) @@ -1014,6 +1021,11 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } + validateDPoPState(apiCredentialType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.renewAuth(refreshToken, audience, scope) request.addParameters(parameters) for (header in headers) { From 6f9839e9af37d6962c703c3157b6503e275c5b31 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Wed, 25 Mar 2026 11:37:58 +0530 Subject: [PATCH 11/12] Added the validation in SSOCredentials flow also --- .../android/authentication/storage/CredentialsManager.kt | 9 ++++++++- .../authentication/storage/SecureCredentialsManager.kt | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 437b623f0..f9a56919b 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt @@ -134,6 +134,12 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.ssoExchange(refreshToken) try { if (parameters.isNotEmpty()) { @@ -623,7 +629,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting return@execute } - validateDPoPState(apiCredentialType)?.let { dpopError -> + val tokenType = apiCredentialType ?: storage.retrieveString(KEY_TOKEN_TYPE) + validateDPoPState(tokenType)?.let { dpopError -> callback.onFailure(dpopError) return@execute } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt index e3b432f29..ef51b4d7a 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt @@ -282,6 +282,12 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } + val tokenType = storage.retrieveString(KEY_TOKEN_TYPE) ?: existingCredentials.type + validateDPoPState(tokenType)?.let { dpopError -> + callback.onFailure(dpopError) + return@execute + } + val request = authenticationClient.ssoExchange(existingCredentials.refreshToken) try { @@ -1021,7 +1027,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT return@execute } - validateDPoPState(apiCredentialType)?.let { dpopError -> + val tokenType = apiCredentialType ?: storage.retrieveString(KEY_TOKEN_TYPE) ?: existingCredentials.type + validateDPoPState(tokenType)?.let { dpopError -> callback.onFailure(dpopError) return@execute } From fed47bb8c1cbaabd5e1a39b7fe48a58dbfd42b80 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 26 Mar 2026 11:33:13 +0530 Subject: [PATCH 12/12] Added new error type for thumbprint mismatch scenario --- .../android/authentication/storage/BaseCredentialsManager.kt | 4 ++-- .../authentication/storage/CredentialsManagerException.kt | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 7b6fd1350..be2aa1738 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -238,9 +238,9 @@ public abstract class BaseCredentialsManager internal constructor( if (storedThumbprint != null) { if (currentThumbprint != storedThumbprint) { - Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. Clearing stale credentials.") + Log.w(this::class.java.simpleName, "DPoP key thumbprint mismatch. The key pair has changed since credentials were saved. Clearing stale credentials.") clearCredentials() - return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISSING) + return CredentialsManagerException(CredentialsManagerException.Code.DPOP_KEY_MISMATCH) } } else if (currentThumbprint != null) { // Migration: existing DPoP user upgraded — no thumbprint stored yet. diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt index c141ae83d..44c31f7d8 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManagerException.kt @@ -49,6 +49,7 @@ public class CredentialsManagerException : SSO_EXCHANGE_FAILED, MFA_REQUIRED, DPOP_KEY_MISSING, + DPOP_KEY_MISMATCH, DPOP_NOT_CONFIGURED, UNKNOWN_ERROR } @@ -163,6 +164,8 @@ public class CredentialsManagerException : public val DPOP_KEY_MISSING: CredentialsManagerException = CredentialsManagerException(Code.DPOP_KEY_MISSING) + public val DPOP_KEY_MISMATCH: CredentialsManagerException = + CredentialsManagerException(Code.DPOP_KEY_MISMATCH) public val DPOP_NOT_CONFIGURED: CredentialsManagerException = CredentialsManagerException(Code.DPOP_NOT_CONFIGURED) @@ -215,6 +218,7 @@ public class CredentialsManagerException : Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed." Code.MFA_REQUIRED -> "Multi-factor authentication is required to complete the credential renewal." Code.DPOP_KEY_MISSING -> "The stored credentials are DPoP-bound but the DPoP key pair is no longer available in the Android KeyStore. Re-authentication is required." + Code.DPOP_KEY_MISMATCH -> "The stored credentials are DPoP-bound but the current DPoP key pair does not match the one used when credentials were saved. Re-authentication is required." Code.DPOP_NOT_CONFIGURED -> "The stored credentials are DPoP-bound but the AuthenticationAPIClient used by this credentials manager was not configured with useDPoP(context). Call AuthenticationAPIClient(auth0).useDPoP(context) and pass the configured client to the credentials manager." Code.UNKNOWN_ERROR -> "An unknown error has occurred while fetching the token. Please check the error cause for more details." }