From 65a132a1596b94637f54a73d3be29379bdf2fbcb Mon Sep 17 00:00:00 2001 From: OmarAlJarrah Date: Wed, 17 Jun 2026 04:43:50 +0300 Subject: [PATCH] docs: state both transports do Basic-only proxy auth The proxy-auth documentation contradicted itself. ProxyOptions told callers the JDK transport "negotiates Basic or Digest" with the proxy, the JDK transport's KDoc steered Digest users to the OkHttp transport, and the OkHttp transport's KDoc said it only ever emits Basic. At most one of those could be right. In reality both shipped transports authenticate the proxy with Basic only. The OkHttp proxyAuthenticator always responds with Credentials.basic(...). The JDK transport installs a java.net.Authenticator on java.net.http.HttpClient, whose built-in handling of a registered authenticator covers the Basic scheme only and does not drive Digest proxy auth through that hook. Reconcile all three KDoc blocks and the two runtime warning strings: - ProxyOptions now states both transports do Basic-only proxy auth and points Digest-proxy users at a caller-supplied client (a java.net.http.HttpClient / OkHttpClient with their own authenticator) passed through create(...). - The JDK transport KDoc records the Basic-only limitation explicitly and drops the "use the OkHttp transport for Digest" steer, which sent readers in a circle. The challenge-handler warning now points at the BYO-client path. - The OkHttp transport warning likewise points at a BYO OkHttpClient with a custom proxyAuthenticator instead of implying another shipped transport supports Digest. Add a ProxyAuthenticator test pinning that the JDK authenticator returns the raw credentials regardless of the advertised scheme, so the Basic-only behaviour is the JDK client's and a Digest-only proxy is not satisfied end-to-end. Closes #109 --- .../org/dexpace/sdk/core/util/ProxyOptions.kt | 19 +++++++++++---- .../sdk/transport/jdkhttp/JdkHttpTransport.kt | 17 +++++++++----- .../jdkhttp/ProxyAuthenticatorTest.kt | 23 ++++++++++++++++++- .../sdk/transport/okhttp/OkHttpTransport.kt | 3 ++- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ProxyOptions.kt b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ProxyOptions.kt index d334aca1..ff63da43 100644 --- a/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ProxyOptions.kt +++ b/sdk-core/src/main/kotlin/org/dexpace/sdk/core/util/ProxyOptions.kt @@ -27,14 +27,23 @@ import java.util.regex.Pattern * * ## Proxy authentication * - * Proxy auth is driven by [username] / [password]. The shipped transports apply them as follows: - * - OkHttp transport: sets up **Basic** proxy authentication from [username] / [password]. - * - JDK transport: passes [username] / [password] to the `java.net.http` stack, which negotiates - * **Basic** or **Digest** with the proxy itself. + * Proxy auth is driven by [username] / [password]. **Both shipped transports authenticate the + * proxy with the Basic scheme only:** + * - OkHttp transport: its `proxyAuthenticator` emits `Proxy-Authorization: Basic …` from + * [username] / [password]. + * - JDK transport: installs a `java.net.Authenticator` on the `java.net.http` client. That + * built-in integration answers Basic proxy challenges only; it does not implement Digest + * proxy auth. + * + * Neither transport performs Digest (or any other non-Basic scheme) proxy authentication. To + * authenticate against a Digest-only proxy, supply your own pre-configured client — a + * `java.net.http.HttpClient` or `OkHttpClient` carrying your own authenticator — through the + * transport's `create(...)` entry point; the SDK uses such a client as-is and does not override + * its proxy authentication. * * [challengeHandler] is **currently not honoured by any shipped transport** — it is reserved for * a future pluggable proxy-auth mechanism. Setting it has no effect today (the transports ignore - * it and log a warning), so supply [username] / [password] for proxy authentication. + * it and log a warning), so supply [username] / [password] for Basic proxy authentication. * * ## Bypass-all semantics (breaking change from pre-v2 API) * diff --git a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt index fdb0e83a..65f90dd1 100644 --- a/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt +++ b/sdk-transport-jdkhttp/src/main/kotlin/org/dexpace/sdk/transport/jdkhttp/JdkHttpTransport.kt @@ -376,14 +376,18 @@ public class JdkHttpTransport private constructor( * [ProxyAuthenticator]. The JDK client picks up this authenticator at request * time; it answers **only** proxy (407) challenges whose host/port match the * configured proxy, returning `null` for origin-server (401) challenges so the - * proxy credentials never leak to an origin host. + * proxy credentials never leak to an origin host. The `java.net.http` client's + * built-in handling of a registered `Authenticator` covers the **Basic** scheme + * only — this transport does **not** perform Digest proxy authentication. * * A configured [ProxyOptions.challengeHandler] is **not** honoured by this transport: * `java.net.http.HttpClient` exposes no per-407 hook through which a custom * `ChallengeHandler` (e.g. Digest) could be invoked, so the handler is dropped with a - * loud warning rather than silently ignored. Proxy authentication falls back to the - * JDK's own username/password negotiation via [ProxyAuthenticator]. Consumers needing - * Digest proxy auth should use the OkHttp transport. + * loud warning rather than silently ignored. Proxy authentication falls back to Basic + * auth derived from [ProxyOptions.username] / [ProxyOptions.password] via + * [ProxyAuthenticator]. To authenticate against a Digest-only proxy, pass a + * pre-configured `java.net.http.HttpClient` carrying your own `Authenticator` to + * [create]; the SDK uses that client as-is. * * Credentials are deliberately never logged. */ @@ -397,8 +401,9 @@ public class JdkHttpTransport private constructor( .log( "ProxyOptions.challengeHandler is set but the JDK transport cannot invoke a " + "custom ChallengeHandler: java.net.http.HttpClient exposes no per-407 hook. " + - "The handler is ignored; proxy auth falls back to username/password via the " + - "JDK's own auth negotiation. Use the OkHttp transport for Digest proxy auth.", + "The handler is ignored; proxy auth falls back to Basic auth derived from " + + "ProxyOptions.username / ProxyOptions.password. For Digest proxy auth, pass a " + + "pre-configured java.net.http.HttpClient with your own Authenticator to create().", ) } if (options.type != ProxyOptions.Type.HTTP) { diff --git a/sdk-transport-jdkhttp/src/test/kotlin/org/dexpace/sdk/transport/jdkhttp/ProxyAuthenticatorTest.kt b/sdk-transport-jdkhttp/src/test/kotlin/org/dexpace/sdk/transport/jdkhttp/ProxyAuthenticatorTest.kt index 6be33145..86046421 100644 --- a/sdk-transport-jdkhttp/src/test/kotlin/org/dexpace/sdk/transport/jdkhttp/ProxyAuthenticatorTest.kt +++ b/sdk-transport-jdkhttp/src/test/kotlin/org/dexpace/sdk/transport/jdkhttp/ProxyAuthenticatorTest.kt @@ -61,10 +61,31 @@ class ProxyAuthenticatorTest { assertNull(auth, "a proxy challenge on a non-configured port must not receive credentials") } + /** + * Pins the documented proxy-auth contract: the credentials this authenticator returns are + * the raw username/password, with no scheme negotiation of its own. Whether a challenge is + * actually satisfied is decided by the `java.net.http` client's built-in handling of a + * registered `Authenticator`, which covers the **Basic** scheme only — it does not drive + * Digest proxy auth through this hook. The authenticator therefore returns the same + * credentials whether the proxy advertises `Basic` or `Digest`; a Digest-only proxy is not + * authenticated end-to-end, matching the [JdkHttpTransport.Builder] KDoc. + */ + @Test + fun `proxy challenge credentials carry no scheme of their own`() { + val proxy = Authenticator.RequestorType.PROXY + val basic = challenge(host = "proxy.example", port = 3128, type = proxy, scheme = "Basic") + val digest = challenge(host = "proxy.example", port = 3128, type = proxy, scheme = "Digest") + requireNotNull(basic) { "Basic proxy challenge must be answered" } + requireNotNull(digest) { "the authenticator does not inspect the scheme string" } + assertEquals(basic.userName, digest.userName) + assertEquals(String(basic.password), String(digest.password)) + } + private fun challenge( host: String, port: Int, type: Authenticator.RequestorType, + scheme: String = "Basic", ): PasswordAuthentication? = Authenticator.requestPasswordAuthentication( authenticator, @@ -73,7 +94,7 @@ class ProxyAuthenticatorTest { port, "http", "challenge", - "Basic", + scheme, URL("http://$host:$port/"), type, ) diff --git a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/OkHttpTransport.kt b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/OkHttpTransport.kt index c2a30777..d43c6373 100644 --- a/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/OkHttpTransport.kt +++ b/sdk-transport-okhttp/src/main/kotlin/org/dexpace/sdk/transport/okhttp/OkHttpTransport.kt @@ -369,7 +369,8 @@ public class OkHttpTransport private constructor( "The OkHttp transport does not honour ProxyOptions.challengeHandler; it is " + "ignored. Proxy authentication falls back to Basic auth derived from " + "ProxyOptions.username / ProxyOptions.password. Supply those credentials, " + - "or use a transport that supports a custom proxy challenge handler.", + "or for Digest proxy auth pass a pre-configured OkHttpClient with your own " + + "proxyAuthenticator to create().", ) } val javaType =