diff --git a/.github/actions/rl-scanner/action.yml b/.github/actions/rl-scanner/action.yml
index b55195740..9fd39bc9f 100644
--- a/.github/actions/rl-scanner/action.yml
+++ b/.github/actions/rl-scanner/action.yml
@@ -33,7 +33,7 @@ runs:
- name: Install RL Wrapper
shell: bash
run: |
- pip install rl-wrapper>=1.0.0 --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python-local/simple"
+ pip install rl-wrapper --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python/simple"
- name: Run RL Scanner
shell: bash
diff --git a/V4_MIGRATION_GUIDE.md b/V4_MIGRATION_GUIDE.md
index 28570c660..eb015ba68 100644
--- a/V4_MIGRATION_GUIDE.md
+++ b/V4_MIGRATION_GUIDE.md
@@ -1,10 +1,30 @@
# Migration Guide from SDK v3 to v4
-## Overview
+> **Note:** This guide is actively maintained during the v4 development phase. As new changes are merged, this document will be updated to reflect the latest breaking changes and migration steps.
-v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest
-Android development environment. This guide documents the changes required when migrating from v3 to
-v4.
+v4 of the Auth0 Android SDK includes significant build toolchain updates, updated default values for better out-of-the-box behavior, and behavior changes to simplify credential management. This guide documents the changes required when migrating from v3 to v4.
+
+---
+
+## Table of Contents
+
+- [**Requirements Changes**](#requirements-changes)
+ + [Java Version](#java-version)
+ + [Gradle and Android Gradle Plugin](#gradle-and-android-gradle-plugin)
+ + [Kotlin Version](#kotlin-version)
+- [**Breaking Changes**](#breaking-changes)
+ + [Classes Removed](#classes-removed)
+ + [DPoP Configuration Moved to Builder](#dpop-configuration-moved-to-builder)
+- [**Default Values Changed**](#default-values-changed)
+ + [Credentials Manager minTTL](#credentials-manager-minttl)
+- [**Behavior Changes**](#behavior-changes)
+ + [clearCredentials() Now Clears All Storage](#clearCredentials-now-clears-all-storage)
+ + [Storage Interface: New removeAll() Method](#storage-interface-new-removeall-method)
+- [**Dependency Changes**](#dependency-changes)
+ + [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency)
+ + [DefaultClient.Builder](#defaultclientbuilder)
+
+---
## Requirements Changes
@@ -103,6 +123,60 @@ WebAuthProvider
This change ensures that DPoP configuration is scoped to individual login requests rather than
persisting across the entire application lifecycle.
+## Default Values Changed
+
+### Credentials Manager `minTTL`
+
+**Change:** The default `minTtl` value changed from `0` to `60` seconds.
+
+This change affects the following Credentials Manager methods:
+
+- `getCredentials(callback)` / `awaitCredentials()`
+- `getCredentials(scope, minTtl, callback)` / `awaitCredentials(scope, minTtl)`
+- `getCredentials(scope, minTtl, parameters, callback)` / `awaitCredentials(scope, minTtl, parameters)`
+- `getCredentials(scope, minTtl, parameters, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, forceRefresh)`
+- `getCredentials(scope, minTtl, parameters, headers, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, headers, forceRefresh)`
+- `hasValidCredentials()`
+
+**Impact:** Credentials will be renewed if they expire within 60 seconds, instead of only when already expired.
+
+
+ Migration example
+
+```kotlin
+// v3 - minTtl defaulted to 0, had to be set explicitly
+credentialsManager.getCredentials(scope = null, minTtl = 60, callback = callback)
+
+// v4 - minTtl defaults to 60 seconds
+credentialsManager.getCredentials(callback)
+
+// v4 - use 0 to restore v3 behavior
+credentialsManager.getCredentials(scope = null, minTtl = 0, callback = callback)
+```
+
+
+**Reason:** A `minTtl` of `0` meant credentials were not renewed until expired, which could result in delivering access tokens that expire immediately after retrieval, causing subsequent API requests to fail. Setting a default value of `60` seconds ensures the access token remains valid for a reasonable period.
+
+## Behavior Changes
+
+### `clearCredentials()` Now Clears All Storage
+
+**Change:** `clearCredentials()` now calls `Storage.removeAll()` instead of removing individual credential keys.
+
+In v3, `clearCredentials()` removed only specific credential keys (access token, refresh token, ID token, etc.) from the underlying `Storage`.
+
+In v4, `clearCredentials()` calls `Storage.removeAll()`, which clears **all** values in the storage — including any API credentials stored for specific audiences.
+
+**Impact:** If you need to remove only the primary credentials while preserving other stored data, consider using a separate `Storage` instance for API credentials.
+
+**Reason:** This simplifies credential cleanup and ensures no stale data remains in storage after logout. It aligns the behavior with the Swift SDK's `clear()` method, which also clears all stored values.
+
+### `Storage` Interface: New `removeAll()` Method
+
+**Change:** The `Storage` interface now includes a `removeAll()` method with a default empty implementation.
+
+**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()`.
+
## 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 d3ac32d59..23e0c1b6a 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
@@ -22,6 +22,17 @@ public abstract class BaseCredentialsManager internal constructor(
) {
private var _clock: Clock = ClockImpl()
+ public companion object {
+ /**
+ * Default minimum time to live (in seconds) for the access token.
+ * When retrieving credentials, if the access token has less than this amount of time
+ * remaining before expiration, it will be automatically renewed.
+ * This ensures the access token is valid for at least a short window after retrieval,
+ * preventing downstream API call failures from nearly-expired tokens.
+ */
+ public const val DEFAULT_MIN_TTL: Int = 60
+ }
+
/**
* Updates the clock instance used for expiration verification purposes.
* The use of this method can help on situations where the clock comes from an external synced source.
@@ -83,7 +94,7 @@ public abstract class BaseCredentialsManager internal constructor(
public abstract fun getApiCredentials(
audience: String,
scope: String? = null,
- minTtl: Int = 0,
+ minTtl: Int = DEFAULT_MIN_TTL,
parameters: Map = emptyMap(),
headers: Map = emptyMap(),
callback: Callback
@@ -139,7 +150,7 @@ public abstract class BaseCredentialsManager internal constructor(
public abstract suspend fun awaitApiCredentials(
audience: String,
scope: String? = null,
- minTtl: Int = 0,
+ minTtl: Int = DEFAULT_MIN_TTL,
parameters: Map = emptyMap(),
headers: Map = emptyMap()
): APICredentials
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 31f8e62f4..db23e8b18 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
@@ -244,7 +244,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitCredentials(): Credentials {
- return awaitCredentials(null, 0)
+ return awaitCredentials(null, DEFAULT_MIN_TTL)
}
/**
@@ -390,7 +390,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
* @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException].
*/
override fun getCredentials(callback: Callback) {
- getCredentials(null, 0, callback)
+ getCredentials(null, DEFAULT_MIN_TTL, callback)
}
/**
@@ -702,7 +702,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
* @return whether there are valid credentials stored on this manager.
*/
override fun hasValidCredentials(): Boolean {
- return hasValidCredentials(0)
+ return hasValidCredentials(DEFAULT_MIN_TTL.toLong())
}
/**
@@ -727,13 +727,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
* Removes the credentials from the storage if present.
*/
override fun clearCredentials() {
- storage.remove(KEY_ACCESS_TOKEN)
- storage.remove(KEY_REFRESH_TOKEN)
- storage.remove(KEY_ID_TOKEN)
- storage.remove(KEY_TOKEN_TYPE)
- storage.remove(KEY_EXPIRES_AT)
- storage.remove(KEY_SCOPE)
- storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
+ storage.removeAll()
}
/**
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 a6e86c492..cb273a13b 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
@@ -409,7 +409,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
@JvmSynthetic
@Throws(CredentialsManagerException::class)
override suspend fun awaitCredentials(): Credentials {
- return awaitCredentials(null, 0)
+ return awaitCredentials(null, DEFAULT_MIN_TTL)
}
/**
@@ -579,7 +579,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
override fun getCredentials(
callback: Callback
) {
- getCredentials(null, 0, callback)
+ getCredentials(null, DEFAULT_MIN_TTL, callback)
}
/**
@@ -754,10 +754,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* Delete the stored credentials
*/
override fun clearCredentials() {
- storage.remove(KEY_CREDENTIALS)
- storage.remove(KEY_EXPIRES_AT)
- storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
- storage.remove(KEY_CAN_REFRESH)
+ storage.removeAll()
clearBiometricSession()
Log.d(TAG, "Credentials were just removed from the storage")
}
@@ -779,7 +776,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* @return whether this manager contains a valid non-expired pair of credentials or not.
*/
override fun hasValidCredentials(): Boolean {
- return hasValidCredentials(0)
+ return hasValidCredentials(DEFAULT_MIN_TTL.toLong())
}
/**
diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt
index b9c5bb8d4..72ebfebdc 100644
--- a/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt
+++ b/auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt
@@ -75,6 +75,10 @@ public class SharedPreferencesStorage @JvmOverloads constructor(
sp.edit().remove(name).apply()
}
+ override fun removeAll() {
+ sp.edit().clear().apply()
+ }
+
private companion object {
private const val SHARED_PREFERENCES_NAME = "com.auth0.authentication.storage"
}
diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt
index f699cd49e..d2e788f2a 100644
--- a/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt
+++ b/auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt
@@ -75,4 +75,9 @@ public interface Storage {
* @param name the name of the value to remove.
*/
public fun remove(name: String)
+
+ /**
+ * Removes all values from the storage.
+ */
+ public fun removeAll() {}
}
\ No newline at end of file
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 c69534fda..b947ba596 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
@@ -3,6 +3,7 @@ package com.auth0.android.authentication.storage
import com.auth0.android.NetworkErrorException
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
+import com.auth0.android.authentication.storage.BaseCredentialsManager.Companion.DEFAULT_MIN_TTL
import com.auth0.android.callback.Callback
import com.auth0.android.request.Request
import com.auth0.android.request.internal.GsonProvider
@@ -672,7 +673,7 @@ public class CredentialsManagerTest {
Mockito.`when`(
client.renewAuth("refresh_token", "audience")
).thenReturn(request)
- val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 1 * 1000)
+ val newDate = Date(CredentialsMock.CURRENT_TIME_MS + (DEFAULT_MIN_TTL + 10) * 1000L)
val jwtMock = mock()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
@@ -1479,13 +1480,7 @@ public class CredentialsManagerTest {
@Test
public fun shouldClearCredentials() {
manager.clearCredentials()
- verify(storage).remove("com.auth0.id_token")
- verify(storage).remove("com.auth0.access_token")
- verify(storage).remove("com.auth0.refresh_token")
- verify(storage).remove("com.auth0.token_type")
- verify(storage).remove("com.auth0.expires_at")
- verify(storage).remove("com.auth0.scope")
- verify(storage).remove("com.auth0.cache_expires_at")
+ verify(storage).removeAll()
verifyNoMoreInteractions(storage)
}
@@ -1770,6 +1765,103 @@ public class CredentialsManagerTest {
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
}
+ @Test
+ public fun shouldRenewCredentialsViaCallbackWhenTokenExpiresWithinDefaultMinTtl() {
+ // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type")
+ Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope")
+ Mockito.`when`(
+ client.renewAuth("refreshToken")
+ ).thenReturn(request)
+ val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
+ val jwtMock = mock()
+ Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
+ Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
+
+ val renewedCredentials =
+ Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
+ Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
+ // Use no-arg getCredentials which now uses DEFAULT_MIN_TTL
+ manager.getCredentials(callback)
+ verify(callback).onSuccess(
+ credentialsCaptor.capture()
+ )
+ // Verify renewal was triggered (client.renewAuth was called)
+ verify(client).renewAuth("refreshToken")
+ val retrievedCredentials = credentialsCaptor.firstValue
+ MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
+ MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("newId"))
+ MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess"))
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ public fun shouldAwaitRenewedCredentialsWhenTokenExpiresWithinDefaultMinTtl(): Unit = runTest {
+ // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type")
+ Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope")
+ Mockito.`when`(
+ client.renewAuth("refreshToken")
+ ).thenReturn(request)
+ val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
+ val jwtMock = mock()
+ Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
+ Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
+
+ val renewedCredentials =
+ Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
+ Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
+ // Use no-arg awaitCredentials which now uses DEFAULT_MIN_TTL
+ val result = manager.awaitCredentials()
+ // Verify renewal was triggered
+ verify(client).renewAuth("refreshToken")
+ MatcherAssert.assertThat(result, Is.`is`(Matchers.notNullValue()))
+ MatcherAssert.assertThat(result.idToken, Is.`is`("newId"))
+ MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess"))
+ }
+
+ @Test
+ public fun shouldNotHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlAndNoRefreshToken() {
+ // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), and no refresh token
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn(null)
+ Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
+ // No-arg hasValidCredentials now uses DEFAULT_MIN_TTL, so token expiring in 30s is invalid
+ Assert.assertFalse(manager.hasValidCredentials())
+ }
+
+ @Test
+ public fun shouldHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlButRefreshTokenAvailable() {
+ // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), but refresh token is available
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
+ Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
+ // Even though token expires within DEFAULT_MIN_TTL, refresh token makes it valid
+ MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
+ }
+
@Test
public fun shouldNotHaveCredentialsWhenAccessTokenAndIdTokenAreMissing() {
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn(null)
@@ -1812,7 +1904,7 @@ public class CredentialsManagerTest {
//now, update the clock and retry
manager.setClock(object : Clock {
override fun getCurrentTimeMillis(): Long {
- return CredentialsMock.CURRENT_TIME_MS - 1000
+ return CredentialsMock.CURRENT_TIME_MS - (DEFAULT_MIN_TTL * 1000 + 1000)
}
})
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
@@ -1829,7 +1921,6 @@ public class CredentialsManagerTest {
})
}
-
@Test
public fun shouldAddParametersToRequest() {
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt
index 42449325e..d5f19678e 100644
--- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt
+++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerBiometricPolicyTest.kt
@@ -270,7 +270,7 @@ public class SecureCredentialsManagerBiometricPolicyTest {
// Clear credentials
manager.clearCredentials()
- verify(mockStorage, atLeastOnce()).remove(any())
+ verify(mockStorage).removeAll()
// Session should be invalid
assert(!manager.isBiometricSessionValid())
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 45fb805b2..4d29f1224 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
@@ -9,6 +9,7 @@ import com.auth0.android.Auth0
import com.auth0.android.NetworkErrorException
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
+import com.auth0.android.authentication.storage.BaseCredentialsManager.Companion.DEFAULT_MIN_TTL
import com.auth0.android.callback.Callback
import com.auth0.android.request.Request
import com.auth0.android.request.internal.GsonProvider
@@ -769,9 +770,7 @@ public class SecureCredentialsManagerTest {
exception.message,
Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Any previously stored content is now lost. Please try saving the credentials again.")
)
- verify(storage).remove("com.auth0.credentials")
- verify(storage).remove("com.auth0.credentials_expires_at")
- verify(storage).remove("com.auth0.credentials_can_refresh")
+ verify(storage).removeAll()
}
@Test
@@ -866,9 +865,7 @@ public class SecureCredentialsManagerTest {
"Any previously stored content is now lost. Please try saving the credentials again."
)
)
- verify(storage).remove("com.auth0.credentials")
- verify(storage).remove("com.auth0.credentials_expires_at")
- verify(storage).remove("com.auth0.credentials_can_refresh")
+ verify(storage).removeAll()
}
@Test
@@ -2152,10 +2149,7 @@ public class SecureCredentialsManagerTest {
@Test
public fun shouldClearCredentials() {
manager.clearCredentials()
- verify(storage).remove("com.auth0.credentials")
- verify(storage).remove("com.auth0.credentials_expires_at")
- verify(storage).remove("com.auth0.credentials_access_token_expires_at")
- verify(storage).remove("com.auth0.credentials_can_refresh")
+ verify(storage).removeAll()
verifyNoMoreInteractions(storage)
}
@@ -2501,6 +2495,103 @@ public class SecureCredentialsManagerTest {
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
}
+ @Test
+ public fun shouldRenewCredentialsViaCallbackWhenTokenExpiresWithinDefaultMinTtl() {
+ Mockito.`when`(localAuthenticationManager.authenticate()).then {
+ localAuthenticationManager.resultCallback.onSuccess(true)
+ }
+ // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
+ val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 30 * 1000)
+ insertTestCredentials(false, true, true, expiresAt, "scope")
+ Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at"))
+ .thenReturn(expiresAt.time)
+ val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
+ val jwtMock = mock()
+ Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
+ Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
+ Mockito.`when`(
+ client.renewAuth("refreshToken")
+ ).thenReturn(request)
+ val expectedCredentials =
+ Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
+ Mockito.`when`(request.execute()).thenReturn(expectedCredentials)
+ val expectedJson = gson.toJson(expectedCredentials)
+ Mockito.`when`(crypto.encrypt(expectedJson.toByteArray()))
+ .thenReturn(expectedJson.toByteArray())
+ // Use no-arg getCredentials which now uses DEFAULT_MIN_TTL
+ manager.getCredentials(callback)
+ verify(callback).onSuccess(
+ credentialsCaptor.capture()
+ )
+ // Verify renewal was triggered
+ verify(client).renewAuth("refreshToken")
+ val retrievedCredentials = credentialsCaptor.firstValue
+ MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
+ MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("newId"))
+ MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess"))
+ }
+
+ @Test
+ @ExperimentalCoroutinesApi
+ public fun shouldAwaitRenewedCredentialsWhenTokenExpiresWithinDefaultMinTtl(): Unit = runTest {
+ Mockito.`when`(localAuthenticationManager.authenticate()).then {
+ localAuthenticationManager.resultCallback.onSuccess(true)
+ }
+ // Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
+ val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS + 30 * 1000)
+ insertTestCredentials(false, true, true, expiresAt, "scope")
+ Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at"))
+ .thenReturn(expiresAt.time)
+ val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
+ val jwtMock = mock()
+ Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
+ Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
+ Mockito.`when`(
+ client.renewAuth("refreshToken")
+ ).thenReturn(request)
+ val expectedCredentials =
+ Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
+ Mockito.`when`(request.execute()).thenReturn(expectedCredentials)
+ val expectedJson = gson.toJson(expectedCredentials)
+ Mockito.`when`(crypto.encrypt(expectedJson.toByteArray()))
+ .thenReturn(expectedJson.toByteArray())
+ // Use no-arg awaitCredentials which now uses DEFAULT_MIN_TTL
+ val result = manager.awaitCredentials()
+ // Verify renewal was triggered
+ verify(client).renewAuth("refreshToken")
+ MatcherAssert.assertThat(result, Is.`is`(Matchers.notNullValue()))
+ MatcherAssert.assertThat(result.idToken, Is.`is`("newId"))
+ MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess"))
+ }
+
+ @Test
+ public fun shouldNotHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlAndNoRefreshToken() {
+ // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), and no refresh token
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveBoolean("com.auth0.credentials_can_refresh"))
+ .thenReturn(false)
+ Mockito.`when`(storage.retrieveString("com.auth0.credentials"))
+ .thenReturn("{\"access_token\":\"accessToken\"}")
+ // No-arg hasValidCredentials now uses DEFAULT_MIN_TTL, so token expiring in 30s is invalid
+ Assert.assertFalse(manager.hasValidCredentials())
+ }
+
+ @Test
+ public fun shouldHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlButRefreshTokenAvailable() {
+ // Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), but refresh token is available
+ val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
+ Mockito.`when`(storage.retrieveLong("com.auth0.credentials_access_token_expires_at"))
+ .thenReturn(expirationTime)
+ Mockito.`when`(storage.retrieveBoolean("com.auth0.credentials_can_refresh"))
+ .thenReturn(true)
+ Mockito.`when`(storage.retrieveString("com.auth0.credentials"))
+ .thenReturn("{\"access_token\":\"accessToken\", \"refresh_token\":\"refreshToken\"}")
+ // Even though token expires within DEFAULT_MIN_TTL, refresh token makes it valid
+ MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
+ }
+
@Test
public fun shouldHaveCredentialsWhenTheAliasUsedHasNotBeenMigratedYet() {
val expirationTime = CredentialsMock.ONE_HOUR_AHEAD_MS
@@ -3334,7 +3425,7 @@ public class SecureCredentialsManagerTest {
//now, update the clock and retry
manager.setClock(object : Clock {
override fun getCurrentTimeMillis(): Long {
- return CredentialsMock.CURRENT_TIME_MS - 1000
+ return CredentialsMock.CURRENT_TIME_MS - (DEFAULT_MIN_TTL * 1000 + 1000)
}
})
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java
index 82fdfc210..89c2c7983 100644
--- a/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java
+++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SharedPreferencesStorageTest.java
@@ -221,4 +221,13 @@ public void shouldRemovePreferencesKey() {
verify(sharedPreferencesEditor).apply();
}
+ @Test
+ public void shouldRemoveAllPreferencesKeys() {
+ when(sharedPreferencesEditor.clear()).thenReturn(sharedPreferencesEditor);
+ SharedPreferencesStorage storage = new SharedPreferencesStorage(context);
+ storage.removeAll();
+ verify(sharedPreferencesEditor).clear();
+ verify(sharedPreferencesEditor).apply();
+ }
+
}
\ No newline at end of file
diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
index 672aeb215..038708839 100644
--- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
+++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt
@@ -45,7 +45,6 @@ import org.hamcrest.core.IsNot.not
import org.hamcrest.core.IsNull.notNullValue
import org.junit.Assert
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
@@ -54,6 +53,7 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
+import org.junit.Ignore
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@@ -1539,18 +1539,17 @@ public class WebAuthProviderTest {
}
- // TODO: https://auth0team.atlassian.net/browse/SDK-7752
+ @Ignore("Requires security provider fix - see SDK-7752")
@Test
- @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated")
@Throws(Exception::class)
public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() {
val pkce = Mockito.mock(PKCE::class.java)
`when`(pkce.codeChallenge).thenReturn("challenge")
- val mockAPI = AuthenticationAPIMockServer()
- mockAPI.willReturnEmptyJsonWebKeys()
+ val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java)
val authCallback = mock>()
- val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain)
- proxyAccount.networkingClient = SSLTestUtils.testClient
+ val proxyAccount =
+ Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN)
+ proxyAccount.networkingClient = networkingClient
login(proxyAccount)
.withState("1234567890")
.withNonce(JwtTestUtils.EXPECTED_NONCE)
@@ -1583,12 +1582,19 @@ public class WebAuthProviderTest {
Date(),
"codeScope"
)
+ // Mock JWKS response with empty keys (no matching RSA key for kid)
+ val emptyJwksJson = """{"keys": []}"""
+ val jwksInputStream: InputStream = ByteArrayInputStream(emptyJwksJson.toByteArray())
+ val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap())
+ Mockito.doReturn(jwksResponse).`when`(networkingClient).load(
+ eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"),
+ any()
+ )
Mockito.doAnswer {
callbackCaptor.firstValue.onSuccess(codeCredentials)
null
}.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture())
Assert.assertTrue(resume(intent))
- mockAPI.takeRequest()
ShadowLooper.idleMainLooper()
verify(authCallback).onFailure(authExceptionCaptor.capture())
val error = authExceptionCaptor.firstValue
@@ -1604,7 +1610,6 @@ public class WebAuthProviderTest {
error.cause?.message,
`is`("Could not find a public key for kid \"key123\"")
)
- mockAPI.shutdown()
}
@Test
@@ -1674,18 +1679,17 @@ public class WebAuthProviderTest {
}
- //TODO: https://auth0team.atlassian.net/browse/SDK-7752
+ @Ignore("Requires security provider fix - see SDK-7752")
@Test
- @Ignore("Fix these failing tests in CI once Roboelectric and other dependencies are updated")
@Throws(Exception::class)
public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() {
val pkce = Mockito.mock(PKCE::class.java)
`when`(pkce.codeChallenge).thenReturn("challenge")
- val mockAPI = AuthenticationAPIMockServer()
- mockAPI.willReturnValidJsonWebKeys()
+ val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java)
val authCallback = mock>()
- val proxyAccount: Auth0 = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, mockAPI.domain)
- proxyAccount.networkingClient = SSLTestUtils.testClient
+ val proxyAccount =
+ Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN)
+ proxyAccount.networkingClient = networkingClient
login(proxyAccount)
.withState("1234567890")
.withNonce("abcdefg")
@@ -1717,12 +1721,19 @@ public class WebAuthProviderTest {
Date(),
"codeScope"
)
+ // Mock JWKS response with valid keys
+ val encoded = Files.readAllBytes(Paths.get("src/test/resources/rsa_jwks.json"))
+ val jwksInputStream: InputStream = ByteArrayInputStream(encoded)
+ val jwksResponse = ServerResponse(200, jwksInputStream, emptyMap())
+ Mockito.doReturn(jwksResponse).`when`(networkingClient).load(
+ eq(proxyAccount.getDomainUrl() + ".well-known/jwks.json"),
+ any()
+ )
Mockito.doAnswer {
callbackCaptor.firstValue.onSuccess(codeCredentials)
null
}.`when`(pkce).getToken(eq("1234"), callbackCaptor.capture())
Assert.assertTrue(resume(intent))
- mockAPI.takeRequest()
ShadowLooper.idleMainLooper()
verify(authCallback).onFailure(authExceptionCaptor.capture())
val error = authExceptionCaptor.firstValue
@@ -1738,7 +1749,6 @@ public class WebAuthProviderTest {
error.cause?.message,
`is`("Could not find a public key for kid \"null\"")
)
- mockAPI.shutdown()
}
@Test
diff --git a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java
index 0c096ba3f..17100bf9f 100644
--- a/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java
+++ b/auth0/src/test/java/com/auth0/android/request/internal/ThreadSwitcherShadow.java
@@ -26,4 +26,9 @@ public ThreadSwitcherShadow() {
public void backgroundThread(Runnable runnable) {
executor.execute(runnable);
}
+
+ @Implementation
+ public void mainThread(Runnable runnable) {
+ executor.execute(runnable);
+ }
}