Skip to content
Draft
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
61 changes: 61 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,67 @@ SecureCredentialsManager manager = new SecureCredentialsManager(apiClient, this,
```
</details>

#### Automatic Retry on Credential Retrieval

Both `CredentialsManager` and `SecureCredentialsManager` support automatic retry with exponential backoff for credential retrieval operations (`getCredentials()` and its variants) that fail due to transient errors. This is particularly useful for handling temporary network issues on mobile devices during credential renewal.

> **Note:** Automatic retry is only supported for credential retrieval operations (`getCredentials()`, `awaitCredentials()`). It does not apply to other operations such as SSO credentials, API credentials, or explicit renewal calls.

**Retryable Errors:**
- Network errors (timeouts, connection lost, DNS failures)
- Rate limiting (HTTP 429)
- Server errors (HTTP 5xx)

**Configuring Retries:**

The `maxRetries` parameter can be set when creating a `CredentialsManager` or `SecureCredentialsManager`. The default value is 0 (no retries). It is recommended to use a maximum of 2 retries.

```kotlin
val storage = SharedPreferencesStorage(this)
val manager = CredentialsManager(
authenticationClient = apiClient,
storage = storage,
maxRetries = 2 // Enable up to 2 retry attempts
)
```

<details>
<summary>Using Java</summary>

```java
Storage storage = new SharedPreferencesStorage(this);
CredentialsManager manager = new CredentialsManager(apiClient, storage, 2);
```
</details>

For `SecureCredentialsManager`:

```kotlin
val storage = SharedPreferencesStorage(this)
val manager = SecureCredentialsManager(
context = this,
account = account,
storage = storage,
maxRetries = 2 // Enable up to 2 retry attempts
)
```

<details>
<summary>Using Java</summary>

```java
Storage storage = new SharedPreferencesStorage(this);
SecureCredentialsManager manager = new SecureCredentialsManager(this, account, storage, 2);
```
</details>

**Important Considerations:**

- **Scope:** Automatic retry only works for credential retrieval operations (`getCredentials()`, `awaitCredentials()` and their overloads). Other operations like getting SSO credentials or API credentials do not support automatic retry.
- **Auth0 Tenant Configuration**: Ensure your Auth0 tenant is configured with a minimum **180-second refresh token rotation overlap period** to safely retry credential renewals without requiring user re-authentication.
- **Exponential Backoff**: The retry mechanism uses exponential backoff with delays of 0.5s, 1s, 2s, etc., between attempts.
- **Non-Retryable Errors**: Errors like invalid refresh tokens or authentication failures will not be retried automatically.

#### Requiring Authentication

You can require the user authentication to obtain credentials. This will make the manager prompt the user with the device's configured Lock Screen, which they must pass correctly in order to obtain the credentials. **This feature is only available on devices where the user has setup a secured Lock Screen** (PIN, Pattern, Password or Fingerprint).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ public class AuthenticationException : Auth0Exception {
public val isTooManyAttempts: Boolean
get() = "too_many_attempts" == code

/**
* Returns true if this error is retryable with exponential backoff.
* Retryable errors include network errors, rate limiting (429), and server errors (5xx).
*/
public val isRetryable: Boolean
get() = isNetworkError || statusCode == 429 || statusCode in 500..599

internal companion object {
internal const val ERROR_VALUE_AUTHENTICATION_CANCELED = "a0.authentication_canceled"
internal const val ERROR_KEY_URI_NULL = "a0.auth.authorize_uri"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
authenticationClient: AuthenticationAPIClient,
storage: Storage,
jwtDecoder: JWTDecoder,
private val serialExecutor: Executor
private val serialExecutor: Executor,
private val maxRetries: Int
) : BaseCredentialsManager(authenticationClient, storage, jwtDecoder) {

private val gson: Gson = GsonProvider.gson
Expand All @@ -39,12 +40,18 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
*
* @param authenticationClient the Auth0 Authentication client to refresh credentials with.
* @param storage the storage to use for the credentials.
* @param maxRetries the maximum number of retry attempts for credential renewal when encountering transient errors. Default is 0 (no retries).
*/
public constructor(authenticationClient: AuthenticationAPIClient, storage: Storage) : this(
public constructor(
authenticationClient: AuthenticationAPIClient,
storage: Storage,
maxRetries: Int = 0
) : this(
authenticationClient,
storage,
JWTDecoder(),
Executors.newSingleThreadExecutor()
Executors.newSingleThreadExecutor(),
maxRetries.coerceAtLeast(0)
)

public override val userProfile: UserProfile?
Expand Down Expand Up @@ -469,6 +476,21 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
headers: Map<String, String>,
forceRefresh: Boolean,
callback: Callback<Credentials, CredentialsManagerException>
) {
getCredentialsWithRetry(scope, minTtl, parameters, headers, forceRefresh, 0, callback)
}

/**
* Internal method that implements retry logic for credential retrieval.
*/
private fun getCredentialsWithRetry(
scope: String?,
minTtl: Int,
parameters: Map<String, String>,
headers: Map<String, String>,
forceRefresh: Boolean,
retryCount: Int,
callback: Callback<Credentials, CredentialsManagerException>
) {
serialExecutor.execute {
val accessToken = storage.retrieveString(KEY_ACCESS_TOKEN)
Expand Down Expand Up @@ -544,6 +566,26 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
saveCredentials(credentials)
callback.onSuccess(credentials)
} catch (error: AuthenticationException) {
if (shouldRetryRenewal(error, retryCount)) {
val delay = calculateRetryDelay(retryCount)
Log.d(
TAG,
"Retrying credential renewal (attempt ${retryCount + 1}/$maxRetries) after ${delay}ms due to retryable error: ${error.getDescription()}"
)
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
getCredentialsWithRetry(
scope,
minTtl,
parameters,
headers,
forceRefresh,
retryCount + 1,
callback
)
}, delay)
return@execute
}

val exception = when {
error.isRefreshTokenDeleted || error.isInvalidRefreshToken -> CredentialsManagerException.Code.RENEW_FAILED

Expand Down Expand Up @@ -756,6 +798,29 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
return Credentials(idToken, accessToken, tokenType, refreshToken, expiresAt, scope)
}

/**
* Determines if a credential renewal operation should be retried based on the error and current retry count.
*
* @param error the error that occurred during the renewal attempt
* @param retryCount the current number of retry attempts
* @return true if the operation should be retried, false otherwise
*/
private fun shouldRetryRenewal(error: AuthenticationException, retryCount: Int): Boolean {
return retryCount < maxRetries && error.isRetryable
}

/**
* Calculates the exponential backoff delay for a given retry attempt.
* Formula: 2^retryCount * 0.5 seconds
*
* @param retryCount the current retry attempt number (0-indexed)
* @return the delay in milliseconds
*/
private fun calculateRetryDelay(retryCount: Int): Long {
val delaySeconds = Math.pow(2.0, retryCount.toDouble()) * 0.5
return (delaySeconds * 1000).toLong()
}

private companion object {
private const val KEY_ACCESS_TOKEN = "com.auth0.access_token"
private const val KEY_REFRESH_TOKEN = "com.auth0.refresh_token"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
private val fragmentActivity: WeakReference<FragmentActivity>? = null,
private val localAuthenticationOptions: LocalAuthenticationOptions? = null,
private val localAuthenticationManagerFactory: LocalAuthenticationManagerFactory? = null,
private val maxRetries: Int = 0
) : BaseCredentialsManager(apiClient, storage, jwtDecoder) {
private val gson: Gson = GsonProvider.gson

Expand All @@ -52,16 +53,19 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* @param context a valid context
* @param auth0 the Auth0 account information to use
* @param storage the storage implementation to use
* @param maxRetries the maximum number of retry attempts for credential renewal when encountering transient errors. Default is 0 (no retries).
*/
public constructor(
context: Context,
auth0: Auth0,
storage: Storage,
maxRetries: Int = 0
) : this(
AuthenticationAPIClient(auth0),
context,
auth0,
storage
storage,
maxRetries
)

/**
Expand All @@ -79,18 +83,21 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* @param context a valid context
* @param auth0 the Auth0 account information to use
* @param storage the storage implementation to use
* @param maxRetries the maximum number of retry attempts for credential renewal when encountering transient errors. Default is 0 (no retries).
*/
public constructor(
apiClient: AuthenticationAPIClient,
context: Context,
auth0: Auth0,
storage: Storage
storage: Storage,
maxRetries: Int = 0
) : this(
apiClient,
storage,
CryptoUtil(context, storage, KEY_ALIAS),
JWTDecoder(),
auth0.executor
auth0.executor,
maxRetries = maxRetries
)


Expand All @@ -102,20 +109,23 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* @param storage the storage implementation to use
* @param fragmentActivity the FragmentActivity to use for the biometric authentication
* @param localAuthenticationOptions the options of type [LocalAuthenticationOptions] to use for the biometric authentication
* @param maxRetries the maximum number of retry attempts for credential renewal when encountering transient errors. Default is 0 (no retries).
*/
public constructor(
context: Context,
auth0: Auth0,
storage: Storage,
fragmentActivity: FragmentActivity,
localAuthenticationOptions: LocalAuthenticationOptions
localAuthenticationOptions: LocalAuthenticationOptions,
maxRetries: Int = 0
) : this(
AuthenticationAPIClient(auth0),
context,
auth0,
storage,
fragmentActivity,
localAuthenticationOptions
localAuthenticationOptions,
maxRetries
)


Expand Down Expand Up @@ -145,14 +155,16 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* @param storage the storage implementation to use
* @param fragmentActivity the FragmentActivity to use for the biometric authentication
* @param localAuthenticationOptions the options of type [LocalAuthenticationOptions] to use for the biometric authentication
* @param maxRetries the maximum number of retry attempts for credential renewal when encountering transient errors. Default is 0 (no retries).
*/
public constructor(
apiClient: AuthenticationAPIClient,
context: Context,
auth0: Auth0,
storage: Storage,
fragmentActivity: FragmentActivity,
localAuthenticationOptions: LocalAuthenticationOptions
localAuthenticationOptions: LocalAuthenticationOptions,
maxRetries: Int = 0
) : this(
apiClient,
storage,
Expand All @@ -161,7 +173,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
auth0.executor,
WeakReference(fragmentActivity),
localAuthenticationOptions,
DefaultLocalAuthenticationManagerFactory()
DefaultLocalAuthenticationManagerFactory(),
maxRetries
)


Expand Down
Loading
Loading