From acf10a491ba5dfaf3b8102b7530367659aa0b23f Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Wed, 15 Apr 2026 11:15:10 +0530 Subject: [PATCH 1/3] feat :Added new clearAll API to clear all credentaisl and key pairs --- .../storage/BaseCredentialsManager.kt | 1 + .../storage/CredentialsManager.kt | 21 +++++++++-- .../authentication/storage/CryptoUtil.java | 8 +++++ .../storage/SecureCredentialsManager.kt | 24 +++++++++++-- .../storage/CredentialsManagerTest.kt | 6 ++++ .../storage/CryptoUtilTest.java | 35 +++++++++++++++++++ .../storage/SecureCredentialsManagerTest.kt | 7 ++++ 7 files changed, 98 insertions(+), 4 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 23e0c1b6a..b101c40e6 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 @@ -159,6 +159,7 @@ public abstract class BaseCredentialsManager internal constructor( public abstract fun clearCredentials() public abstract fun clearApiCredentials(audience: String, scope: String? = null) + public abstract fun clearAll() public abstract fun hasValidCredentials(): Boolean public abstract fun hasValidCredentials(minTtl: Long): Boolean 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 0d8436b4b..0ddac0b22 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 @@ -6,6 +6,8 @@ import androidx.annotation.VisibleForTesting import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback +import com.auth0.android.dpop.DPoP +import com.auth0.android.dpop.DPoPException import com.auth0.android.request.internal.GsonProvider import com.auth0.android.request.internal.Jwt import com.auth0.android.result.APICredentials @@ -528,7 +530,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting 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 ) @@ -654,7 +657,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting 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 ) @@ -710,6 +714,19 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting storage.removeAll() } + /** + * Removes all credentials, API credentials, and cryptographic key pairs. + * This calls [Storage.removeAll] to clear all stored data + */ + override fun clearAll() { + storage.removeAll() + try { + DPoP.clearKeyPair() + } catch (e: DPoPException) { + Log.e(TAG, "Failed to clear DPoP key pair ${e.stackTraceToString()}") + } + } + /** * Removes the credentials for the given audience from the storage if present. * @param audience Audience for which the [APICredentials] are stored diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java index 4ac745467..e2cfd8059 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/CryptoUtil.java @@ -327,6 +327,14 @@ private void deleteAESKeys() { storage.remove(OLD_KEY_IV_ALIAS); } + /** + * Removes all cryptographic keys (both RSA and AES) used by this instance. + */ + public void deleteAllKeys() { + deleteRSAKeys(); + deleteAESKeys(); + } + /** * Decrypts the given input using a generated RSA Private Key. * Used to decrypt the AES key for later usage. 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 7313774b1..64ecb70a6 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 @@ -10,6 +10,8 @@ import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback +import com.auth0.android.dpop.DPoP +import com.auth0.android.dpop.DPoPException import com.auth0.android.request.internal.GsonProvider import com.auth0.android.result.APICredentials import com.auth0.android.result.Credentials @@ -736,6 +738,22 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT Log.d(TAG, "Credentials were just removed from the storage") } + /** + * Removes all credentials, API credentials, and cryptographic key pairs. + * This calls [Storage.removeAll] to clear all stored data + */ + override fun clearAll() { + storage.removeAll() + crypto.deleteAllKeys() + clearBiometricSession() + try { + DPoP.clearKeyPair() + } catch (e: DPoPException) { + Log.e(TAG, "Failed to clear DPoP key pair ${e.stackTraceToString()}") + } + Log.d(TAG, "All credentials and key pairs were removed") + } + /** * Removes the credentials for the given audience from the storage if present. * @param audience Audience for which the [APICredentials] are stored @@ -890,7 +908,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 ) @@ -1048,7 +1067,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 ) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 7a8ef6674..73854af37 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -1495,6 +1495,12 @@ public class CredentialsManagerTest { verifyNoMoreInteractions(storage) } + @Test + public fun shouldClearAllCredentialsAndDPoPKeyPair() { + manager.clearAll() + verify(storage).removeAll() + } + @Test public fun shouldSaveApiCredentialsWithScopeAsKey() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java index 1f28b30a6..95d0ede42 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CryptoUtilTest.java @@ -2086,6 +2086,41 @@ public void shouldNotPropagateProviderExceptionAsIncompatibleDeviceException() t assertThat(result, is(newAESKey)); } + /* + * deleteAllKeys() tests + */ + + @Test + public void shouldDeleteBothRSAAndAESKeysWhenDeleteAllKeysIsCalled() throws Exception { + cryptoUtil.deleteAllKeys(); + + // Verify RSA keys deleted from KeyStore + Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); + Mockito.verify(keyStore).deleteEntry(OLD_KEY_ALIAS); + + // Verify AES keys deleted from Storage + Mockito.verify(storage).remove(KEY_ALIAS); + Mockito.verify(storage).remove(KEY_ALIAS + "_iv"); + Mockito.verify(storage).remove(OLD_KEY_ALIAS); + Mockito.verify(storage).remove(OLD_KEY_ALIAS + "_iv"); + } + + @Test + public void shouldDeleteAESKeysEvenIfRSAKeyDeletionFails() throws Exception { + doThrow(new KeyStoreException("KeyStore error")).when(keyStore).deleteEntry(anyString()); + + cryptoUtil.deleteAllKeys(); + + // RSA deletion was attempted (first deleteEntry throws, second is never reached) + Mockito.verify(keyStore).deleteEntry(KEY_ALIAS); + + // AES keys should still be deleted from Storage + Mockito.verify(storage).remove(KEY_ALIAS); + Mockito.verify(storage).remove(KEY_ALIAS + "_iv"); + Mockito.verify(storage).remove(OLD_KEY_ALIAS); + Mockito.verify(storage).remove(OLD_KEY_ALIAS + "_iv"); + } + /* * Helper methods */ diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index ef8ba7bd6..e04311890 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -2164,6 +2164,13 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(storage) } + @Test + public fun shouldClearAllCredentialsKeyPairsAndDPoPKeyPair() { + manager.clearAll() + verify(storage).removeAll() + verify(crypto).deleteAllKeys() + } + @Test public fun shouldSaveEncryptedApiCredentialsWithScopeAsKey() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS From 464aefa9cb8de89680c87b8128bd6a351b4cc2cc Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Wed, 15 Apr 2026 11:52:12 +0530 Subject: [PATCH 2/3] updated the migration guide --- V4_MIGRATION_GUIDE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 736a3ddee..0b340975d 100644 --- a/V4_MIGRATION_GUIDE.md +++ b/V4_MIGRATION_GUIDE.md @@ -22,6 +22,8 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update - [**Behavior Changes**](#behavior-changes) + [clearCredentials() Now Clears All Storage](#clearCredentials-now-clears-all-storage) + [Storage Interface: New removeAll() Method](#storage-interface-new-removeall-method) +- [**New APIs**](#new-apis) + + [clearAll() — Full Credential and Key Cleanup](#clearall--full-credential-and-key-cleanup) - [**Dependency Changes**](#dependency-changes) + [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency) + [DefaultClient.Builder](#defaultclientbuilder) @@ -226,6 +228,26 @@ In v4, `clearCredentials()` calls `Storage.removeAll()`, which clears **all** va **Impact:** Existing custom `Storage` implementations will continue to compile and work without changes. Override `removeAll()` to provide the actual clearing behavior if your custom storage is used with `clearCredentials()`. +## New APIs + +### `clearAll()` — Full Credential and Key Cleanup + +v4 introduces a new `clearAll()` method on `CredentialsManager` and `SecureCredentialsManager` that performs a complete cleanup of all stored credentials **and** cryptographic key pairs. + +**Usage:** + +```kotlin +// Clear everything on logout — credentials, DPoP keys, and encryption keys +credentialsManager.clearAll() +``` + +**When to use `clearAll()` vs `clearCredentials()`:** + +- Use **`clearCredentials()`** when you only need to remove stored tokens (e.g., forcing a re-login) but want to preserve cryptographic keys for future sessions. +- Use **`clearAll()`** on full logout or account removal, when you want to ensure no credentials or key material remain on the device. + +> **Note:** `clearAll()` catches any errors from DPoP key pair deletion internally, so it will not throw even if the DPoP key pair was never created or has already been removed. + ## Dependency Changes ### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency) From 1a24eefc416fd8d0aa18278028fba3e3c65f0178 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Wed, 15 Apr 2026 15:51:32 +0530 Subject: [PATCH 3/3] updated the test files --- .../android/authentication/storage/CredentialsManagerTest.kt | 2 +- .../authentication/storage/SecureCredentialsManagerTest.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 73854af37..7b1cd2c68 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -1496,7 +1496,7 @@ public class CredentialsManagerTest { } @Test - public fun shouldClearAllCredentialsAndDPoPKeyPair() { + public fun shouldClearAllCredentials() { manager.clearAll() verify(storage).removeAll() } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index e04311890..124d4b13a 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -2165,10 +2165,11 @@ public class SecureCredentialsManagerTest { } @Test - public fun shouldClearAllCredentialsKeyPairsAndDPoPKeyPair() { + public fun shouldClearAllCredentialsKeyPairsAndBiometricSession() { manager.clearAll() verify(storage).removeAll() verify(crypto).deleteAllKeys() + Assert.assertFalse(manager.isBiometricSessionValid()) } @Test