diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-cefa8ba.json b/.changes/next-release/bugfix-AWSSDKforJavav2-cefa8ba.json new file mode 100644 index 000000000000..dd0ee10b36c9 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-cefa8ba.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fixed 407 Proxy Authentication error when preemptiveBasicAuthenticationEnabled is true. Fixes [#5884](https://github.com/aws/aws-sdk-java-v2/issues/5884)." +} diff --git a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/utils/ApacheUtils.java b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/utils/ApacheUtils.java index e35eca470d53..dbf194cd73e2 100644 --- a/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/utils/ApacheUtils.java +++ b/http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/utils/ApacheUtils.java @@ -19,6 +19,7 @@ import java.io.UncheckedIOException; import org.apache.http.HttpEntity; import org.apache.http.HttpHost; +import org.apache.http.auth.AUTH; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.NTCredentials; @@ -30,6 +31,7 @@ import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.BasicAuthCache; import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.message.BasicHeader; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.apache.ProxyConfiguration; import software.amazon.awssdk.utils.Logger; @@ -149,10 +151,16 @@ private static void addPreemptiveAuthenticationProxy(HttpClientContext clientCon AuthCache authCache = new BasicAuthCache(); // Generate BASIC scheme object and add it to the local auth cache BasicScheme basicAuth = new BasicScheme(); - authCache.put(targetHost, basicAuth); + try { + basicAuth.processChallenge(new BasicHeader(AUTH.PROXY_AUTH, "BASIC realm=default")); + authCache.put(targetHost, basicAuth); + clientContext.setAuthCache(authCache); + } catch (Exception e) { + logger.debug(() -> "Failed to process synthetic challenge for preemptive proxy authentication: " + + e.getMessage()); + } clientContext.setCredentialsProvider(credsProvider); - clientContext.setAuthCache(authCache); } } 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 new file mode 100644 index 000000000000..2c021cae41fa --- /dev/null +++ b/http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java @@ -0,0 +1,148 @@ +/* + * 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.apache; + +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.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +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; + +/** + * Tests proxy preemptive authentication functionality. + * + * Verifies that when preemptiveBasicAuthenticationEnabled(true) is configured, + * the Proxy-Authorization header is sent with the first request to the proxy. + */ +public class ApacheHttpClientProxyAuthTest { + + private WireMockServer mockProxy; + private SdkHttpClient httpClient; + + @BeforeEach + public void setup() { + mockProxy = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + mockProxy.start(); + } + + @AfterEach + public void teardown() { + if (httpClient != null) { + httpClient.close(); + } + if (mockProxy != null) { + mockProxy.stop(); + } + } + + @Test + public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthorizationHeader() throws Exception { + mockProxy.stubFor(any(anyUrl()) + .withHeader("Proxy-Authorization", matching("Basic .+")) + .willReturn(aResponse() + .withStatus(200) + .withBody("Success"))); + + // Create HTTP client with preemptive proxy authentication enabled + httpClient = ApacheHttpClient.builder() + .proxyConfiguration(ProxyConfiguration.builder() + .endpoint(URI.create("http://localhost:" + mockProxy.port())) + .username("testuser") + .password("testpass") + .preemptiveBasicAuthenticationEnabled(true) + .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 with preemptive auth header + HttpExecuteResponse response = httpClient.prepareRequest(executeRequest).call(); + assertThat(response.httpResponse().statusCode()).isEqualTo(200); + + mockProxy.verify(1, anyRequestedFor(anyUrl())); + mockProxy.verify(WireMock.getRequestedFor(anyUrl()) + .withHeader("Proxy-Authorization", matching("Basic .+"))); + } + + @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 .+")) + .willReturn(aResponse() + .withStatus(200) + .withBody("Success"))); + + // Create HTTP client with preemptive proxy authentication disabled + httpClient = ApacheHttpClient.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 .+"))); + } +}