Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Comment thread
utkrishtsahu marked this conversation as resolved.
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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
)
Expand Down
Comment thread
utkrishtsahu marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading