From 3b12574efa0ec2e1b43c3fbd5dd49aabb00e244c Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 5 Feb 2026 22:28:34 +0530 Subject: [PATCH 1/3] fix: Prevent DPoP replay protection error due to Okhttp retry --- .../auth0/android/request/DefaultClient.kt | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt index 928120e6..27f5c85c 100644 --- a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt +++ b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt @@ -1,12 +1,18 @@ package com.auth0.android.request import androidx.annotation.VisibleForTesting +import com.auth0.android.dpop.DPoPUtil import com.auth0.android.request.internal.GsonProvider import com.google.gson.Gson -import okhttp3.* +import okhttp3.Call +import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor @@ -56,6 +62,12 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val okHttpClient: OkHttpClient + // Using another client to prevent OkHttp from retrying network calls especially when using DPoP with replay protection mechanism. + // https://auth0team.atlassian.net/browse/ESD-56048. + // TODO: This should be replaced with the chain.retryOnConnectionFailure() API when we update to OkHttp 5+ + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val nonRetryableOkHttpClient: OkHttpClient + @Throws(IllegalArgumentException::class, IOException::class) override fun load(url: String, options: RequestOptions): ServerResponse { val response = prepareCall(url.toHttpUrl(), options).execute() @@ -90,12 +102,31 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV .url(urlBuilder.build()) .headers(headers) .build() - return okHttpClient.newCall(request) + + // Use non-retryable client for DPoP requests or token refresh requests + val client = if (shouldUseNonRetryableClient(headers)) { + nonRetryableOkHttpClient + } else { + okHttpClient + } + + return client.newCall(request) + } + + /** + * Determines if the request should use the non-retryable OkHttpClient. + * Returns true for: + * 1. Requests with DPoP header + */ + private fun shouldUseNonRetryableClient( + headers: Headers + ): Boolean { + return headers[DPoPUtil.DPOP_HEADER] != null } init { - // client setup val builder = OkHttpClient.Builder() + // Add retry interceptor builder.addInterceptor(RetryInterceptor()) // logging @@ -115,6 +146,11 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV } okHttpClient = builder.build() + + // Non-retryable client for DPoP requests + nonRetryableOkHttpClient = okHttpClient.newBuilder() + .retryOnConnectionFailure(false) + .build() } From 448ea29fce5f22c85a3a3f976ea281321be824c6 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Thu, 5 Feb 2026 22:42:58 +0530 Subject: [PATCH 2/3] Updated the test cases for the new client configuration --- .../android/request/DefaultClientTest.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt index 3b1dccd7..7a71a70b 100644 --- a/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/DefaultClientTest.kt @@ -230,6 +230,43 @@ public class DefaultClientTest { requestAssertions(sentRequest, HttpMethod.PATCH) } + @Test + public fun shouldHaveNonRetryableClientConfigured() { + val client = createDefaultClientForTest(mapOf()) + + assertThat(client.okHttpClient, notNullValue()) + assertThat(client.nonRetryableOkHttpClient, notNullValue()) + + assertThat(client.okHttpClient.retryOnConnectionFailure, equalTo(true)) + assertThat(client.nonRetryableOkHttpClient.retryOnConnectionFailure, equalTo(false)) + } + + @Test + public fun shouldShareSameConfigBetweenClients() { + val client = createDefaultClientForTest(mapOf()) + + assertThat( + client.okHttpClient.interceptors.size, + equalTo(client.nonRetryableOkHttpClient.interceptors.size) + ) + + assertThat( + client.okHttpClient.interceptors[0] is RetryInterceptor, + equalTo(true) + ) + assertThat( + client.nonRetryableOkHttpClient.interceptors[0] is RetryInterceptor, + equalTo(true) + ) + assertThat( + client.okHttpClient.connectTimeoutMillis, + equalTo(client.nonRetryableOkHttpClient.connectTimeoutMillis) + ) + assertThat( + client.okHttpClient.readTimeoutMillis, + equalTo(client.nonRetryableOkHttpClient.readTimeoutMillis) + ) + } //Helper methods private fun requestAssertions( From 1ea9d2fca689b591ccab8140b954eebbea7cb510 Mon Sep 17 00:00:00 2001 From: Prince Mathew Date: Fri, 6 Feb 2026 10:04:02 +0530 Subject: [PATCH 3/3] nit:updated minor comment --- auth0/src/main/java/com/auth0/android/request/DefaultClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt index 27f5c85c..157f4f6b 100644 --- a/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt +++ b/auth0/src/main/java/com/auth0/android/request/DefaultClient.kt @@ -103,7 +103,7 @@ public class DefaultClient @VisibleForTesting(otherwise = VisibleForTesting.PRIV .headers(headers) .build() - // Use non-retryable client for DPoP requests or token refresh requests + // Use non-retryable client for DPoP requests val client = if (shouldUseNonRetryableClient(headers)) { nonRetryableOkHttpClient } else {