diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 230a1805..27060e07 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,3 +31,27 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: directory: coverage + + android-test: + name: Run Android tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Setup + uses: ./.github/actions/setup + + - name: Set up JDK 17 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + + - name: Run Android unit tests + working-directory: example/android + run: ./gradlew :react-native-auth0:testDebugUnitTest diff --git a/EXAMPLES.md b/EXAMPLES.md index 36a6b54c..46c5cbc1 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -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) @@ -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 `networkingOptions` 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 +networkingOptions?: { + connectTimeout?: number; // seconds, default 10 + readTimeout?: number; // seconds, default 10 + writeTimeout?: number; // seconds, default 10 + callTimeout?: number; // seconds, default 0 (no limit) + defaultHeaders?: Record; // 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 ( + + + + ); +} +``` + +### 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', + networkingOptions: { + connectTimeout: 30, + readTimeout: 30, + }, +}); +``` + ## IPSIE Session Expiry > **Platform Support:** iOS, Android, and Web. diff --git a/android/build.gradle b/android/build.gradle index 6d4e8d75..2a7f3ab7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 { diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 90acf923..a8380e54 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -2,6 +2,7 @@ package com.auth0.react import android.app.Activity import android.content.Intent +import android.content.pm.ApplicationInfo import android.os.Build import android.os.Handler import android.os.Looper @@ -20,6 +21,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 @@ -64,6 +66,37 @@ 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, never expose the raw HttpLoggingInterceptor.Logger to JS, and + // ignore the option entirely unless the host app is a debug build. + internal fun buildNetworkingClient(options: ReadableMap, isDebuggable: Boolean): 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 (isDebuggable && options.hasKey("enableLogging")) { + builder.enableLogging(options.getBoolean("enableLogging")) + } + return builder.build() + } + + // Auth0.getInstance() returns a shared singleton per clientId/domain: a sibling client + // (or this same client on re-init) must not inherit another initialization's networking + // config, so this always resolves to a fresh DefaultClient() when options are absent + // rather than leaving the previous networkingClient in place. + internal fun resolveNetworkingClient( + networkingOptions: ReadableMap?, + isDebuggable: Boolean + ): DefaultClient = + networkingOptions?.let { buildNetworkingClient(it, isDebuggable) } ?: DefaultClient() } private val errorCodeMap = mapOf( @@ -282,6 +315,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 useDPoP: Boolean?, maxRetries: Double, credentialsManagerStorageKey: String?, + networkingOptions: ReadableMap?, promise: Promise ) { // Note: maxRetries parameter is ignored on Android as the Auth0.Android SDK @@ -289,12 +323,15 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 // This parameter is accepted for API compatibility with iOS. this.useDPoP = useDPoP ?: false - auth0 = Auth0.getInstance(clientId, domain) - mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext) - myAccount = MyAccount(auth0!!, this.useDPoP, reactContext) - passwordless = Passwordless(auth0!!, this.useDPoP, reactContext) - - val authAPI = AuthenticationAPIClient(auth0!!) + val auth0Instance = Auth0.getInstance(clientId, domain) + auth0 = auth0Instance + val isDebuggable = (reactContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + auth0Instance.networkingClient = resolveNetworkingClient(networkingOptions, isDebuggable) + mfaClient = MfaClient(auth0Instance, this.useDPoP, reactContext) + myAccount = MyAccount(auth0Instance, this.useDPoP, reactContext) + passwordless = Passwordless(auth0Instance, this.useDPoP, reactContext) + + val authAPI = AuthenticationAPIClient(auth0Instance) if (this.useDPoP) { authAPI.useDPoP(reactContext) } diff --git a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt new file mode 100644 index 00000000..cbcf0ea4 --- /dev/null +++ b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt @@ -0,0 +1,124 @@ +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 networkingOptions +// 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 networkingOptions 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), + isDebuggable = true + ) + + 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 networkingOptions are sent on every request`() { + server.enqueue(MockResponse().setBody("{}")) + + val client = A0Auth0Module.buildNetworkingClient( + JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value")), + isDebuggable = true + ) + + client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + + val recordedRequest = server.takeRequest() + assertTrue(recordedRequest.getHeader("X-Custom-Header") == "custom-value") + } + + @Test + fun `enableLogging is ignored on a non-debuggable build even when requested`() { + server.enqueue(MockResponse().setBody("{}")) + + // If the debuggable gate is ever removed, Auth0.Android attaches its logging + // interceptor and this request crashes ("Method ... not mocked") because + // android.util.Log isn't stubbed in this unit test environment - that crash is + // exactly the regression this test is meant to catch. + val client = A0Auth0Module.buildNetworkingClient( + JavaOnlyMap.of("enableLogging", true), + isDebuggable = false + ) + + client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + + val recordedRequest = server.takeRequest() + assertTrue(recordedRequest.method == "GET") + } + + @Test + fun `re-initializing with the same configuration but no options restores the default client`() { + // First "initialization": custom options give a 1s readTimeout. + val customized = A0Auth0Module.resolveNetworkingClient( + JavaOnlyMap.of("readTimeout", 1), + isDebuggable = true + ) + + // Re-initialization with the same clientId/domain but networkingOptions omitted + // must not carry the previous readTimeout forward - it should behave like a fresh + // DefaultClient() (10s default readTimeout). + val reset = A0Auth0Module.resolveNetworkingClient(null, isDebuggable = true) + + server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) + var threw = false + try { + customized.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + } catch (e: IOException) { + threw = true + } + assertTrue("Expected the 1s readTimeout to fire on the customized client", threw) + + server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) + // Should comfortably survive the 3s delay under the restored 10s default readTimeout. + reset.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + } +} diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 99024107..fae1f93b 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -100,8 +100,10 @@ - (dispatch_queue_t)methodQueue useDPoP:(nonnull NSNumber *)useDPoP maxRetries:(double)maxRetries credentialsManagerStorageKey:(NSString * _Nullable)credentialsManagerStorageKey + networkingOptions:(NSDictionary * _Nullable)networkingOptions resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + // networkingOptions 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]; } diff --git a/src/core/utils/__tests__/configSignature.spec.ts b/src/core/utils/__tests__/configSignature.spec.ts index 5369f838..a2f9ed36 100644 --- a/src/core/utils/__tests__/configSignature.spec.ts +++ b/src/core/utils/__tests__/configSignature.spec.ts @@ -84,4 +84,30 @@ describe('getConfigSignature', () => { getConfigSignature({ ...base, maxRetries: 3 }) ); }); + + it('differs when networkingOptions changes', () => { + expect( + getConfigSignature({ + ...base, + networkingOptions: { connectTimeout: 10 }, + }) + ).not.toBe( + getConfigSignature({ + ...base, + networkingOptions: { connectTimeout: 30 }, + }) + ); + }); + + it('is insensitive to networkingOptions key order', () => { + const a = getConfigSignature({ + ...base, + networkingOptions: { connectTimeout: 10, readTimeout: 20 }, + }); + const b = getConfigSignature({ + ...base, + networkingOptions: { readTimeout: 20, connectTimeout: 10 }, + }); + expect(a).toBe(b); + }); }); diff --git a/src/core/utils/configSignature.ts b/src/core/utils/configSignature.ts index acd6f426..69f37b9c 100644 --- a/src/core/utils/configSignature.ts +++ b/src/core/utils/configSignature.ts @@ -9,6 +9,7 @@ const SIGNIFICANT_KEYS = [ 'useDPoP', 'maxRetries', 'credentialsManagerStorageKey', + 'networkingOptions', ] as const satisfies ReadonlyArray; // 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. diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index cdd63eaf..e0947e39 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -109,6 +109,7 @@ export class NativeAuth0Client implements Auth0Client { useDPoP = false, maxRetries, credentialsManagerStorageKey, + networkingOptions, } = options; // Re-init when domain/clientId differ (hasValidInstance) or any other // identity option drifted from what was last applied to the native side. @@ -125,7 +126,8 @@ export class NativeAuth0Client implements Auth0Client { localAuthenticationOptions, useDPoP, maxRetries, - credentialsManagerStorageKey + credentialsManagerStorageKey, + networkingOptions ); } // Record even on the skip path so siblings differing only in a diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts index 8b0c97de..762ff8f1 100644 --- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts @@ -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 // networkingOptions not provided ); // Use client to avoid unused variable warning @@ -142,7 +143,8 @@ describe('NativeAuth0Client', () => { undefined, false, undefined, - 'tenant-b' + 'tenant-b', + undefined ); expect(client).toBeDefined(); }); @@ -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 // networkingOptions not provided ); // Use client to avoid unused variable warning expect(client).toBeDefined(); }); + it('should pass networkingOptions to initialize when provided', async () => { + mockBridgeInstance.hasValidInstance.mockResolvedValue(false); + const networkingOptions = { connectTimeout: 30, readTimeout: 30 }; + + const client = new NativeAuth0Client({ + ...options, + networkingOptions, + }); + await new Promise(process.nextTick); + + expect(mockBridgeInstance.initialize).toHaveBeenCalledWith( + options.clientId, + options.domain, + undefined, + false, + undefined, + undefined, + networkingOptions + ); + + expect(client).toBeDefined(); + }); + it('should ensure initialization is complete before calling a bridge method', async () => { let resolveInitialization: () => void; const initializationPromise = new Promise((resolve) => { @@ -686,6 +712,7 @@ describe('NativeAuth0Client', () => { undefined, false, undefined, + undefined, undefined ); expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1); @@ -729,6 +756,7 @@ describe('NativeAuth0Client', () => { undefined, false, // useDPoP flipped to false undefined, + undefined, undefined ); }); diff --git a/src/platforms/native/bridge/NativeBridge.ts b/src/platforms/native/bridge/NativeBridge.ts index ee1ec9f7..f4879163 100644 --- a/src/platforms/native/bridge/NativeBridge.ts +++ b/src/platforms/native/bridge/NativeBridge.ts @@ -9,6 +9,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, + NetworkingOptions, } from '../../../types'; import type { LocalAuthenticationOptions, @@ -39,6 +40,7 @@ export interface NativeBridge { * @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 networkingOptions Configures the native networking client. **Android only** - ignored on iOS. */ initialize( clientId: string, @@ -46,7 +48,8 @@ export interface NativeBridge { localAuthenticationOptions?: LocalAuthenticationOptions, useDPoP?: boolean, maxRetries?: number, - credentialsManagerStorageKey?: string + credentialsManagerStorageKey?: string, + networkingOptions?: NetworkingOptions ): Promise; /** diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index b05efc4c..47f8b855 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -11,6 +11,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, + NetworkingOptions, } from '../../../types'; import { SafariViewControllerPresentationStyle, @@ -60,7 +61,8 @@ export class NativeBridgeManager implements NativeBridge { localAuthenticationOptions?: LocalAuthenticationOptions, useDPoP: boolean = false, maxRetries: number = 0, - credentialsManagerStorageKey?: string + credentialsManagerStorageKey?: string, + networkingOptions?: NetworkingOptions ): Promise { // This is a new method we'd add to the native side to ensure the // underlying Auth0.swift/Auth0.android SDKs are configured. @@ -73,7 +75,8 @@ export class NativeBridgeManager implements NativeBridge { localAuthenticationOptions, useDPoP, maxRetries, - credentialsManagerStorageKey + credentialsManagerStorageKey, + networkingOptions ); } diff --git a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts index 1ac8c9f4..c77366f9 100644 --- a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts +++ b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts @@ -267,7 +267,8 @@ describe('NativeBridgeManager', () => { undefined, true, 0, - 'tenant-b' + 'tenant-b', + undefined ); }); @@ -282,7 +283,38 @@ describe('NativeBridgeManager', () => { undefined, // localAuthenticationOptions false, // useDPoP default 0, // maxRetries default - undefined // credentialsManagerStorageKey + undefined, // credentialsManagerStorageKey + undefined // networkingOptions + ); + }); + + it('forwards networkingOptions to the native module when provided', async () => { + const networkingOptions = { + connectTimeout: 30, + readTimeout: 30, + defaultHeaders: { 'X-Custom': 'value' }, + }; + + await bridge.initialize( + 'client-id', + 'tenant-c.auth0.com', + undefined, + false, + 0, + undefined, + networkingOptions + ); + + expect( + MockedAuth0NativeModule.initializeAuth0WithConfiguration + ).toHaveBeenCalledWith( + 'client-id', + 'tenant-c.auth0.com', + undefined, + false, + 0, + undefined, + networkingOptions ); }); diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index db9ba29c..1ac2ec68 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -26,7 +26,8 @@ export interface Spec extends TurboModule { { [key: string]: string | Int32 | boolean } | undefined, useDPoP: boolean | undefined, maxRetries: Int32, - credentialsManagerStorageKey: string | undefined + credentialsManagerStorageKey: string | undefined, + networkingOptions: Object | undefined ): Promise; /** diff --git a/src/types/common.ts b/src/types/common.ts index 8c386ddd..8d4103ce 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -214,9 +214,48 @@ export interface Auth0Options { * @remarks Native only (iOS/Android). Has no effect on the web platform. */ credentialsManagerStorageKey?: string; + /** + * Configures the native networking client (OkHttp) that Auth0.Android uses for every + * request it makes (web auth token exchange, credential renewal, MFA, passkeys, etc.). + * @remarks Android only. Accepted on iOS for API compatibility but has no effect. + */ + networkingOptions?: NetworkingOptions; // Telemetry and localAuthenticationOptions are platform-specific extensions } +/** + * Configuration for the native networking client used by Auth0.Android. + * Mirrors `DefaultClient.Builder` from the Auth0.Android SDK. + * + * @remarks Android only. Has no effect on iOS or web. + */ +export interface NetworkingOptions { + /** Connection timeout, in seconds. @default 10 */ + connectTimeout?: number; + /** Read timeout, in seconds. @default 10 */ + readTimeout?: number; + /** Write timeout, in seconds. @default 10 */ + writeTimeout?: number; + /** Overall timeout for the entire call, in seconds. `0` means no timeout. @default 0 */ + callTimeout?: number; + /** + * Headers sent on every request made by the native networking client. If a specific + * request sets a header with the same name, the request-level header takes precedence. + * @default {} + */ + defaultHeaders?: Record; + /** + * Enables verbose HTTP request/response logging to Logcat. + * + * @remarks + * **Debug-only.** Auth0.Android logs full request and response bodies at this level, + * which includes access, refresh, and ID tokens in plaintext for token-endpoint calls. + * Never enable this in production. + * @default false + */ + enableLogging?: boolean; +} + // ========= MFA Flexible Factors Grant Types ========= /** Represents an enrolled MFA authenticator. */