diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md index 4334cf57..20469266 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) @@ -228,6 +230,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) 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 23e0c1b6..b101c40e 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 0d8436b4..0ddac0b2 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 4ac74546..e2cfd805 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 7313774b..64ecb70a 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 7a8ef667..7b1cd2c6 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 shouldClearAllCredentials() { + 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 1f28b30a..95d0ede4 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 ef8ba7bd..124d4b13 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,14 @@ public class SecureCredentialsManagerTest { verifyNoMoreInteractions(storage) } + @Test + public fun shouldClearAllCredentialsKeyPairsAndBiometricSession() { + manager.clearAll() + verify(storage).removeAll() + verify(crypto).deleteAllKeys() + Assert.assertFalse(manager.isBiometricSessionValid()) + } + @Test public fun shouldSaveEncryptedApiCredentialsWithScopeAsKey() { val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS