diff --git a/A0Auth0.podspec b/A0Auth0.podspec
index bf37d2a3..d9571722 100644
--- a/A0Auth0.podspec
+++ b/A0Auth0.podspec
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.source_files = 'ios/**/*.{h,m,mm,swift}'
s.requires_arc = true
- s.dependency 'Auth0', '2.16.1'
+ s.dependency 'Auth0', '2.16.2'
install_modules_dependencies(s)
end
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 62a11e86..a789e7c3 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -13,6 +13,13 @@
- [Set global headers during initialization](#set-global-headers-during-initialization)
- [Using custom headers with Auth0Provider component](#using-custom-headers-with-auth0provider-component)
- [Set request-specific headers](#set-request-specific-headers)
+- [Credential Renewal Retry](#credential-renewal-retry)
+ - [Overview](#credential-renewal-retry-overview)
+ - [Prerequisites](#credential-renewal-retry-prerequisites)
+ - [Using Retry with Hooks](#using-retry-with-hooks)
+ - [Using Retry with Auth0 Class](#using-retry-with-auth0-class)
+ - [Platform Support](#credential-renewal-retry-platform-support)
+ - [Error Handling](#credential-renewal-retry-error-handling)
- [Biometric Authentication](#biometric-authentication)
- [Biometric Policy Types](#biometric-policy-types)
- [Using with Auth0Provider (Hooks)](#using-with-auth0provider-hooks)
@@ -259,6 +266,194 @@ auth0.auth
.catch(console.error);
```
+## Credential Renewal Retry
+
+> **Platform Support:** iOS only.
+
+Automatic retry mechanism for credential renewal to improve reliability in unstable network conditions, particularly important for mobile applications with refresh token rotation enabled.
+
+
+
+### Overview
+
+When your application operates on unstable mobile networks, credential renewal requests may fail due to transient network issues. The `maxRetries` configuration option enables automatic retry with exponential backoff for the following error scenarios:
+
+- **Network errors**: Connection timeouts, DNS failures, unreachable hosts
+- **Rate limiting**: HTTP 429 (Too Many Requests)
+- **Server errors**: HTTP 5xx responses
+
+> **Important:** While the retry mechanism is particularly valuable for refresh token rotation (RRT) scenarios, it can be used to improve credential renewal reliability in any configuration, including non-RRT deployments. The retry logic helps handle transient network failures regardless of your token rotation strategy.
+
+**Example scenario with Refresh Token Rotation:**
+
+1. Request A calls `getCredentials()` and starts a token refresh
+2. Request A successfully hits the server and gets new credentials
+3. Request A fails on the way back (network issue), never reaching the client
+4. The retry mechanism automatically retries the failed request using the same (old) refresh token
+5. The retry succeeds within the refresh token rotation overlap window
+
+> **Critical for RRT:** If you have refresh token rotation enabled, you **must** configure a token overlap period of at least **180 seconds (3 minutes)** in your Auth0 tenant. This overlap window allows retries to succeed using the old refresh token before it expires, preventing users from being locked out due to network failures.
+
+
+
+### Prerequisites
+
+To use the retry mechanism:
+
+1. **SDK Version**: Requires react-native-auth0 v5.4.0 or later
+2. **Scope**: Ensure your authentication requests include the `offline_access` scope to receive refresh tokens
+
+**Additional requirements for Refresh Token Rotation:**
+
+If you have refresh token rotation enabled in your Auth0 tenant:
+
+1. **Token Overlap Period**: Configure an overlap period of at least **180 seconds (3 minutes)** in your Auth0 tenant settings. This is **critical** to ensure retries can succeed using the old refresh token before it expires.
+
+
+
+### Using Retry with Hooks
+
+```jsx
+import React from 'react';
+import { View, Button, Alert } from 'react-native';
+import { Auth0Provider, useAuth0 } from 'react-native-auth0';
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+function MyComponent() {
+ const { getCredentials } = useAuth0();
+
+ const fetchCredentialsWithRetry = async () => {
+ try {
+ // The retry mechanism is automatically applied to all credential renewal attempts
+ const credentials = await getCredentials();
+
+ console.log('Access Token:', credentials.accessToken);
+ // Use credentials for API calls...
+ } catch (error) {
+ console.error('Failed to get credentials after retries:', error);
+ Alert.alert(
+ 'Error',
+ 'Unable to refresh credentials. Please log in again.'
+ );
+ }
+ };
+
+ return (
+
+
+
+ );
+}
+```
+
+
+
+### Using Retry with Auth0 Class
+
+```js
+import Auth0 from 'react-native-auth0';
+
+// Configure retry mechanism at initialization (iOS only)
+const auth0 = new Auth0({
+ domain: 'YOUR_AUTH0_DOMAIN',
+ clientId: 'YOUR_AUTH0_CLIENT_ID',
+ maxRetries: 2, // Recommended maximum of 2 retries
+});
+
+// Get credentials - retry mechanism is automatically applied
+try {
+ const credentials = await auth0.credentialsManager.getCredentials();
+
+ console.log('Access Token:', credentials.accessToken);
+} catch (error) {
+ console.error('Credential renewal failed after retries:', error);
+}
+```
+
+
+
+### Platform Support
+
+| Platform | Support | Behavior |
+| ----------- | -------------------- | ---------------------------------------------------------------------- |
+| **iOS** | ✅ Full Support | Uses exponential backoff retry with Auth0.swift v2.14+ |
+| **Android** | ⚠️ Parameter Ignored | Auth0.Android SDK does not currently support retry configuration |
+| **Web** | ⚠️ Parameter Ignored | @auth0/auth0-spa-js SDK does not currently support retry configuration |
+
+**Default Behavior:**
+
+- `maxRetries` defaults to **0** (no retries) to maintain backward compatibility
+- Recommended maximum: **2 retries**
+- Each retry uses exponential backoff to avoid overwhelming the server
+
+
+
+### Error Handling
+
+The retry mechanism only retries on transient, recoverable errors. The following errors will **not** trigger a retry:
+
+- Invalid refresh token
+- Refresh token expired
+- Refresh token revoked
+- Client authentication failures
+- Authorization errors (insufficient permissions)
+
+Example with comprehensive error handling:
+
+```jsx
+import { useAuth0 } from 'react-native-auth0';
+
+function MyComponent() {
+ const { getCredentials, authorize } = useAuth0();
+
+ const fetchCredentials = async () => {
+ try {
+ const credentials = await getCredentials(
+ undefined,
+ undefined,
+ undefined,
+ false,
+ 2
+ );
+ return credentials;
+ } catch (error) {
+ // Check if it's a non-retryable error that requires re-authentication
+ if (
+ error.code === 'NO_REFRESH_TOKEN' ||
+ error.code === 'RENEW_FAILED' ||
+ error.message?.includes('refresh token')
+ ) {
+ console.log('Refresh token invalid, re-authenticating...');
+ // Trigger a new login flow
+ await authorize({ scope: 'openid profile offline_access' });
+ } else {
+ console.error('Transient error after retries:', error);
+ throw error;
+ }
+ }
+ };
+
+ // ...
+}
+```
+
+**Best Practices:**
+
+1. **Use moderate retry counts**: Recommended maximum of 2 retries to balance reliability with performance
+2. **Configure adequate overlap period**: Ensure your Auth0 tenant has at least 180 seconds token overlap configured
+3. **Test on real devices**: Simulate network instability during testing to validate retry behavior
+
## Biometric Authentication
> **Platform Support:** Native only (iOS/Android)
diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
index 9cd830fa..dbffe189 100644
--- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt
+++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt
@@ -168,8 +168,13 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
domain: String,
localAuthenticationOptions: ReadableMap?,
useDPoP: Boolean?,
+ maxRetries: Double,
promise: Promise
) {
+ // Note: maxRetries parameter is ignored on Android as the Auth0.Android SDK
+ // does not currently support retry configuration for credential renewal.
+ // This parameter is accepted for API compatibility with iOS.
+
this.useDPoP = useDPoP ?: true
auth0 = Auth0.getInstance(clientId, domain)
diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
index ec052015..8c028c7e 100644
--- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
+++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
@@ -26,6 +26,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
domain: String,
localAuthenticationOptions: ReadableMap?,
useDPoP: Boolean?,
+ maxRetries: Double,
promise: Promise
)
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index cdb51df4..c8e2e384 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,6 +1,6 @@
PODS:
- - A0Auth0 (5.3.0):
- - Auth0 (= 2.16.1)
+ - A0Auth0 (5.3.1):
+ - Auth0 (= 2.16.2)
- boost
- DoubleConversion
- fast_float
@@ -28,7 +28,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - Auth0 (2.16.1):
+ - Auth0 (2.16.2):
- JWTDecode (= 3.3.0)
- SimpleKeychain (= 1.3.0)
- boost (1.84.0)
@@ -2918,8 +2918,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- A0Auth0: 30d506d65dee5cbb3556454ad26ac5bd92ed6c80
- Auth0: b218669c4bd8c8fa873e2eae15444c43f5f6c318
+ A0Auth0: 57429fa910de0b4733e9c200ff05322cd37e7c21
+ Auth0: f08ad1b74d49ffe806175845ef326d9a7674b34c
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm
index ffe6a4c1..d3729630 100644
--- a/ios/A0Auth0.mm
+++ b/ios/A0Auth0.mm
@@ -98,9 +98,10 @@ - (dispatch_queue_t)methodQueue
domain:(NSString *)domain
localAuthenticationOptions:(NSDictionary * _Nullable)localAuthenticationOptions
useDPoP:(nonnull NSNumber *)useDPoP
+ maxRetries:(double)maxRetries
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
- [self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP resolve:resolve reject:reject];
+ [self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP maxRetries:(NSInteger)maxRetries resolve:resolve reject:reject];
}
@@ -189,9 +190,9 @@ - (BOOL)checkHasValidNativeBridgeInstance:(NSString*) clientId domain:(NSString
return valid;
}
-- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain withLocalAuthenticationOptions:(NSDictionary*) options useDPoP:(NSNumber *)useDPoP resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
+- (void)tryAndInitializeNativeBridge:(NSString *)clientId domain:(NSString *)domain withLocalAuthenticationOptions:(NSDictionary*) options useDPoP:(NSNumber *)useDPoP maxRetries:(NSInteger)maxRetries resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
BOOL useDPoPBool = [useDPoP boolValue];
- NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options useDPoP:useDPoPBool resolve:resolve reject:reject];
+ NativeBridge *bridge = [[NativeBridge alloc] initWithClientId:clientId domain:domain localAuthenticationOptions:options useDPoP:useDPoPBool maxRetries:maxRetries resolve:resolve reject:reject];
self.nativeBridge = bridge;
}
#ifdef RCT_NEW_ARCH_ENABLED
diff --git a/ios/NativeBridge.swift b/ios/NativeBridge.swift
index 7e6c8f0f..27f8fb90 100644
--- a/ios/NativeBridge.swift
+++ b/ios/NativeBridge.swift
@@ -43,17 +43,19 @@ public class NativeBridge: NSObject {
var clientId: String
var domain: String
var useDPoP: Bool
+ var maxRetries: Int
- @objc public init(clientId: String, domain: String, localAuthenticationOptions: [String: Any]?, useDPoP: Bool, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
+ @objc public init(clientId: String, domain: String, localAuthenticationOptions: [String: Any]?, useDPoP: Bool, maxRetries: Int, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
var auth0 = Auth0
.authentication(clientId: clientId, domain: domain)
self.clientId = clientId
self.domain = domain
self.useDPoP = useDPoP
+ self.maxRetries = maxRetries
if self.useDPoP {
auth0 = auth0.useDPoP()
}
- self.credentialsManager = CredentialsManager(authentication: auth0)
+ self.credentialsManager = CredentialsManager(authentication: auth0, maxRetries: maxRetries)
super.init()
if let localAuthenticationOptions = localAuthenticationOptions {
if let title = localAuthenticationOptions["title"] as? String {
diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts
index 91803ecc..4b4e9abe 100644
--- a/src/platforms/native/adapters/NativeAuth0Client.ts
+++ b/src/platforms/native/adapters/NativeAuth0Client.ts
@@ -79,6 +79,7 @@ export class NativeAuth0Client implements IAuth0Client {
domain,
localAuthenticationOptions,
useDPoP = true,
+ maxRetries,
} = options;
const hasValidInstance = await bridge.hasValidInstance(clientId, domain);
if (!hasValidInstance) {
@@ -86,7 +87,8 @@ export class NativeAuth0Client implements IAuth0Client {
clientId,
domain,
localAuthenticationOptions,
- useDPoP
+ useDPoP,
+ maxRetries
);
}
}
diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
index 6b831c5c..b4f4e279 100644
--- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
+++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
@@ -110,7 +110,8 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
undefined, // No local auth options provided in this test
- true // useDPoP defaults to true
+ true, // useDPoP defaults to true
+ undefined // maxRetries not provided
);
// Use client to avoid unused variable warning
@@ -131,7 +132,8 @@ describe('NativeAuth0Client', () => {
options.clientId,
options.domain,
localAuthOptions,
- true // useDPoP defaults to true
+ true, // useDPoP defaults to true
+ undefined // maxRetries not provided
);
// Use client to avoid unused variable warning
diff --git a/src/platforms/native/bridge/INativeBridge.ts b/src/platforms/native/bridge/INativeBridge.ts
index 7a62ccdf..9462f031 100644
--- a/src/platforms/native/bridge/INativeBridge.ts
+++ b/src/platforms/native/bridge/INativeBridge.ts
@@ -33,12 +33,14 @@ export interface INativeBridge {
* @param domain The Auth0 application domain.
* @param localAuthenticationOptions Options for local authentication.
* @param useDPoP Whether to enable DPoP (Demonstrating Proof-of-Possession) for token requests.
+ * @param maxRetries The maximum number of retry attempts for transient errors during credential renewal. **iOS only** - ignored on Android. Defaults to 0.
*/
initialize(
clientId: string,
domain: string,
localAuthenticationOptions?: LocalAuthenticationOptions,
- useDPoP?: boolean
+ useDPoP?: boolean,
+ maxRetries?: number
): Promise;
/**
@@ -89,6 +91,7 @@ export interface INativeBridge {
*
* @param scope The scopes to request during a token refresh.
* @param minTtl The minimum time-to-live (in seconds) for the access token.
+ * @param parameters Additional parameters to send during the token refresh request.
* @param forceRefresh If true, forces a token refresh.
* @returns A promise that resolves with the credentials.
*/
diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts
index a1e36643..c888a1b1 100644
--- a/src/platforms/native/bridge/NativeBridgeManager.ts
+++ b/src/platforms/native/bridge/NativeBridgeManager.ts
@@ -54,7 +54,8 @@ export class NativeBridgeManager implements INativeBridge {
clientId: string,
domain: string,
localAuthenticationOptions?: LocalAuthenticationOptions,
- useDPoP: boolean = true
+ useDPoP: boolean = true,
+ maxRetries: number = 0
): Promise {
// This is a new method we'd add to the native side to ensure the
// underlying Auth0.swift/Auth0.android SDKs are configured.
@@ -65,7 +66,8 @@ export class NativeBridgeManager implements INativeBridge {
clientId,
domain,
localAuthenticationOptions,
- useDPoP
+ useDPoP,
+ maxRetries
);
}
diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts
index 36c18272..1a9c8d6b 100644
--- a/src/specs/NativeA0Auth0.ts
+++ b/src/specs/NativeA0Auth0.ts
@@ -25,7 +25,8 @@ export interface Spec extends TurboModule {
localAuthenticationOptions:
| { [key: string]: string | Int32 | boolean }
| undefined,
- useDPoP: boolean | undefined
+ useDPoP: boolean | undefined,
+ maxRetries: Int32
): Promise;
/**
diff --git a/src/types/common.ts b/src/types/common.ts
index cd11d720..659c87e5 100644
--- a/src/types/common.ts
+++ b/src/types/common.ts
@@ -146,6 +146,15 @@ export interface Auth0Options {
* @see https://datatracker.ietf.org/doc/html/rfc9449
*/
useDPoP?: boolean;
+ /**
+ * The maximum number of retry attempts for transient errors during credential renewal.
+ * Helps handle network failures and transient errors when using refresh token rotation.
+ * **iOS only** - This parameter is accepted on Android for API compatibility but has no effect
+ * as the Auth0.Android SDK does not currently support retry configuration.
+ * @default 0 (no retries)
+ * @platform ios
+ */
+ maxRetries?: number;
// Telemetry and localAuthenticationOptions are platform-specific extensions
}