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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/rl-scanner/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the V4_Migration guide for this change. Refer the Swift one

}

/**
* 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.
Expand Down Expand Up @@ -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<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap(),
callback: Callback<APICredentials, CredentialsManagerException>
Expand Down Expand Up @@ -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<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap()
): APICredentials
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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<Credentials, CredentialsManagerException>) {
getCredentials(null, 0, callback)
getCredentials(null, DEFAULT_MIN_TTL, callback)
}

/**
Expand Down Expand Up @@ -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())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -579,7 +579,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
override fun getCredentials(
callback: Callback<Credentials, CredentialsManagerException>
) {
getCredentials(null, 0, callback)
getCredentials(null, DEFAULT_MIN_TTL, callback)
}

/**
Expand Down Expand Up @@ -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())
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Contributor

@pmathew92 pmathew92 Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a corresponding method in the BaseCredentialsManager class , which in turn calls this method. Implement the actual method in the CredentialsManager and SecureCredentialsManager class. Update the V4_ Migration guide with these changes
Better is to update the clear method implementation of the ~CredentialsManagerandSecureCredentialsManager` class to call the removeAll method. And update the same in the behaviour change section in the migration guide and other required doc changes

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding removeAll() as an abstract method to the public Storage interface is a breaking API change for any external/custom Storage implementations (source-breaking for Kotlin, and can lead to AbstractMethodError at runtime for already-compiled implementations). Consider introducing a new sub-interface (e.g., ClearableStorage), providing a backward-compatible alternative (e.g., an extension function), and/or documenting this as a breaking change in the appropriate migration guide/changelog.

Suggested change
public fun removeAll()
public fun removeAll() { }

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
Expand Down Expand Up @@ -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<Jwt>()
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<Jwt>()
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)
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this +1000 ?

}
})
MatcherAssert.assertThat(manager.hasValidCredentials(), Is.`is`(true))
Expand All @@ -1829,7 +1927,6 @@ public class CredentialsManagerTest {
})
}


@Test
public fun shouldAddParametersToRequest() {
Mockito.`when`(storage.retrieveString("com.auth0.id_token")).thenReturn("idToken")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Jwt>()
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<Jwt>()
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
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
Loading
Loading