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 =