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
63 changes: 63 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
- [Using Retry with Auth0 Class](#using-retry-with-auth0-class)
- [Platform Support](#platform-support)
- [Error Handling](#error-handling)
- [Android Networking Configuration](#android-networking-configuration)
- [Using Networking Options with Hooks](#using-networking-options-with-hooks)
- [Using Networking Options with Auth0 Class](#using-networking-options-with-auth0-class)
- [IPSIE Session Expiry](#ipsie-session-expiry)
- [Biometric Authentication](#biometric-authentication)
- [Biometric Policy Types](#biometric-policy-types)
Expand Down Expand Up @@ -637,6 +640,66 @@ function MyComponent() {
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

## Android Networking Configuration

> **Platform Support:** Android only. Accepted on iOS for API compatibility but has no effect.

The `androidNetworkingOptions` configuration option lets you tune the native networking client (`DefaultClient` from Auth0.Android's OkHttp-based stack) used for every request the native SDK makes on your behalf — web auth token exchange, credential renewal, MFA, passkeys, and My Account API calls.

```ts
androidNetworkingOptions?: {
connectTimeout?: number; // seconds, default 10
readTimeout?: number; // seconds, default 10
writeTimeout?: number; // seconds, default 10
callTimeout?: number; // seconds, default 0 (no limit)
defaultHeaders?: Record<string, string>; // sent on every request, default {}
enableLogging?: boolean; // default false
};
```

Any option you omit falls back to Auth0.Android's own default.

> [!WARNING]
> `enableLogging` is **debug-only**. When enabled, Auth0.Android logs full HTTP request and response bodies to Logcat — including access, refresh, and ID tokens returned from token-endpoint calls, in plaintext. Never enable it in a production build.

### Using Networking Options with Hooks

```jsx
import React from 'react';
import { Auth0Provider } from 'react-native-auth0';

function App() {
return (
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
androidNetworkingOptions={{
connectTimeout: 30,
readTimeout: 30,
defaultHeaders: { 'X-App-Version': '1.2.3' },
}}
>
<MyComponent />
</Auth0Provider>
);
}
```

### Using Networking Options with Auth0 Class

```js
import Auth0 from 'react-native-auth0';

const auth0 = new Auth0({
domain: 'YOUR_AUTH0_DOMAIN',
clientId: 'YOUR_AUTH0_CLIENT_ID',
androidNetworkingOptions: {
connectTimeout: 30,
readTimeout: 30,
},
});
```

## IPSIE Session Expiry

> **Platform Support:** iOS, Android, and Web.
Expand Down
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.browser:browser:1.10.0"
implementation 'com.auth0.android:auth0:4.0.1'

testImplementation 'junit:junit:4.13.2'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
}

react {
Expand Down
21 changes: 21 additions & 0 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.auth0.android.dpop.DPoPException
import com.auth0.android.provider.BrowserPicker
import com.auth0.android.provider.CustomTabsOptions
import com.auth0.android.provider.WebAuthProvider
import com.auth0.android.request.DefaultClient
import com.auth0.android.request.PublicKeyCredentials
import com.auth0.android.request.UserData
import com.auth0.android.result.APICredentials
Expand Down Expand Up @@ -64,6 +65,24 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
private const val DPOP_INVALID_TOKEN_TYPE_CODE = "DPOP_INVALID_TOKEN_TYPE"
private const val DPOP_MISSING_PARAMETER_CODE = "DPOP_MISSING_PARAMETER"
private const val DPOP_CLEAR_KEY_FAILED_CODE = "DPOP_CLEAR_KEY_FAILED"

// Builds the DefaultClient Auth0.Android uses for every request it makes (web auth
// token exchange, credential renewal, MFA, passkeys, etc.). Unset keys fall through to
// Auth0.Android's own Builder defaults. `enableLogging` is debug-only: Auth0.Android logs
// full request/response bodies (including tokens) at that level, so we never call
// `logger(...)` ourselves and never expose the raw HttpLoggingInterceptor.Logger to JS.
internal fun buildNetworkingClient(options: ReadableMap): DefaultClient {
val builder = DefaultClient.Builder()
if (options.hasKey("connectTimeout")) builder.connectTimeout(options.getInt("connectTimeout"))
if (options.hasKey("readTimeout")) builder.readTimeout(options.getInt("readTimeout"))
if (options.hasKey("writeTimeout")) builder.writeTimeout(options.getInt("writeTimeout"))
if (options.hasKey("callTimeout")) builder.callTimeout(options.getInt("callTimeout"))
options.getMap("defaultHeaders")?.let { headers ->
builder.defaultHeaders(headers.toHashMap().mapValues { it.value?.toString() ?: "" })
}
if (options.hasKey("enableLogging")) builder.enableLogging(options.getBoolean("enableLogging"))
return builder.build()
}
}

private val errorCodeMap = mapOf(
Expand Down Expand Up @@ -282,6 +301,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
useDPoP: Boolean?,
maxRetries: Double,
credentialsManagerStorageKey: String?,
androidNetworkingOptions: ReadableMap?,
promise: Promise
) {
// Note: maxRetries parameter is ignored on Android as the Auth0.Android SDK
Expand All @@ -290,6 +310,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0

this.useDPoP = useDPoP ?: false
auth0 = Auth0.getInstance(clientId, domain)
androidNetworkingOptions?.let { auth0!!.networkingClient = buildNetworkingClient(it) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target context ---'
sed -n '260,340p' android/src/main/java/com/auth0/react/A0Auth0Module.kt
printf '%s\n' '--- Auth0 dependency declarations ---'
rg -n -i 'auth0.android|auth0-android|com.auth0' android gradle* build.gradle* settings.gradle* package.json yarn.lock 2>/dev/null | head -200
printf '%s\n' '--- initialization and networking usages ---'
rg -n 'initializeAuth0WithConfiguration|buildNetworkingClient|networkingClient|androidNetworkingOptions|Auth0\.getInstance' android/src android 2>/dev/null | head -250

Repository: auth0/react-native-auth0

Length of output: 22137


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target context ---'
sed -n '260,340p' android/src/main/java/com/auth0/react/A0Auth0Module.kt
printf '%s\n' '--- Auth0 dependency declarations ---'
rg -n -i 'auth0.android|auth0-android|com.auth0' android gradle\* build.gradle\* settings.gradle\* package.json yarn.lock 2>/dev/null | head -200
printf '%s\n' '--- initialization and networking usages ---'
rg -n 'initializeAuth0WithConfiguration|buildNetworkingClient|networkingClient|androidNetworkingOptions|Auth0\.getInstance' android/src android 2>/dev/null | head -250

Repository: auth0/react-native-auth0

Length of output: 22137


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/auth0/Auth0.Android/4.0.1/auth0/src/main/java/com/auth0/android/Auth0.kt'
python3 - "$url" <<'PY'
import sys
from urllib.request import urlopen
url = sys.argv[1]
text = urlopen(url, timeout=10).read().decode()
for i, line in enumerate(text.splitlines(), 1):
    if 'getInstance' in line or 'networkingClient' in line or 'companion object' in line:
        start = max(1, i - 8)
        end = min(len(text.splitlines()), i + 14)
        print(f'--- lines {start}-{end} ---')
        print('\n'.join(f'{n}: {text.splitlines()[n-1]}' for n in range(start, end + 1)))
PY

Repository: auth0/react-native-auth0

Length of output: 2448


🌐 Web query:

Auth0.Android 4.0.1 Auth0.getInstance networkingClient singleton source

💡 Result:

In the Auth0.Android SDK (including version 4.0.1), the Auth0.getInstance method acts as a factory that returns a singleton instance of the Auth0 class for a given configuration [1]. Regarding your query about the networkingClient: 1. Singleton Behavior: The Auth0 class is managed as a singleton per configuration [1]. When you call Auth0.getInstance, it checks if an instance with the specified client ID and domain already exists; if so, it returns that existing instance [1]. 2. NetworkingClient Property: The Auth0 instance contains a networkingClient property (type NetworkingClient) which is used for all HTTP requests made by the library's various API clients (e.g., AuthenticationAPIClient, UsersAPIClient) [2][3][4]. 3. Customization: By default, this property is initialized with a DefaultClient [3]. You can replace it with a custom implementation by setting the networkingClient property directly on your Auth0 singleton instance [2][3]. Example of configuring the networking client: val account = Auth0.getInstance("{YOUR_CLIENT_ID}", "{YOUR_DOMAIN}") account.networkingClient = CustomNetworkingClient // Replace with your implementation Note that because the Auth0 instance is a singleton, changing the networkingClient on that instance will affect all subsequent API operations that rely on that specific Auth0 account instance [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
url='https://raw.githubusercontent.com/auth0/Auth0.Android/4.0.1/auth0/src/main/java/com/auth0/android/Auth0.kt'
curl -fsSLk "$url" | nl -ba | awk '
  /companion object|fun getInstance|networkingClient|private constructor|class Auth0/ {
    start = NR - 8; if (start < 1) start = 1
    end = NR + 14
    print "--- lines " start "-" end " ---"
    for (i = start; i <= end; i++) lines[i] = lines[i]
  }
  { lines[NR] = $0 }
  END {
    # Re-read is not possible in this one-pass filter; print source-focused matches instead.
    for (i = 1; i <= NR; i++)
      if (lines[i] ~ /companion object|fun getInstance|networkingClient|private constructor|class Auth0/)
        print i ":" lines[i]
  }'
printf '%s\n' '--- local client builder and imports ---'
sed -n '1,105p' android/src/main/java/com/auth0/react/A0Auth0Module.kt

Repository: auth0/react-native-auth0

Length of output: 6617


Reset the networking client when options are omitted.

Auth0.getInstance(clientId, domain) reuses the same instance. A later initialization without androidNetworkingOptions preserves the previous custom networkingClient. Assign DefaultClient() when the options are absent, and add a same-configuration re-initialization test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/auth0/react/A0Auth0Module.kt` at line 313, Update
the initialization flow around androidNetworkingOptions and
Auth0.getInstance(clientId, domain) so the Auth0 instance’s networkingClient is
explicitly reset to DefaultClient() when options are absent, while retaining
buildNetworkingClient(it) for provided options. Add a test covering
same-configuration re-initialization after custom networking options, verifying
the client is restored to the default.

Source: MCP tools

mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext)
myAccount = MyAccount(auth0!!, this.useDPoP, reactContext)
passwordless = Passwordless(auth0!!, this.useDPoP, reactContext)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.auth0.react

import com.auth0.android.request.HttpMethod
import com.auth0.android.request.RequestOptions
import com.facebook.react.bridge.JavaOnlyMap
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis

// Proves that A0Auth0Module.buildNetworkingClient() genuinely threads androidNetworkingOptions
// into the DefaultClient it builds, rather than just compiling. Exercises the client against a
// real (local) server so the OkHttp timeout machinery actually runs.
class A0Auth0ModuleNetworkingOptionsTest {

private lateinit var server: MockWebServer

@Before
fun setUp() {
server = MockWebServer()
server.start()
}

@After
fun tearDown() {
server.shutdown()
}

@Test
fun `readTimeout from androidNetworkingOptions is applied to the built DefaultClient`() {
val configuredTimeoutSeconds = 1
// Stall the response well past the configured timeout.
server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}"))

val client = A0Auth0Module.buildNetworkingClient(
JavaOnlyMap.of("readTimeout", configuredTimeoutSeconds)
)

var threw = false
val elapsedMillis = measureTimeMillis {
try {
client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET))
fail("Expected the configured read timeout to fire")
} catch (e: IOException) {
threw = true
}
}

assertTrue("Expected an IOException from the read timeout", threw)
// The server stalls for 3s; a working 1s readTimeout must fire well before that.
assertTrue(
"Expected the call to fail near the configured ${configuredTimeoutSeconds}s timeout, took ${elapsedMillis}ms",
elapsedMillis < TimeUnit.SECONDS.toMillis(2)
)
}

@Test
fun `defaultHeaders from androidNetworkingOptions are sent on every request`() {
server.enqueue(MockResponse().setBody("{}"))

val client = A0Auth0Module.buildNetworkingClient(
JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value"))
)

client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET))

val recordedRequest = server.takeRequest()
assertTrue(recordedRequest.getHeader("X-Custom-Header") == "custom-value")
}
}
2 changes: 2 additions & 0 deletions ios/A0Auth0.mm
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ - (dispatch_queue_t)methodQueue
useDPoP:(nonnull NSNumber *)useDPoP
maxRetries:(double)maxRetries
credentialsManagerStorageKey:(NSString * _Nullable)credentialsManagerStorageKey
androidNetworkingOptions:(NSDictionary * _Nullable)androidNetworkingOptions
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
// androidNetworkingOptions is Android-only; intentionally not forwarded to NativeBridge.
[self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP maxRetries:(NSInteger)maxRetries credentialsManagerStorageKey:credentialsManagerStorageKey resolve:resolve reject:reject];
}

Expand Down
26 changes: 26 additions & 0 deletions src/core/utils/__tests__/configSignature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,30 @@ describe('getConfigSignature', () => {
getConfigSignature({ ...base, maxRetries: 3 })
);
});

it('differs when androidNetworkingOptions changes', () => {
expect(
getConfigSignature({
...base,
androidNetworkingOptions: { connectTimeout: 10 },
})
).not.toBe(
getConfigSignature({
...base,
androidNetworkingOptions: { connectTimeout: 30 },
})
);
});

it('is insensitive to androidNetworkingOptions key order', () => {
const a = getConfigSignature({
...base,
androidNetworkingOptions: { connectTimeout: 10, readTimeout: 20 },
});
const b = getConfigSignature({
...base,
androidNetworkingOptions: { readTimeout: 20, connectTimeout: 10 },
});
expect(a).toBe(b);
});
});
1 change: 1 addition & 0 deletions src/core/utils/configSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const SIGNIFICANT_KEYS = [
'useDPoP',
'maxRetries',
'credentialsManagerStorageKey',
'androidNetworkingOptions',
] as const satisfies ReadonlyArray<keyof Auth0Options>;

// Stable, order-independent identity string for a config: keys the factory cache, the provider memo, and the native re-init decision. Object values are sorted so key order doesn't matter.
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 @@ -107,6 +107,7 @@ export class NativeAuth0Client implements IAuth0Client {
useDPoP = false,
maxRetries,
credentialsManagerStorageKey,
androidNetworkingOptions,
} = options;
// Re-init when domain/clientId differ (hasValidInstance) or any other
// identity option drifted from what was last applied to the native side.
Expand All @@ -123,7 +124,8 @@ export class NativeAuth0Client implements IAuth0Client {
localAuthenticationOptions,
useDPoP,
maxRetries,
credentialsManagerStorageKey
credentialsManagerStorageKey,
androidNetworkingOptions
);
}
// Record even on the skip path so siblings differing only in a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ describe('NativeAuth0Client', () => {
undefined, // No local auth options provided in this test
false, // useDPoP defaults to false
undefined, // maxRetries not provided
undefined // credentialsManagerStorageKey not provided
undefined, // credentialsManagerStorageKey not provided
undefined // androidNetworkingOptions not provided
);

// Use client to avoid unused variable warning
Expand All @@ -142,7 +143,8 @@ describe('NativeAuth0Client', () => {
undefined,
false,
undefined,
'tenant-b'
'tenant-b',
undefined
);
expect(client).toBeDefined();
});
Expand All @@ -163,13 +165,37 @@ describe('NativeAuth0Client', () => {
localAuthOptions,
false, // useDPoP defaults to false
undefined, // maxRetries not provided
undefined // credentialsManagerStorageKey not provided
undefined, // credentialsManagerStorageKey not provided
undefined // androidNetworkingOptions not provided
);

// Use client to avoid unused variable warning
expect(client).toBeDefined();
});

it('should pass androidNetworkingOptions to initialize when provided', async () => {
mockBridgeInstance.hasValidInstance.mockResolvedValue(false);
const androidNetworkingOptions = { connectTimeout: 30, readTimeout: 30 };

const client = new NativeAuth0Client({
...options,
androidNetworkingOptions,
});
await new Promise(process.nextTick);

expect(mockBridgeInstance.initialize).toHaveBeenCalledWith(
options.clientId,
options.domain,
undefined,
false,
undefined,
undefined,
androidNetworkingOptions
);

expect(client).toBeDefined();
});

it('should ensure initialization is complete before calling a bridge method', async () => {
let resolveInitialization: () => void;
const initializationPromise = new Promise<void>((resolve) => {
Expand Down Expand Up @@ -686,6 +712,7 @@ describe('NativeAuth0Client', () => {
undefined,
false,
undefined,
undefined,
undefined
);
expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -729,6 +756,7 @@ describe('NativeAuth0Client', () => {
undefined,
false, // useDPoP flipped to false
undefined,
undefined,
undefined
);
});
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 @@ -9,6 +9,7 @@ import type {
MfaEnrollmentChallenge,
MfaChallengeResult,
PasskeyChallengeResponse,
AndroidNetworkingOptions,
} from '../../../types';
import type {
LocalAuthenticationOptions,
Expand Down Expand Up @@ -39,14 +40,16 @@ export interface INativeBridge {
* @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.
* @param credentialsManagerStorageKey Namespaces the credentials store. **Android only** SharedPreferences file name. **iOS only** Keychain service name. Defaults to the shared store when omitted.
* @param androidNetworkingOptions Configures the native networking client. **Android only** - ignored on iOS.
*/
initialize(
clientId: string,
domain: string,
localAuthenticationOptions?: LocalAuthenticationOptions,
useDPoP?: boolean,
maxRetries?: number,
credentialsManagerStorageKey?: string
credentialsManagerStorageKey?: string,
androidNetworkingOptions?: AndroidNetworkingOptions
): Promise<void>;

/**
Expand Down
Loading
Loading