Skip to content

Commit 01e7644

Browse files
committed
feat: allow configuring Auth0.Android's native networking client
Adds `androidNetworkingOptions` to `Auth0Options`, letting apps tune the OkHttp-based `DefaultClient` (connect/read/write/call timeouts, default headers, and debug logging) that Auth0.Android uses for every native request. Android only; accepted and ignored on iOS for API compatibility. Ref: SDK-10614
1 parent 3f04576 commit 01e7644

14 files changed

Lines changed: 314 additions & 10 deletions

File tree

EXAMPLES.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
- [Using Retry with Auth0 Class](#using-retry-with-auth0-class)
2222
- [Platform Support](#platform-support)
2323
- [Error Handling](#error-handling)
24+
- [Android Networking Configuration](#android-networking-configuration)
25+
- [Using Networking Options with Hooks](#using-networking-options-with-hooks)
26+
- [Using Networking Options with Auth0 Class](#using-networking-options-with-auth0-class)
2427
- [IPSIE Session Expiry](#ipsie-session-expiry)
2528
- [Biometric Authentication](#biometric-authentication)
2629
- [Biometric Policy Types](#biometric-policy-types)
@@ -637,6 +640,66 @@ function MyComponent() {
637640
2. **Configure adequate overlap period**: Ensure your Auth0 tenant has at least 180 seconds token overlap configured
638641
3. **Test on real devices**: Simulate network instability during testing to validate retry behavior
639642
643+
## Android Networking Configuration
644+
645+
> **Platform Support:** Android only. Accepted on iOS for API compatibility but has no effect.
646+
647+
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.
648+
649+
```ts
650+
androidNetworkingOptions?: {
651+
connectTimeout?: number; // seconds, default 10
652+
readTimeout?: number; // seconds, default 10
653+
writeTimeout?: number; // seconds, default 10
654+
callTimeout?: number; // seconds, default 0 (no limit)
655+
defaultHeaders?: Record<string, string>; // sent on every request, default {}
656+
enableLogging?: boolean; // default false
657+
};
658+
```
659+
660+
Any option you omit falls back to Auth0.Android's own default.
661+
662+
> [!WARNING]
663+
> `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.
664+
665+
### Using Networking Options with Hooks
666+
667+
```jsx
668+
import React from 'react';
669+
import { Auth0Provider } from 'react-native-auth0';
670+
671+
function App() {
672+
return (
673+
<Auth0Provider
674+
domain="YOUR_AUTH0_DOMAIN"
675+
clientId="YOUR_AUTH0_CLIENT_ID"
676+
androidNetworkingOptions={{
677+
connectTimeout: 30,
678+
readTimeout: 30,
679+
defaultHeaders: { 'X-App-Version': '1.2.3' },
680+
}}
681+
>
682+
<MyComponent />
683+
</Auth0Provider>
684+
);
685+
}
686+
```
687+
688+
### Using Networking Options with Auth0 Class
689+
690+
```js
691+
import Auth0 from 'react-native-auth0';
692+
693+
const auth0 = new Auth0({
694+
domain: 'YOUR_AUTH0_DOMAIN',
695+
clientId: 'YOUR_AUTH0_CLIENT_ID',
696+
androidNetworkingOptions: {
697+
connectTimeout: 30,
698+
readTimeout: 30,
699+
},
700+
});
701+
```
702+
640703
## IPSIE Session Expiry
641704
642705
> **Platform Support:** iOS, Android, and Web.

android/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ android {
5151
abortOnError false
5252
}
5353

54+
testOptions {
55+
unitTests.returnDefaultValues = true
56+
}
57+
5458
compileOptions {
5559
sourceCompatibility JavaVersion.VERSION_17
5660
targetCompatibility JavaVersion.VERSION_17
@@ -79,6 +83,9 @@ dependencies {
7983
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
8084
implementation "androidx.browser:browser:1.10.0"
8185
implementation 'com.auth0.android:auth0:4.0.1'
86+
87+
testImplementation 'junit:junit:4.13.2'
88+
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
8289
}
8390

8491
react {

android/src/main/java/com/auth0/react/A0Auth0Module.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.auth0.android.dpop.DPoPException
2020
import com.auth0.android.provider.BrowserPicker
2121
import com.auth0.android.provider.CustomTabsOptions
2222
import com.auth0.android.provider.WebAuthProvider
23+
import com.auth0.android.request.DefaultClient
2324
import com.auth0.android.request.PublicKeyCredentials
2425
import com.auth0.android.request.UserData
2526
import com.auth0.android.result.APICredentials
@@ -64,6 +65,24 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
6465
private const val DPOP_INVALID_TOKEN_TYPE_CODE = "DPOP_INVALID_TOKEN_TYPE"
6566
private const val DPOP_MISSING_PARAMETER_CODE = "DPOP_MISSING_PARAMETER"
6667
private const val DPOP_CLEAR_KEY_FAILED_CODE = "DPOP_CLEAR_KEY_FAILED"
68+
69+
// Builds the DefaultClient Auth0.Android uses for every request it makes (web auth
70+
// token exchange, credential renewal, MFA, passkeys, etc.). Unset keys fall through to
71+
// Auth0.Android's own Builder defaults. `enableLogging` is debug-only: Auth0.Android logs
72+
// full request/response bodies (including tokens) at that level, so we never call
73+
// `logger(...)` ourselves and never expose the raw HttpLoggingInterceptor.Logger to JS.
74+
internal fun buildNetworkingClient(options: ReadableMap): DefaultClient {
75+
val builder = DefaultClient.Builder()
76+
if (options.hasKey("connectTimeout")) builder.connectTimeout(options.getInt("connectTimeout"))
77+
if (options.hasKey("readTimeout")) builder.readTimeout(options.getInt("readTimeout"))
78+
if (options.hasKey("writeTimeout")) builder.writeTimeout(options.getInt("writeTimeout"))
79+
if (options.hasKey("callTimeout")) builder.callTimeout(options.getInt("callTimeout"))
80+
options.getMap("defaultHeaders")?.let { headers ->
81+
builder.defaultHeaders(headers.toHashMap().mapValues { it.value?.toString() ?: "" })
82+
}
83+
if (options.hasKey("enableLogging")) builder.enableLogging(options.getBoolean("enableLogging"))
84+
return builder.build()
85+
}
6786
}
6887

6988
private val errorCodeMap = mapOf(
@@ -282,6 +301,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
282301
useDPoP: Boolean?,
283302
maxRetries: Double,
284303
credentialsManagerStorageKey: String?,
304+
androidNetworkingOptions: ReadableMap?,
285305
promise: Promise
286306
) {
287307
// Note: maxRetries parameter is ignored on Android as the Auth0.Android SDK
@@ -290,6 +310,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0
290310

291311
this.useDPoP = useDPoP ?: false
292312
auth0 = Auth0.getInstance(clientId, domain)
313+
androidNetworkingOptions?.let { auth0!!.networkingClient = buildNetworkingClient(it) }
293314
mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext)
294315
myAccount = MyAccount(auth0!!, this.useDPoP, reactContext)
295316
passwordless = Passwordless(auth0!!, this.useDPoP, reactContext)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.auth0.react
2+
3+
import com.auth0.android.request.HttpMethod
4+
import com.auth0.android.request.RequestOptions
5+
import com.facebook.react.bridge.JavaOnlyMap
6+
import okhttp3.mockwebserver.MockResponse
7+
import okhttp3.mockwebserver.MockWebServer
8+
import org.junit.After
9+
import org.junit.Assert.assertTrue
10+
import org.junit.Assert.fail
11+
import org.junit.Before
12+
import org.junit.Test
13+
import java.io.IOException
14+
import java.util.concurrent.TimeUnit
15+
import kotlin.system.measureTimeMillis
16+
17+
// Proves that A0Auth0Module.buildNetworkingClient() genuinely threads androidNetworkingOptions
18+
// into the DefaultClient it builds, rather than just compiling. Exercises the client against a
19+
// real (local) server so the OkHttp timeout machinery actually runs.
20+
class A0Auth0ModuleNetworkingOptionsTest {
21+
22+
private lateinit var server: MockWebServer
23+
24+
@Before
25+
fun setUp() {
26+
server = MockWebServer()
27+
server.start()
28+
}
29+
30+
@After
31+
fun tearDown() {
32+
server.shutdown()
33+
}
34+
35+
@Test
36+
fun `readTimeout from androidNetworkingOptions is applied to the built DefaultClient`() {
37+
val configuredTimeoutSeconds = 1
38+
// Stall the response well past the configured timeout.
39+
server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}"))
40+
41+
val client = A0Auth0Module.buildNetworkingClient(
42+
JavaOnlyMap.of("readTimeout", configuredTimeoutSeconds)
43+
)
44+
45+
var threw = false
46+
val elapsedMillis = measureTimeMillis {
47+
try {
48+
client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET))
49+
fail("Expected the configured read timeout to fire")
50+
} catch (e: IOException) {
51+
threw = true
52+
}
53+
}
54+
55+
assertTrue("Expected an IOException from the read timeout", threw)
56+
// The server stalls for 3s; a working 1s readTimeout must fire well before that.
57+
assertTrue(
58+
"Expected the call to fail near the configured ${configuredTimeoutSeconds}s timeout, took ${elapsedMillis}ms",
59+
elapsedMillis < TimeUnit.SECONDS.toMillis(2)
60+
)
61+
}
62+
63+
@Test
64+
fun `defaultHeaders from androidNetworkingOptions are sent on every request`() {
65+
server.enqueue(MockResponse().setBody("{}"))
66+
67+
val client = A0Auth0Module.buildNetworkingClient(
68+
JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value"))
69+
)
70+
71+
client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET))
72+
73+
val recordedRequest = server.takeRequest()
74+
assertTrue(recordedRequest.getHeader("X-Custom-Header") == "custom-value")
75+
}
76+
}

ios/A0Auth0.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ - (dispatch_queue_t)methodQueue
100100
useDPoP:(nonnull NSNumber *)useDPoP
101101
maxRetries:(double)maxRetries
102102
credentialsManagerStorageKey:(NSString * _Nullable)credentialsManagerStorageKey
103+
androidNetworkingOptions:(NSDictionary * _Nullable)androidNetworkingOptions
103104
resolve:(RCTPromiseResolveBlock)resolve
104105
reject:(RCTPromiseRejectBlock)reject) {
106+
// androidNetworkingOptions is Android-only; intentionally not forwarded to NativeBridge.
105107
[self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP maxRetries:(NSInteger)maxRetries credentialsManagerStorageKey:credentialsManagerStorageKey resolve:resolve reject:reject];
106108
}
107109

src/core/utils/__tests__/configSignature.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,30 @@ describe('getConfigSignature', () => {
8484
getConfigSignature({ ...base, maxRetries: 3 })
8585
);
8686
});
87+
88+
it('differs when androidNetworkingOptions changes', () => {
89+
expect(
90+
getConfigSignature({
91+
...base,
92+
androidNetworkingOptions: { connectTimeout: 10 },
93+
})
94+
).not.toBe(
95+
getConfigSignature({
96+
...base,
97+
androidNetworkingOptions: { connectTimeout: 30 },
98+
})
99+
);
100+
});
101+
102+
it('is insensitive to androidNetworkingOptions key order', () => {
103+
const a = getConfigSignature({
104+
...base,
105+
androidNetworkingOptions: { connectTimeout: 10, readTimeout: 20 },
106+
});
107+
const b = getConfigSignature({
108+
...base,
109+
androidNetworkingOptions: { readTimeout: 20, connectTimeout: 10 },
110+
});
111+
expect(a).toBe(b);
112+
});
87113
});

src/core/utils/configSignature.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const SIGNIFICANT_KEYS = [
99
'useDPoP',
1010
'maxRetries',
1111
'credentialsManagerStorageKey',
12+
'androidNetworkingOptions',
1213
] as const satisfies ReadonlyArray<keyof Auth0Options>;
1314

1415
// 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.

src/platforms/native/adapters/NativeAuth0Client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export class NativeAuth0Client implements IAuth0Client {
107107
useDPoP = false,
108108
maxRetries,
109109
credentialsManagerStorageKey,
110+
androidNetworkingOptions,
110111
} = options;
111112
// Re-init when domain/clientId differ (hasValidInstance) or any other
112113
// identity option drifted from what was last applied to the native side.
@@ -123,7 +124,8 @@ export class NativeAuth0Client implements IAuth0Client {
123124
localAuthenticationOptions,
124125
useDPoP,
125126
maxRetries,
126-
credentialsManagerStorageKey
127+
credentialsManagerStorageKey,
128+
androidNetworkingOptions
127129
);
128130
}
129131
// Record even on the skip path so siblings differing only in a

src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ describe('NativeAuth0Client', () => {
120120
undefined, // No local auth options provided in this test
121121
false, // useDPoP defaults to false
122122
undefined, // maxRetries not provided
123-
undefined // credentialsManagerStorageKey not provided
123+
undefined, // credentialsManagerStorageKey not provided
124+
undefined // androidNetworkingOptions not provided
124125
);
125126

126127
// Use client to avoid unused variable warning
@@ -142,7 +143,8 @@ describe('NativeAuth0Client', () => {
142143
undefined,
143144
false,
144145
undefined,
145-
'tenant-b'
146+
'tenant-b',
147+
undefined
146148
);
147149
expect(client).toBeDefined();
148150
});
@@ -163,13 +165,37 @@ describe('NativeAuth0Client', () => {
163165
localAuthOptions,
164166
false, // useDPoP defaults to false
165167
undefined, // maxRetries not provided
166-
undefined // credentialsManagerStorageKey not provided
168+
undefined, // credentialsManagerStorageKey not provided
169+
undefined // androidNetworkingOptions not provided
167170
);
168171

169172
// Use client to avoid unused variable warning
170173
expect(client).toBeDefined();
171174
});
172175

176+
it('should pass androidNetworkingOptions to initialize when provided', async () => {
177+
mockBridgeInstance.hasValidInstance.mockResolvedValue(false);
178+
const androidNetworkingOptions = { connectTimeout: 30, readTimeout: 30 };
179+
180+
const client = new NativeAuth0Client({
181+
...options,
182+
androidNetworkingOptions,
183+
});
184+
await new Promise(process.nextTick);
185+
186+
expect(mockBridgeInstance.initialize).toHaveBeenCalledWith(
187+
options.clientId,
188+
options.domain,
189+
undefined,
190+
false,
191+
undefined,
192+
undefined,
193+
androidNetworkingOptions
194+
);
195+
196+
expect(client).toBeDefined();
197+
});
198+
173199
it('should ensure initialization is complete before calling a bridge method', async () => {
174200
let resolveInitialization: () => void;
175201
const initializationPromise = new Promise<void>((resolve) => {
@@ -686,6 +712,7 @@ describe('NativeAuth0Client', () => {
686712
undefined,
687713
false,
688714
undefined,
715+
undefined,
689716
undefined
690717
);
691718
expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1);
@@ -729,6 +756,7 @@ describe('NativeAuth0Client', () => {
729756
undefined,
730757
false, // useDPoP flipped to false
731758
undefined,
759+
undefined,
732760
undefined
733761
);
734762
});

src/platforms/native/bridge/INativeBridge.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
MfaEnrollmentChallenge,
1010
MfaChallengeResult,
1111
PasskeyChallengeResponse,
12+
AndroidNetworkingOptions,
1213
} from '../../../types';
1314
import type {
1415
LocalAuthenticationOptions,
@@ -39,14 +40,16 @@ export interface INativeBridge {
3940
* @param useDPoP Whether to enable DPoP (Demonstrating Proof-of-Possession) for token requests.
4041
* @param maxRetries The maximum number of retry attempts for transient errors during credential renewal. **iOS only** - ignored on Android. Defaults to 0.
4142
* @param credentialsManagerStorageKey Namespaces the credentials store. **Android only** SharedPreferences file name. **iOS only** Keychain service name. Defaults to the shared store when omitted.
43+
* @param androidNetworkingOptions Configures the native networking client. **Android only** - ignored on iOS.
4244
*/
4345
initialize(
4446
clientId: string,
4547
domain: string,
4648
localAuthenticationOptions?: LocalAuthenticationOptions,
4749
useDPoP?: boolean,
4850
maxRetries?: number,
49-
credentialsManagerStorageKey?: string
51+
credentialsManagerStorageKey?: string,
52+
androidNetworkingOptions?: AndroidNetworkingOptions
5053
): Promise<void>;
5154

5255
/**

0 commit comments

Comments
 (0)