From 7b5611a1881132d7e93ee30df9857ea40e7ce895 Mon Sep 17 00:00:00 2001 From: Prince Mathew <17837162+pmathew92@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:57:14 +0530 Subject: [PATCH 1/6] chore: updated the RL wrapper installation path (#917) --- .github/actions/rl-scanner/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 845f6ca6ffcc95806f8e7ed1bbe6dd8e5317e789 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Fri, 20 Feb 2026 13:24:55 +0530 Subject: [PATCH 2/6] Add DEFAULT_MIN_TTL boundary tests for CredentialsManager and SecureCredentialsManager --- .../storage/BaseCredentialsManager.kt | 15 ++- .../storage/CredentialsManager.kt | 6 +- .../storage/SecureCredentialsManager.kt | 6 +- .../storage/SharedPreferencesStorage.kt | 4 + .../android/authentication/storage/Storage.kt | 5 + .../storage/CredentialsManagerTest.kt | 103 +++++++++++++++++- .../storage/SecureCredentialsManagerTest.kt | 100 ++++++++++++++++- .../storage/SharedPreferencesStorageTest.java | 9 ++ .../android/provider/WebAuthProviderTest.kt | 44 +++++--- .../internal/ThreadSwitcherShadow.java | 5 + 10 files changed, 268 insertions(+), 29 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 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..0e967fa2f 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()) } /** 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..14c6fc0f9 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) } /** @@ -779,7 +779,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..4e0644045 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..3423fc9de 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) @@ -1770,6 +1771,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 +1910,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 +1927,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/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index 45fb805b2..e6ff95f3b 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 @@ -2501,6 +2502,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 +3432,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..1939fdd78 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 @@ -57,6 +56,7 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1539,18 +1539,17 @@ public class WebAuthProviderTest { } - // TODO: https://auth0team.atlassian.net/browse/SDK-7752 + @ConscryptMode(ConscryptMode.Mode.OFF) @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.spy(DefaultClient()) 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 + @ConscryptMode(ConscryptMode.Mode.OFF) @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.spy(DefaultClient()) 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); + } } From cfd3ba9222767edadbefdb7f01b4b82741079647 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 13:40:59 +0530 Subject: [PATCH 3/6] Handled review comments --- V4_MIGRATION_GUIDE.md | 82 ++++++++++++++++++- .../storage/CredentialsManager.kt | 8 +- .../storage/SecureCredentialsManager.kt | 5 +- .../android/authentication/storage/Storage.kt | 2 +- .../storage/CredentialsManagerTest.kt | 8 +- ...reCredentialsManagerBiometricPolicyTest.kt | 2 +- .../storage/SecureCredentialsManagerTest.kt | 13 +-- 7 files changed, 86 insertions(+), 34 deletions(-) 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/CredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt index 0e967fa2f..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 @@ -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 14c6fc0f9..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 @@ -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") } 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 4e0644045..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 @@ -79,5 +79,5 @@ public interface Storage { /** * Removes all values from the storage. */ - public fun removeAll() + 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 3423fc9de..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 @@ -1480,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) } 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 e6ff95f3b..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 @@ -770,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 @@ -867,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 @@ -2153,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) } From e6111b94c5502e55ca1dd08061143a4f7fc0d660 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 14:12:23 +0530 Subject: [PATCH 4/6] Fixing UT failure case --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1939fdd78..f974d00b4 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -1545,7 +1545,7 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java) val authCallback = mock>() val proxyAccount = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) @@ -1685,7 +1685,7 @@ public class WebAuthProviderTest { public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { val pkce = Mockito.mock(PKCE::class.java) `when`(pkce.codeChallenge).thenReturn("challenge") - val networkingClient: NetworkingClient = Mockito.spy(DefaultClient()) + val networkingClient: NetworkingClient = Mockito.mock(NetworkingClient::class.java) val authCallback = mock>() val proxyAccount = Auth0.getInstance(JwtTestUtils.EXPECTED_AUDIENCE, JwtTestUtils.EXPECTED_BASE_DOMAIN) From b5a495aa6ed73774f7c10496192d4496b3853c5c Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 16:09:48 +0530 Subject: [PATCH 5/6] Removed @ConscryptMode --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 3 --- 1 file changed, 3 deletions(-) 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 f974d00b4..cbf52f501 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -56,7 +56,6 @@ import org.mockito.MockitoAnnotations import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config -import org.robolectric.annotation.ConscryptMode import org.robolectric.shadows.ShadowLooper import java.io.ByteArrayInputStream import java.io.InputStream @@ -1539,7 +1538,6 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1679,7 +1677,6 @@ public class WebAuthProviderTest { } - @ConscryptMode(ConscryptMode.Mode.OFF) @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() { From 7aa2dbc35fea6a9f382d2613bc2acd99ee8c8fca Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Feb 2026 16:43:22 +0530 Subject: [PATCH 6/6] reverted @ignore test cases will handle in SDK-7752 --- .../java/com/auth0/android/provider/WebAuthProviderTest.kt | 3 +++ 1 file changed, 3 insertions(+) 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 cbf52f501..038708839 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -53,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 @@ -1538,6 +1539,7 @@ public class WebAuthProviderTest { } + @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenRSAKeyIsMissingFromJWKSet() { @@ -1677,6 +1679,7 @@ public class WebAuthProviderTest { } + @Ignore("Requires security provider fix - see SDK-7752") @Test @Throws(Exception::class) public fun shouldFailToResumeLoginWhenKeyIdIsMissingFromIdTokenHeader() {