Skip to content

Commit df9c408

Browse files
authored
feat: Add Storage.removeAll(), default minTTL of 60s, and fix @ignore tests (#918)
2 parents ec444ff + 7aa2dbc commit df9c408

File tree

13 files changed

+354
-63
lines changed

13 files changed

+354
-63
lines changed

.github/actions/rl-scanner/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ runs:
3333
- name: Install RL Wrapper
3434
shell: bash
3535
run: |
36-
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"
36+
pip install rl-wrapper --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python/simple"
3737
3838
- name: Run RL Scanner
3939
shell: bash

V4_MIGRATION_GUIDE.md

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
# Migration Guide from SDK v3 to v4
22

3-
## Overview
3+
> **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.
44
5-
v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest
6-
Android development environment. This guide documents the changes required when migrating from v3 to
7-
v4.
5+
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.
6+
7+
---
8+
9+
## Table of Contents
10+
11+
- [**Requirements Changes**](#requirements-changes)
12+
+ [Java Version](#java-version)
13+
+ [Gradle and Android Gradle Plugin](#gradle-and-android-gradle-plugin)
14+
+ [Kotlin Version](#kotlin-version)
15+
- [**Breaking Changes**](#breaking-changes)
16+
+ [Classes Removed](#classes-removed)
17+
+ [DPoP Configuration Moved to Builder](#dpop-configuration-moved-to-builder)
18+
- [**Default Values Changed**](#default-values-changed)
19+
+ [Credentials Manager minTTL](#credentials-manager-minttl)
20+
- [**Behavior Changes**](#behavior-changes)
21+
+ [clearCredentials() Now Clears All Storage](#clearCredentials-now-clears-all-storage)
22+
+ [Storage Interface: New removeAll() Method](#storage-interface-new-removeall-method)
23+
- [**Dependency Changes**](#dependency-changes)
24+
+ [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency)
25+
+ [DefaultClient.Builder](#defaultclientbuilder)
26+
27+
---
828

929
## Requirements Changes
1030

@@ -103,6 +123,60 @@ WebAuthProvider
103123
This change ensures that DPoP configuration is scoped to individual login requests rather than
104124
persisting across the entire application lifecycle.
105125

126+
## Default Values Changed
127+
128+
### Credentials Manager `minTTL`
129+
130+
**Change:** The default `minTtl` value changed from `0` to `60` seconds.
131+
132+
This change affects the following Credentials Manager methods:
133+
134+
- `getCredentials(callback)` / `awaitCredentials()`
135+
- `getCredentials(scope, minTtl, callback)` / `awaitCredentials(scope, minTtl)`
136+
- `getCredentials(scope, minTtl, parameters, callback)` / `awaitCredentials(scope, minTtl, parameters)`
137+
- `getCredentials(scope, minTtl, parameters, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, forceRefresh)`
138+
- `getCredentials(scope, minTtl, parameters, headers, forceRefresh, callback)` / `awaitCredentials(scope, minTtl, parameters, headers, forceRefresh)`
139+
- `hasValidCredentials()`
140+
141+
**Impact:** Credentials will be renewed if they expire within 60 seconds, instead of only when already expired.
142+
143+
<details>
144+
<summary>Migration example</summary>
145+
146+
```kotlin
147+
// v3 - minTtl defaulted to 0, had to be set explicitly
148+
credentialsManager.getCredentials(scope = null, minTtl = 60, callback = callback)
149+
150+
// v4 - minTtl defaults to 60 seconds
151+
credentialsManager.getCredentials(callback)
152+
153+
// v4 - use 0 to restore v3 behavior
154+
credentialsManager.getCredentials(scope = null, minTtl = 0, callback = callback)
155+
```
156+
</details>
157+
158+
**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.
159+
160+
## Behavior Changes
161+
162+
### `clearCredentials()` Now Clears All Storage
163+
164+
**Change:** `clearCredentials()` now calls `Storage.removeAll()` instead of removing individual credential keys.
165+
166+
In v3, `clearCredentials()` removed only specific credential keys (access token, refresh token, ID token, etc.) from the underlying `Storage`.
167+
168+
In v4, `clearCredentials()` calls `Storage.removeAll()`, which clears **all** values in the storage — including any API credentials stored for specific audiences.
169+
170+
**Impact:** If you need to remove only the primary credentials while preserving other stored data, consider using a separate `Storage` instance for API credentials.
171+
172+
**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.
173+
174+
### `Storage` Interface: New `removeAll()` Method
175+
176+
**Change:** The `Storage` interface now includes a `removeAll()` method with a default empty implementation.
177+
178+
**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()`.
179+
106180
## Dependency Changes
107181

108182
### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency)

auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ public abstract class BaseCredentialsManager internal constructor(
2222
) {
2323
private var _clock: Clock = ClockImpl()
2424

25+
public companion object {
26+
/**
27+
* Default minimum time to live (in seconds) for the access token.
28+
* When retrieving credentials, if the access token has less than this amount of time
29+
* remaining before expiration, it will be automatically renewed.
30+
* This ensures the access token is valid for at least a short window after retrieval,
31+
* preventing downstream API call failures from nearly-expired tokens.
32+
*/
33+
public const val DEFAULT_MIN_TTL: Int = 60
34+
}
35+
2536
/**
2637
* Updates the clock instance used for expiration verification purposes.
2738
* 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(
8394
public abstract fun getApiCredentials(
8495
audience: String,
8596
scope: String? = null,
86-
minTtl: Int = 0,
97+
minTtl: Int = DEFAULT_MIN_TTL,
8798
parameters: Map<String, String> = emptyMap(),
8899
headers: Map<String, String> = emptyMap(),
89100
callback: Callback<APICredentials, CredentialsManagerException>
@@ -139,7 +150,7 @@ public abstract class BaseCredentialsManager internal constructor(
139150
public abstract suspend fun awaitApiCredentials(
140151
audience: String,
141152
scope: String? = null,
142-
minTtl: Int = 0,
153+
minTtl: Int = DEFAULT_MIN_TTL,
143154
parameters: Map<String, String> = emptyMap(),
144155
headers: Map<String, String> = emptyMap()
145156
): APICredentials

auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
244244
@JvmSynthetic
245245
@Throws(CredentialsManagerException::class)
246246
override suspend fun awaitCredentials(): Credentials {
247-
return awaitCredentials(null, 0)
247+
return awaitCredentials(null, DEFAULT_MIN_TTL)
248248
}
249249

250250
/**
@@ -390,7 +390,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
390390
* @param callback the callback that will receive a valid [Credentials] or the [CredentialsManagerException].
391391
*/
392392
override fun getCredentials(callback: Callback<Credentials, CredentialsManagerException>) {
393-
getCredentials(null, 0, callback)
393+
getCredentials(null, DEFAULT_MIN_TTL, callback)
394394
}
395395

396396
/**
@@ -702,7 +702,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
702702
* @return whether there are valid credentials stored on this manager.
703703
*/
704704
override fun hasValidCredentials(): Boolean {
705-
return hasValidCredentials(0)
705+
return hasValidCredentials(DEFAULT_MIN_TTL.toLong())
706706
}
707707

708708
/**
@@ -727,13 +727,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
727727
* Removes the credentials from the storage if present.
728728
*/
729729
override fun clearCredentials() {
730-
storage.remove(KEY_ACCESS_TOKEN)
731-
storage.remove(KEY_REFRESH_TOKEN)
732-
storage.remove(KEY_ID_TOKEN)
733-
storage.remove(KEY_TOKEN_TYPE)
734-
storage.remove(KEY_EXPIRES_AT)
735-
storage.remove(KEY_SCOPE)
736-
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
730+
storage.removeAll()
737731
}
738732

739733
/**

auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
409409
@JvmSynthetic
410410
@Throws(CredentialsManagerException::class)
411411
override suspend fun awaitCredentials(): Credentials {
412-
return awaitCredentials(null, 0)
412+
return awaitCredentials(null, DEFAULT_MIN_TTL)
413413
}
414414

415415
/**
@@ -579,7 +579,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
579579
override fun getCredentials(
580580
callback: Callback<Credentials, CredentialsManagerException>
581581
) {
582-
getCredentials(null, 0, callback)
582+
getCredentials(null, DEFAULT_MIN_TTL, callback)
583583
}
584584

585585
/**
@@ -754,10 +754,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
754754
* Delete the stored credentials
755755
*/
756756
override fun clearCredentials() {
757-
storage.remove(KEY_CREDENTIALS)
758-
storage.remove(KEY_EXPIRES_AT)
759-
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
760-
storage.remove(KEY_CAN_REFRESH)
757+
storage.removeAll()
761758
clearBiometricSession()
762759
Log.d(TAG, "Credentials were just removed from the storage")
763760
}
@@ -779,7 +776,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
779776
* @return whether this manager contains a valid non-expired pair of credentials or not.
780777
*/
781778
override fun hasValidCredentials(): Boolean {
782-
return hasValidCredentials(0)
779+
return hasValidCredentials(DEFAULT_MIN_TTL.toLong())
783780
}
784781

785782
/**

auth0/src/main/java/com/auth0/android/authentication/storage/SharedPreferencesStorage.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ public class SharedPreferencesStorage @JvmOverloads constructor(
7575
sp.edit().remove(name).apply()
7676
}
7777

78+
override fun removeAll() {
79+
sp.edit().clear().apply()
80+
}
81+
7882
private companion object {
7983
private const val SHARED_PREFERENCES_NAME = "com.auth0.authentication.storage"
8084
}

auth0/src/main/java/com/auth0/android/authentication/storage/Storage.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,9 @@ public interface Storage {
7575
* @param name the name of the value to remove.
7676
*/
7777
public fun remove(name: String)
78+
79+
/**
80+
* Removes all values from the storage.
81+
*/
82+
public fun removeAll() {}
7883
}

auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt

Lines changed: 101 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.auth0.android.authentication.storage
33
import com.auth0.android.NetworkErrorException
44
import com.auth0.android.authentication.AuthenticationAPIClient
55
import com.auth0.android.authentication.AuthenticationException
6+
import com.auth0.android.authentication.storage.BaseCredentialsManager.Companion.DEFAULT_MIN_TTL
67
import com.auth0.android.callback.Callback
78
import com.auth0.android.request.Request
89
import com.auth0.android.request.internal.GsonProvider
@@ -672,7 +673,7 @@ public class CredentialsManagerTest {
672673
Mockito.`when`(
673674
client.renewAuth("refresh_token", "audience")
674675
).thenReturn(request)
675-
val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 1 * 1000)
676+
val newDate = Date(CredentialsMock.CURRENT_TIME_MS + (DEFAULT_MIN_TTL + 10) * 1000L)
676677
val jwtMock = mock<Jwt>()
677678
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
678679
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
@@ -1479,13 +1480,7 @@ public class CredentialsManagerTest {
14791480
@Test
14801481
public fun shouldClearCredentials() {
14811482
manager.clearCredentials()
1482-
verify(storage).remove("com.auth0.id_token")
1483-
verify(storage).remove("com.auth0.access_token")
1484-
verify(storage).remove("com.auth0.refresh_token")
1485-
verify(storage).remove("com.auth0.token_type")
1486-
verify(storage).remove("com.auth0.expires_at")
1487-
verify(storage).remove("com.auth0.scope")
1488-
verify(storage).remove("com.auth0.cache_expires_at")
1483+
verify(storage).removeAll()
14891484
verifyNoMoreInteractions(storage)
14901485
}
14911486

@@ -1770,6 +1765,103 @@ public class CredentialsManagerTest {
17701765
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
17711766
}
17721767

1768+
@Test
1769+
public fun shouldRenewCredentialsViaCallbackWhenTokenExpiresWithinDefaultMinTtl() {
1770+
// Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
1771+
val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
1772+
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
1773+
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
1774+
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
1775+
Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type")
1776+
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
1777+
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
1778+
.thenReturn(expirationTime)
1779+
Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope")
1780+
Mockito.`when`(
1781+
client.renewAuth("refreshToken")
1782+
).thenReturn(request)
1783+
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
1784+
val jwtMock = mock<Jwt>()
1785+
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
1786+
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
1787+
1788+
val renewedCredentials =
1789+
Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
1790+
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
1791+
// Use no-arg getCredentials which now uses DEFAULT_MIN_TTL
1792+
manager.getCredentials(callback)
1793+
verify(callback).onSuccess(
1794+
credentialsCaptor.capture()
1795+
)
1796+
// Verify renewal was triggered (client.renewAuth was called)
1797+
verify(client).renewAuth("refreshToken")
1798+
val retrievedCredentials = credentialsCaptor.firstValue
1799+
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
1800+
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("newId"))
1801+
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess"))
1802+
}
1803+
1804+
@Test
1805+
@ExperimentalCoroutinesApi
1806+
public fun shouldAwaitRenewedCredentialsWhenTokenExpiresWithinDefaultMinTtl(): Unit = runTest {
1807+
// Token expires in 30 seconds, which is within DEFAULT_MIN_TTL (60s)
1808+
val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
1809+
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
1810+
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
1811+
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
1812+
Mockito.`when`(storage.retrieveString("com.auth0.token_type")).thenReturn("type")
1813+
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
1814+
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
1815+
.thenReturn(expirationTime)
1816+
Mockito.`when`(storage.retrieveString("com.auth0.scope")).thenReturn("scope")
1817+
Mockito.`when`(
1818+
client.renewAuth("refreshToken")
1819+
).thenReturn(request)
1820+
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
1821+
val jwtMock = mock<Jwt>()
1822+
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
1823+
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
1824+
1825+
val renewedCredentials =
1826+
Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "newScope")
1827+
Mockito.`when`(request.execute()).thenReturn(renewedCredentials)
1828+
// Use no-arg awaitCredentials which now uses DEFAULT_MIN_TTL
1829+
val result = manager.awaitCredentials()
1830+
// Verify renewal was triggered
1831+
verify(client).renewAuth("refreshToken")
1832+
MatcherAssert.assertThat(result, Is.`is`(Matchers.notNullValue()))
1833+
MatcherAssert.assertThat(result.idToken, Is.`is`("newId"))
1834+
MatcherAssert.assertThat(result.accessToken, Is.`is`("newAccess"))
1835+
}
1836+
1837+
@Test
1838+
public fun shouldNotHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlAndNoRefreshToken() {
1839+
// Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), and no refresh token
1840+
val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
1841+
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
1842+
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
1843+
.thenReturn(expirationTime)
1844+
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn(null)
1845+
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
1846+
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
1847+
// No-arg hasValidCredentials now uses DEFAULT_MIN_TTL, so token expiring in 30s is invalid
1848+
Assert.assertFalse(manager.hasValidCredentials())
1849+
}
1850+
1851+
@Test
1852+
public fun shouldHaveValidCredentialsWhenTokenExpiresWithinDefaultMinTtlButRefreshTokenAvailable() {
1853+
// Token expires in 30 seconds, within DEFAULT_MIN_TTL (60s), but refresh token is available
1854+
val expirationTime = CredentialsMock.CURRENT_TIME_MS + 30 * 1000
1855+
Mockito.`when`(storage.retrieveLong("com.auth0.expires_at")).thenReturn(expirationTime)
1856+
Mockito.`when`(storage.retrieveLong("com.auth0.cache_expires_at"))
1857+
.thenReturn(expirationTime)
1858+
Mockito.`when`(storage.retrieveString("com.auth0.refresh_token")).thenReturn("refreshToken")
1859+
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
1860+
Mockito.`when`(storage.retrieveString("com.auth0.access_token")).thenReturn("accessToken")
1861+
// Even though token expires within DEFAULT_MIN_TTL, refresh token makes it valid
1862+
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
1863+
}
1864+
17731865
@Test
17741866
public fun shouldNotHaveCredentialsWhenAccessTokenAndIdTokenAreMissing() {
17751867
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn(null)
@@ -1812,7 +1904,7 @@ public class CredentialsManagerTest {
18121904
//now, update the clock and retry
18131905
manager.setClock(object : Clock {
18141906
override fun getCurrentTimeMillis(): Long {
1815-
return CredentialsMock.CURRENT_TIME_MS - 1000
1907+
return CredentialsMock.CURRENT_TIME_MS - (DEFAULT_MIN_TTL * 1000 + 1000)
18161908
}
18171909
})
18181910
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
@@ -1829,7 +1921,6 @@ public class CredentialsManagerTest {
18291921
})
18301922
}
18311923

1832-
18331924
@Test
18341925
public fun shouldAddParametersToRequest() {
18351926
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")

0 commit comments

Comments
 (0)