Skip to content

Commit ccf3061

Browse files

File tree

5 files changed

+220
-12
lines changed

5 files changed

+220
-12
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Apache5 HTTP Client (Preview)",
4+
"contributor": "",
5+
"description": "Fix bug where Basic proxy authentication fails with credentials not found."
6+
}

http-clients/apache-client/src/test/java/software/amazon/awssdk/http/apache/ApacheHttpClientProxyAuthTest.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
import static com.github.tomakehurst.wiremock.client.WireMock.any;
2020
import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
2121
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
22-
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
2323
import static org.assertj.core.api.Assertions.assertThat;
2424

2525
import com.github.tomakehurst.wiremock.WireMockServer;
2626
import com.github.tomakehurst.wiremock.client.WireMock;
2727
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
2828
import java.net.URI;
29+
import java.util.Base64;
2930
import org.junit.jupiter.api.AfterEach;
3031
import org.junit.jupiter.api.BeforeEach;
3132
import org.junit.jupiter.api.Test;
@@ -42,6 +43,13 @@
4243
* the Proxy-Authorization header is sent with the first request to the proxy.
4344
*/
4445
public class ApacheHttpClientProxyAuthTest {
46+
private static final String USERNAME = "testuser";
47+
private static final String PASSWORD = "testpass";
48+
49+
// Header value is "Basic " + base64(<username> + ':' + <password>)
50+
// https://datatracker.ietf.org/doc/html/rfc7617#section-2
51+
private static final String BASIC_PROXY_AUTH_HEADER =
52+
"Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes());
4553

4654
private WireMockServer mockProxy;
4755
private SdkHttpClient httpClient;
@@ -65,7 +73,7 @@ public void teardown() {
6573
@Test
6674
public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthorizationHeader() throws Exception {
6775
mockProxy.stubFor(any(anyUrl())
68-
.withHeader("Proxy-Authorization", matching("Basic .+"))
76+
.withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER))
6977
.willReturn(aResponse()
7078
.withStatus(200)
7179
.withBody("Success")));
@@ -74,8 +82,8 @@ public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthori
7482
httpClient = ApacheHttpClient.builder()
7583
.proxyConfiguration(ProxyConfiguration.builder()
7684
.endpoint(URI.create("http://localhost:" + mockProxy.port()))
77-
.username("testuser")
78-
.password("testpass")
85+
.username(USERNAME)
86+
.password(PASSWORD)
7987
.preemptiveBasicAuthenticationEnabled(true)
8088
.build())
8189
.build();
@@ -96,7 +104,7 @@ public void proxyAuthentication_whenPreemptiveAuthEnabled_shouldSendProxyAuthori
96104

97105
mockProxy.verify(1, anyRequestedFor(anyUrl()));
98106
mockProxy.verify(WireMock.getRequestedFor(anyUrl())
99-
.withHeader("Proxy-Authorization", matching("Basic .+")));
107+
.withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER)));
100108
}
101109

102110
@Test
@@ -109,7 +117,7 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes
109117

110118
// Second request with auth header should succeed
111119
mockProxy.stubFor(any(anyUrl())
112-
.withHeader("Proxy-Authorization", matching("Basic .+"))
120+
.withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER))
113121
.willReturn(aResponse()
114122
.withStatus(200)
115123
.withBody("Success")));
@@ -118,8 +126,8 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes
118126
httpClient = ApacheHttpClient.builder()
119127
.proxyConfiguration(ProxyConfiguration.builder()
120128
.endpoint(URI.create("http://localhost:" + mockProxy.port()))
121-
.username("testuser")
122-
.password("testpass")
129+
.username(USERNAME)
130+
.password(PASSWORD)
123131
.preemptiveBasicAuthenticationEnabled(false)
124132
.build())
125133
.build();
@@ -143,6 +151,6 @@ public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeRes
143151
// First request without auth header
144152
mockProxy.verify(1, anyRequestedFor(anyUrl()).withoutHeader("Proxy-Authorization"));
145153
// Second request with auth header
146-
mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", matching("Basic .+")));
154+
mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", equalTo(BASIC_PROXY_AUTH_HEADER)));
147155
}
148156
}

http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/internal/utils/Apache5Utils.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.hc.client5.http.auth.Credentials;
2323
import org.apache.hc.client5.http.auth.CredentialsProvider;
2424
import org.apache.hc.client5.http.auth.NTCredentials;
25+
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
2526
import org.apache.hc.client5.http.config.RequestConfig;
2627
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
2728
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
@@ -35,7 +36,6 @@
3536

3637
@SdkInternalApi
3738
public final class Apache5Utils {
38-
3939
private Apache5Utils() {
4040
}
4141

@@ -71,14 +71,14 @@ public static HttpClientContext newClientContext(ProxyConfiguration proxyConfigu
7171
*/
7272
public static CredentialsProvider newProxyCredentialsProvider(ProxyConfiguration proxyConfiguration) {
7373
BasicCredentialsProvider provider = new BasicCredentialsProvider();
74-
provider.setCredentials(newAuthScope(proxyConfiguration), newNtCredentials(proxyConfiguration));
74+
provider.setCredentials(newAuthScope(proxyConfiguration), proxyCredentials(proxyConfiguration));
7575
return provider;
7676
}
7777

7878
/**
7979
* Returns a new instance of NTCredentials used for proxy authentication.
8080
*/
81-
private static Credentials newNtCredentials(ProxyConfiguration proxyConfiguration) {
81+
private static NTCredentials ntCredentials(ProxyConfiguration proxyConfiguration) {
8282
// Deprecated NTCredentials is used to maintain backward compatibility with Apache4.
8383
return new NTCredentials(
8484
proxyConfiguration.username(),
@@ -88,6 +88,23 @@ private static Credentials newNtCredentials(ProxyConfiguration proxyConfiguratio
8888
);
8989
}
9090

91+
/**
92+
* Returns the credentials object used to authenticate with a proxy. This method returns either an {@link NTCredentials}
93+
* object if either {@link ProxyConfiguration#ntlmDomain()} or {@link ProxyConfiguration#ntlmWorkstation()} are present,
94+
* otherwise it returns a {@link UsernamePasswordCredentials}.
95+
*/
96+
private static Credentials proxyCredentials(ProxyConfiguration proxyConfiguration) {
97+
if (proxyConfiguration.ntlmWorkstation() != null || proxyConfiguration.ntlmDomain() != null) {
98+
return ntCredentials(proxyConfiguration);
99+
}
100+
return usernamePasswordCredentials(proxyConfiguration);
101+
}
102+
103+
public static UsernamePasswordCredentials usernamePasswordCredentials(ProxyConfiguration proxyConfiguration) {
104+
return new UsernamePasswordCredentials(proxyConfiguration.username(),
105+
proxyConfiguration.password().toCharArray());
106+
}
107+
91108
/**
92109
* Returns a new instance of AuthScope used for proxy authentication.
93110
*/
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache5;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.any;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
22+
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
import com.github.tomakehurst.wiremock.WireMockServer;
26+
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
27+
import java.net.URI;
28+
import java.util.Base64;
29+
import org.junit.jupiter.api.AfterEach;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
import software.amazon.awssdk.http.HttpExecuteRequest;
33+
import software.amazon.awssdk.http.HttpExecuteResponse;
34+
import software.amazon.awssdk.http.SdkHttpClient;
35+
import software.amazon.awssdk.http.SdkHttpMethod;
36+
import software.amazon.awssdk.http.SdkHttpRequest;
37+
38+
public class Apache5HttpClientProxyAuthTest {
39+
private static final String USERNAME = "testuser";
40+
private static final String PASSWORD = "testpass";
41+
42+
// Header value is "Basic " + base64(<username> + ':' + <password>)
43+
// https://datatracker.ietf.org/doc/html/rfc7617#section-2
44+
private static final String BASIC_PROXY_AUTH_HEADER =
45+
"Basic " + Base64.getEncoder().encodeToString((USERNAME + ":" + PASSWORD).getBytes());
46+
47+
private WireMockServer mockProxy;
48+
private SdkHttpClient httpClient;
49+
50+
@BeforeEach
51+
public void setup() {
52+
mockProxy = new WireMockServer(WireMockConfiguration.options().dynamicPort());
53+
mockProxy.start();
54+
}
55+
56+
@AfterEach
57+
public void teardown() {
58+
if (mockProxy != null) {
59+
mockProxy.stop();
60+
mockProxy = null;
61+
}
62+
63+
if (httpClient != null) {
64+
httpClient.close();
65+
httpClient = null;
66+
}
67+
}
68+
69+
@Test
70+
public void proxyAuthentication_whenPreemptiveAuthDisabled_shouldUseChallengeResponseAuth() throws Exception {
71+
// First request without auth header should get 407
72+
mockProxy.stubFor(any(anyUrl())
73+
.willReturn(aResponse()
74+
.withStatus(407)
75+
.withHeader("Proxy-Authenticate", "Basic realm=\"proxy\"")));
76+
77+
// Second request with auth header should succeed
78+
mockProxy.stubFor(any(anyUrl())
79+
.withHeader("Proxy-Authorization", matching(BASIC_PROXY_AUTH_HEADER))
80+
.willReturn(aResponse()
81+
.withStatus(200)
82+
.withBody("Success")));
83+
84+
// Create HTTP client with preemptive proxy authentication disabled
85+
httpClient = Apache5HttpClient.builder()
86+
.proxyConfiguration(ProxyConfiguration.builder()
87+
.endpoint(URI.create("http://localhost:" + mockProxy.port()))
88+
.username("testuser")
89+
.password("testpass")
90+
.preemptiveBasicAuthenticationEnabled(false)
91+
.build())
92+
.build();
93+
94+
// Create a request
95+
SdkHttpRequest request = SdkHttpRequest.builder()
96+
.method(SdkHttpMethod.GET)
97+
.uri(URI.create("http://example.com/test"))
98+
.build();
99+
100+
HttpExecuteRequest executeRequest = HttpExecuteRequest.builder()
101+
.request(request)
102+
.build();
103+
104+
// Execute the request - should succeed after challenge-response
105+
HttpExecuteResponse response = httpClient.prepareRequest(executeRequest).call();
106+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
107+
108+
// Verify challenge-response flow - 2 requests total
109+
mockProxy.verify(2, anyRequestedFor(anyUrl()));
110+
// First request without auth header
111+
mockProxy.verify(1, anyRequestedFor(anyUrl()).withoutHeader("Proxy-Authorization"));
112+
// Second request with auth header
113+
mockProxy.verify(1, anyRequestedFor(anyUrl()).withHeader("Proxy-Authorization", matching(BASIC_PROXY_AUTH_HEADER)));
114+
}
115+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache5.internal.utils;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.net.URI;
21+
import org.apache.hc.client5.http.auth.AuthScope;
22+
import org.apache.hc.client5.http.auth.NTCredentials;
23+
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
24+
import org.junit.jupiter.api.Test;
25+
import software.amazon.awssdk.http.apache5.ProxyConfiguration;
26+
27+
public class Apache5UtilsTest {
28+
private static final AuthScope AUTH_SCOPE = new AuthScope("localhost", 8080);
29+
30+
@Test
31+
public void proxyCredentials_ntlmDetailsNotPresent_usesUsernameAndPassword() {
32+
ProxyConfiguration config =
33+
ProxyConfiguration.builder().username("name").password("pass").endpoint(URI.create("localhost:8080")).build();
34+
35+
assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null))
36+
.isInstanceOf(UsernamePasswordCredentials.class);
37+
}
38+
39+
@Test
40+
public void proxyCredentials_ntlmWorkstationPresent_usesNtCredentials() {
41+
ProxyConfiguration config = ProxyConfiguration.builder()
42+
.username("name")
43+
.password("pass")
44+
.ntlmWorkstation("workstation")
45+
.endpoint(URI.create("localhost:8080")).build();
46+
47+
assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null))
48+
.isInstanceOf(NTCredentials.class);
49+
}
50+
51+
@Test
52+
public void proxyCredentials_ntlmDomainPresent_usesNtCredentials() {
53+
ProxyConfiguration config = ProxyConfiguration.builder()
54+
.username("name")
55+
.password("pass")
56+
.ntlmDomain("domain")
57+
.endpoint(URI.create("localhost:8080")).build();
58+
59+
assertThat(Apache5Utils.newProxyCredentialsProvider(config).getCredentials(AUTH_SCOPE, null))
60+
.isInstanceOf(NTCredentials.class);
61+
}
62+
}

0 commit comments

Comments
 (0)