From 2a3af9cc6dc8835ee8bcca3f7bb50b4c21b21bb4 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Mon, 24 Nov 2025 14:24:15 -0800 Subject: [PATCH] Fix proxy basic auth In Apache5, the auth scheme requires a specific class type for the Credentials object, otherwise the credentials will be considered missing. NTLMScheme: https://github.com/apache/httpcomponents-client/blob/79992d832cc5d9fb4ce8307200dae844085b9d3d/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/NTLMScheme.java#L143 BasicScheme: https://github.com/apache/httpcomponents-client/blob/79992d832cc5d9fb4ce8307200dae844085b9d3d/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicScheme.java#L159 --- ...gfix-Apache5HTTPClientPreview-169e794.json | 6 + .../apache/ApacheHttpClientProxyAuthTest.java | 26 ++-- .../apache5/internal/utils/Apache5Utils.java | 23 +++- .../Apache5HttpClientProxyAuthTest.java | 115 ++++++++++++++++++ .../internal/utils/Apache5UtilsTest.java | 62 ++++++++++ 5 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 .changes/next-release/bugfix-Apache5HTTPClientPreview-169e794.json create mode 100644 http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientProxyAuthTest.java create mode 100644 http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5UtilsTest.java diff --git a/.changes/next-release/bugfix-Apache5HTTPClientPreview-169e794.json b/.changes/next-release/bugfix-Apache5HTTPClientPreview-169e794.json new file mode 100644 index 000000000000..dd8a603e7187 --- /dev/null +++ b/.changes/next-release/bugfix-Apache5HTTPClientPreview-169e794.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Apache5 HTTP Client (Preview)", + "contributor": "", + "description": "Fix bug where Basic proxy authentication fails with credentials not found." +} diff --git a/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java b/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java index 2c021cae41fa..e1e8b5792afa 100644 --- a/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java +++ b/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java @@ -19,13 +19,14 @@ import static com.github.tomakehurst.wiremock.client.WireMock.any; import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; -import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static org.assertj.core.api.Assertions.assertThat; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import java.net.URI; +import java.util.Base64; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,6 +43,13 @@ * the Proxy-Authorization header is sent with the first request to the proxy. */ public class ApacheHttpClientProxyAuthTest { + private static final String USERNAME = "testuser"; + private static final String PASSWORD = "testpass"; + + // Header value is "Basic " + base64( + ':' + ) + // https://datatracker.ietf.org/doc/html/rfc7617#section-2 + private static final String BASIC_PROXY_AUTH_HEADER = + "Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes()); private WireMockServer mockProxy; private SdkHttpClient httpClient; @@ -65,7 +73,7 @@ public void teardown() { @Test public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthorizationHeader() throws Exception { mockProxy.stubFor(any(anyUrl()) - .withHeader("Proxy-Authorization", matching("Basic .+")) + .withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER)) .willReturn(aResponse() .withStatus(200) .withBody("Success"))); @@ -74,8 +82,8 @@ public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthori httpClient = ApacheHttpClient.builder() .proxyConfiguration(ProxyConfiguration.builder() .endpoint(URI.create("http://localhost:" + mockProxy.port())) - .username("testuser") - .password("testpass") + .username(USERNAME) + .password(PASSWORD) .preemptiveBasicAuthenticationEnabled(true) .build()) .build(); @@ -96,7 +104,7 @@ public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthori mockProxy.verify(1, anyRequestedFor(anyUrl())); mockProxy.verify(WireMock.getRequestedFor(anyUrl()) - .withHeader("Proxy-Authorization", matching("Basic .+"))); + .withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER))); } @Test @@ -109,7 +117,7 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes // Second request with auth header should succeed mockProxy.stubFor(any(anyUrl()) - .withHeader("Proxy-Authorization", matching("Basic .+")) + .withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER)) .willReturn(aResponse() .withStatus(200) .withBody("Success"))); @@ -118,8 +126,8 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes httpClient = ApacheHttpClient.builder() .proxyConfiguration(ProxyConfiguration.builder() .endpoint(URI.create("http://localhost:" + mockProxy.port())) - .username("testuser") - .password("testpass") + .username(USERNAME) + .password(PASSWORD) .preemptiveBasicAuthenticationEnabled(false) .build()) .build(); @@ -143,6 +151,6 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes // First request without auth header mockProxy.verify(1, anyRequestedFor(anyUrl()).withoutHeader("Proxy-Authorization")); // Second request with auth header - mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", matching("Basic .+"))); + mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER))); } } diff --git a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java index 2b6afe771775..d559808e7843 100644 --- a/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java +++ b/http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java @@ -22,6 +22,7 @@ import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.NTCredentials; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; @@ -35,7 +36,6 @@ @SdkInternalApi public final class Apache5Utils { - private Apache5Utils() { } @@ -71,14 +71,14 @@ public static HttpClientContext newClientContext(ProxyConfiguration proxyConfigu */ public static CredentialsProvider newProxyCredentialsProvider(ProxyConfiguration proxyConfiguration) { BasicCredentialsProvider provider = new BasicCredentialsProvider(); - provider.setCredentials(newAuthScope(proxyConfiguration), newNtCredentials(proxyConfiguration)); + provider.setCredentials(newAuthScope(proxyConfiguration), proxyCredentials(proxyConfiguration)); return provider; } /** * Returns a new instance of NTCredentials used for proxy authentication. */ - private static Credentials newNtCredentials(ProxyConfiguration proxyConfiguration) { + private static NTCredentials ntCredentials(ProxyConfiguration proxyConfiguration) { // Deprecated NTCredentials is used to maintain backward compatibility with Apache4. return new NTCredentials( proxyConfiguration.username(), @@ -88,6 +88,23 @@ private static Credentials newNtCredentials(ProxyConfiguration proxyConfiguratio ); } + /** + * Returns the credentials object used to authenticate with a proxy. This method returns either an {@link NTCredentials} + * object if either {@link ProxyConfiguration#ntlmDomain()} or {@link ProxyConfiguration#ntlmWorkstation()} are present, + * otherwise it returns a {@link UsernamePasswordCredentials}. + */ + private static Credentials proxyCredentials(ProxyConfiguration proxyConfiguration) { + if (proxyConfiguration.ntlmWorkstation() != null || proxyConfiguration.ntlmDomain() != null) { + return ntCredentials(proxyConfiguration); + } + return usernamePasswordCredentials(proxyConfiguration); + } + + public static UsernamePasswordCredentials usernamePasswordCredentials(ProxyConfiguration proxyConfiguration) { + return new UsernamePasswordCredentials(proxyConfiguration.username(), + proxyConfiguration.password().toCharArray()); + } + /** * Returns a new instance of AuthScope used for proxy authentication. */ diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientProxyAuthTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientProxyAuthTest.java new file mode 100644 index 000000000000..0f7649d4d67d --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/Apache5HttpClientProxyAuthTest.java @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.apache5; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.matching; +import static org.assertj.core.api.Assertions.assertThat; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +import java.util.Base64; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; + +public class Apache5HttpClientProxyAuthTest { + private static final String USERNAME = "testuser"; + private static final String PASSWORD = "testpass"; + + // Header value is "Basic " + base64( + ':' + ) + // https://datatracker.ietf.org/doc/html/rfc7617#section-2 + private static final String BASIC_PROXY_AUTH_HEADER = + "Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes()); + + private WireMockServer mockProxy; + private SdkHttpClient httpClient; + + @BeforeEach + public void setup() { + mockProxy = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockProxy.start(); +} + + @AfterEach + public void teardown() { + if (mockProxy != null) { + mockProxy.stop(); + mockProxy = null; + } + + if (httpClient != null) { + httpClient.close(); + httpClient = null; + } + } + + @Test + public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeResponseAuth() throws Exception { + // First request without auth header should get 407 + mockProxy.stubFor(any(anyUrl()) + .willReturn(aResponse() + .withStatus(407) + .withHeader("Proxy-Authenticate", "Basic realm=\"proxy\""))); + + // Second request with auth header should succeed + mockProxy.stubFor(any(anyUrl()) + .withHeader("Proxy-Authorization", matching(BASIC_PROXY_AUTH_HEADER)) + .willReturn(aResponse() + .withStatus(200) + .withBody("Success"))); + + // Create HTTP client with preemptive proxy authentication disabled + httpClient = Apache5HttpClient.builder() + .proxyConfiguration(ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:" + mockProxy.port())) + .username("testuser") + .password("testpass") + .preemptiveBasicAuthenticationEnabled(false) + .build()) + .build(); + + // Create a request + SdkHttpRequest request = SdkHttpRequest.builder() + .method(SdkHttpMethod.GET) + .uri(URI.create("http://example.com/test")) + .build(); + + HttpExecuteRequest executeRequest = HttpExecuteRequest.builder() + .request(request) + .build(); + + // Execute the request - should succeed after challenge-response + HttpExecuteResponse response = httpClient.prepareRequest(executeRequest).call(); + assertThat(response.httpResponse().statusCode()).isEqualTo(200); + + // Verify challenge-response flow - 2 requests total + mockProxy.verify(2, anyRequestedFor(anyUrl())); + // First request without auth header + mockProxy.verify(1, anyRequestedFor(anyUrl()).withoutHeader("Proxy-Authorization")); + // Second request with auth header + mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", matching(BASIC_PROXY_AUTH_HEADER))); + } +} diff --git a/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5UtilsTest.java b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5UtilsTest.java new file mode 100644 index 000000000000..ec566a625bfc --- /dev/null +++ b/http-clients/apache5-client/src/test/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5UtilsTest.java @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.apache5.internal.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.NTCredentials; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.apache5.ProxyConfiguration; + +public class Apache5UtilsTest { + private static final AuthScope AUTH_SCOPE = new AuthScope("localhost", 8080); + + @Test + public void proxyCredentials_ntlmDetailsNotPresent_usesUsernameAndPassword() { + ProxyConfiguration config = + ProxyConfiguration.builder().username("name").password("pass").endpoint(URI.create("localhost:8080")).build(); + + assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null)) + .isInstanceOf(UsernamePasswordCredentials.class); + } + + @Test + public void proxyCredentials_ntlmWorkstationPresent_usesNtCredentials() { + ProxyConfiguration config = ProxyConfiguration.builder() + .username("name") + .password("pass") + .ntlmWorkstation("workstation") + .endpoint(URI.create("localhost:8080")).build(); + + assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null)) + .isInstanceOf(NTCredentials.class); + } + + @Test + public void proxyCredentials_ntlmDomainPresent_usesNtCredentials() { + ProxyConfiguration config = ProxyConfiguration.builder() + .username("name") + .password("pass") + .ntlmDomain("domain") + .endpoint(URI.create("localhost:8080")).build(); + + assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null)) + .isInstanceOf(NTCredentials.class); + } +}