Skip to content
Merged
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 A0Auth0.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
195 changes: 195 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

<a name="credential-renewal-retry-overview"></a>

### 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.

<a name="credential-renewal-retry-prerequisites"></a>

### 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.

<a name="using-retry-with-hooks"></a>

### 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 (
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
maxRetries={2} // Configure retry mechanism at initialization (iOS only)
>
<MyComponent />
</Auth0Provider>
);
}

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 (
<View>
<Button title="Get Credentials" onPress={fetchCredentialsWithRetry} />
</View>
);
}
```

<a name="using-retry-with-auth0-class"></a>

### 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);
}
```

<a name="credential-renewal-retry-platform-support"></a>

### 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

<a name="credential-renewal-retry-error-handling"></a>

### 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)
Expand Down
5 changes: 5 additions & 0 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ
domain: String,
localAuthenticationOptions: ReadableMap?,
useDPoP: Boolean?,
maxRetries: Double,
promise: Promise
)

Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions ios/A0Auth0.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}


Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions ios/NativeBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/platforms/native/adapters/NativeAuth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,16 @@ export class NativeAuth0Client implements IAuth0Client {
domain,
localAuthenticationOptions,
useDPoP = true,
maxRetries,
} = options;
const hasValidInstance = await bridge.hasValidInstance(clientId, domain);
if (!hasValidInstance) {
await bridge.initialize(
clientId,
domain,
localAuthenticationOptions,
useDPoP
useDPoP,
maxRetries
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/platforms/native/bridge/INativeBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
6 changes: 4 additions & 2 deletions src/platforms/native/bridge/NativeBridgeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
// This is a new method we'd add to the native side to ensure the
// underlying Auth0.swift/Auth0.android SDKs are configured.
Expand All @@ -65,7 +66,8 @@ export class NativeBridgeManager implements INativeBridge {
clientId,
domain,
localAuthenticationOptions,
useDPoP
useDPoP,
maxRetries
);
}

Expand Down
Loading
Loading