Skip to content

Commit 31f81ae

Browse files
committed
HTTPCLIENT-2414 - Fix Basic auth cache scoping across path prefixes
Preserve AuthExchange pathPrefix on reset to avoid preemptive Authorization reuse outside the protection space.
1 parent 9189d11 commit 31f81ae

3 files changed

Lines changed: 142 additions & 7 deletions

File tree

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@
2626
*/
2727
package org.apache.hc.client5.testing.sync;
2828

29+
import static org.hamcrest.MatcherAssert.assertThat;
30+
2931
import java.io.ByteArrayInputStream;
3032
import java.nio.charset.StandardCharsets;
3133
import java.security.SecureRandom;
3234
import java.util.Arrays;
3335
import java.util.Collections;
36+
import java.util.List;
3437
import java.util.Queue;
3538
import java.util.concurrent.ConcurrentLinkedQueue;
39+
import java.util.concurrent.CopyOnWriteArrayList;
3640
import java.util.concurrent.atomic.AtomicLong;
3741
import java.util.function.Consumer;
3842
import java.util.stream.Collectors;
@@ -57,6 +61,7 @@
5761
import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
5862
import org.apache.hc.client5.http.protocol.HttpClientContext;
5963
import org.apache.hc.client5.testing.BasicTestAuthenticator;
64+
import org.apache.hc.client5.testing.auth.AuthResult;
6065
import org.apache.hc.client5.testing.auth.Authenticator;
6166
import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
6267
import org.apache.hc.client5.testing.classic.AuthenticatingDecorator;
@@ -82,6 +87,7 @@
8287
import org.apache.hc.core5.http.protocol.HttpContext;
8388
import org.apache.hc.core5.http.support.BasicResponseBuilder;
8489
import org.apache.hc.core5.net.URIAuthority;
90+
import org.hamcrest.CoreMatchers;
8591
import org.junit.jupiter.api.Assertions;
8692
import org.junit.jupiter.api.Test;
8793
import org.mockito.Mockito;
@@ -339,8 +345,9 @@ void testBasicAuthenticationCredentialsCaching() throws Exception {
339345

340346
Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
341347

342-
Assertions.assertEquals(Arrays.asList(401, 200, 200, 200, 200, 200),
343-
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()));
348+
assertThat(
349+
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
350+
CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200, 200)));
344351
}
345352

346353
@Test
@@ -381,8 +388,9 @@ void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
381388
// There should be only single auth strategy call for all successful message exchanges
382389
Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
383390

384-
Assertions.assertEquals(Arrays.asList(401, 200, 200, 200, 200),
385-
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()));
391+
assertThat(
392+
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
393+
CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
386394

387395
responseQueue.clear();
388396
authCache.clear();
@@ -400,10 +408,11 @@ void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
400408
}
401409

402410
// There should be an auth strategy call for all successful message exchanges
403-
Mockito.verify(authStrategy, Mockito.times(2)).select(Mockito.any(), Mockito.any(), Mockito.any());
411+
Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any());
404412

405-
Assertions.assertEquals(Arrays.asList(200, 401, 200, 200, 401, 200),
406-
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()));
413+
assertThat(
414+
responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
415+
CoreMatchers.equalTo(Arrays.asList(200, 401, 200, 401, 200, 401, 200)));
407416
}
408417

409418
@Test
@@ -814,4 +823,124 @@ void testBearerTokenAuthentication() throws Exception {
814823
});
815824
}
816825

826+
@Test
827+
void testBasicAuthenticationCredentialsCachingDifferentPathPrefixesSameContext() throws Exception {
828+
final List<RequestSnapshot> requests = new CopyOnWriteArrayList<>();
829+
final Authenticator authenticator = new BasicTestAuthenticator("test:test", "test realm") {
830+
@Override
831+
public AuthResult perform(final URIAuthority authority,
832+
final String requestUri,
833+
final String credentials) {
834+
requests.add(new RequestSnapshot(requestUri, credentials != null));
835+
return super.perform(authority, requestUri, credentials);
836+
}
837+
};
838+
configureServerWithBasicAuth(authenticator, bootstrap -> bootstrap.register("*", new EchoHandler()));
839+
final HttpHost target = startServer();
840+
841+
final List<Integer> responseCodes = new CopyOnWriteArrayList<>();
842+
843+
configureClient(builder -> builder
844+
.addResponseInterceptorLast((response, entity, context) -> responseCodes.add(response.getCode())));
845+
846+
final TestClient client = client();
847+
848+
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
849+
.add(target, "test", "test".toCharArray())
850+
.build();
851+
852+
final HttpClientContext context = HttpClientContext.create();
853+
context.setAuthCache(new BasicAuthCache());
854+
context.setCredentialsProvider(credentialsProvider);
855+
856+
for (final String requestPath : new String[]{"/blah/a", "/blubb/b"}) {
857+
final HttpGet httpGet = new HttpGet(requestPath);
858+
client.execute(target, httpGet, context, response -> {
859+
EntityUtils.consume(response.getEntity());
860+
return null;
861+
});
862+
}
863+
864+
Assertions.assertEquals(Arrays.asList(401, 200, 401, 200), responseCodes);
865+
866+
assertHandshakePerPath(requests, "/blah/a");
867+
assertHandshakePerPath(requests, "/blubb/b");
868+
}
869+
870+
private static void assertHandshakePerPath(final List<RequestSnapshot> requests, final String path) {
871+
final List<RequestSnapshot> filtered = requests.stream()
872+
.filter(r -> path.equals(r.path))
873+
.collect(Collectors.toList());
874+
875+
Assertions.assertEquals(2, filtered.size(), "Expected 2 requests for " + path + " (challenge + retry)");
876+
Assertions.assertFalse(filtered.get(0).hasAuthorization, "First request to " + path + " must not be preemptive");
877+
Assertions.assertTrue(filtered.get(1).hasAuthorization, "Second request to " + path + " must carry Authorization");
878+
}
879+
880+
private static final class RequestSnapshot {
881+
private final String path;
882+
private final boolean hasAuthorization;
883+
884+
private RequestSnapshot(final String path, final boolean hasAuthorization) {
885+
this.path = path;
886+
this.hasAuthorization = hasAuthorization;
887+
}
888+
}
889+
890+
@Test
891+
void testBasicAuthenticationCredentialsCachingPerPrefixAndReuseWithinPrefix() throws Exception {
892+
final List<RequestSnapshot> requests = new CopyOnWriteArrayList<>();
893+
final Authenticator authenticator = new BasicTestAuthenticator("test:test", "test realm") {
894+
@Override
895+
public AuthResult perform(final URIAuthority authority,
896+
final String requestUri,
897+
final String credentials) {
898+
requests.add(new RequestSnapshot(requestUri, credentials != null));
899+
return super.perform(authority, requestUri, credentials);
900+
}
901+
};
902+
configureServerWithBasicAuth(authenticator, bootstrap -> bootstrap.register("*", new EchoHandler()));
903+
final HttpHost target = startServer();
904+
905+
final List<Integer> responseCodes = new CopyOnWriteArrayList<>();
906+
configureClient(builder -> builder
907+
.addResponseInterceptorLast((response, entity, context) -> responseCodes.add(response.getCode())));
908+
909+
final TestClient client = client();
910+
911+
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
912+
.add(target, "test", "test".toCharArray())
913+
.build();
914+
915+
final HttpClientContext context = HttpClientContext.create();
916+
context.setAuthCache(new BasicAuthCache());
917+
context.setCredentialsProvider(credentialsProvider);
918+
919+
// First hit per prefix must challenge; subsequent hits under same prefix should be preemptive (200 only)
920+
for (final String requestPath : new String[]{"/blah/a", "/blubb/b", "/blubb/c", "/blah/d"}) {
921+
final HttpGet httpGet = new HttpGet(requestPath);
922+
client.execute(target, httpGet, context, response -> {
923+
EntityUtils.consume(response.getEntity());
924+
return null;
925+
});
926+
}
927+
928+
Assertions.assertEquals(Arrays.asList(401, 200, 401, 200, 200, 200), responseCodes);
929+
930+
assertHandshakePerPath(requests, "/blah/a");
931+
assertHandshakePerPath(requests, "/blubb/b");
932+
assertPreemptivePerPath(requests, "/blubb/c");
933+
assertPreemptivePerPath(requests, "/blah/d");
934+
}
935+
936+
private static void assertPreemptivePerPath(final List<RequestSnapshot> requests, final String path) {
937+
final List<RequestSnapshot> filtered = requests.stream()
938+
.filter(r -> path.equals(r.path))
939+
.collect(Collectors.toList());
940+
941+
Assertions.assertEquals(1, filtered.size(), "Expected 1 request for " + path + " (preemptive)");
942+
Assertions.assertTrue(filtered.get(0).hasAuthorization, "Request to " + path + " must carry Authorization");
943+
}
944+
945+
817946
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ private boolean needAuthentication(
340340
targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response,
341341
targetAuthStrategy, targetAuthExchange, context);
342342

343+
if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() == null) {
344+
targetAuthExchange.setPathPrefix(pathPrefix);
345+
}
343346
if (authCacheKeeper != null) {
344347
authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
345348
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ private boolean needAuthentication(
301301
targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response,
302302
targetAuthStrategy, targetAuthExchange, context);
303303

304+
if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() == null) {
305+
targetAuthExchange.setPathPrefix(pathPrefix);
306+
}
304307
if (authCacheKeeper != null) {
305308
authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
306309
}

0 commit comments

Comments
 (0)