You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2021/09/27 17:14:03 UTC

[httpcomponents-client] 02/04: AuthCache conformance to RFC 7617

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit d3ca96cca1ac05e71771a483ea617c542b1c7dcf
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Sep 26 11:21:13 2021 +0200

    AuthCache conformance to RFC 7617
---
 .../AbstractHttpAsyncClientAuthentication.java     |  78 +++++++++
 .../testing/async/TestH2ClientAuthentication.java  |  12 ++
 .../async/TestHttp1ClientAuthentication.java       |  12 ++
 .../testing/sync/TestClientAuthentication.java     |  74 ++++++++
 .../apache/hc/client5/http/SchemePortResolver.java |  10 ++
 .../org/apache/hc/client5/http/auth/AuthCache.java |  61 +++++++
 .../apache/hc/client5/http/auth/AuthExchange.java  |  16 ++
 .../http/impl/DefaultAuthenticationStrategy.java   |   4 +-
 .../http/impl/DefaultSchemePortResolver.java       |  14 +-
 .../{ProtocolSupport.java => RequestSupport.java}  |  61 +++----
 .../client5/http/impl/async/AsyncConnectExec.java  |   8 +-
 .../client5/http/impl/async/AsyncProtocolExec.java |  36 ++--
 .../hc/client5/http/impl/auth/AuthCacheKeeper.java |  33 ++--
 .../hc/client5/http/impl/auth/BasicAuthCache.java  |  96 +++++++++--
 .../hc/client5/http/impl/classic/ConnectExec.java  |   8 +-
 .../hc/client5/http/impl/classic/ProtocolExec.java |  31 +++-
 .../hc/client5/http/impl/TestProtocolSupport.java  |  55 ------
 .../hc/client5/http/impl/TestRequestSupport.java   |  53 ++++++
 .../client5/http/impl/auth/TestBasicAuthCache.java |   2 +-
 .../http/impl/auth/TestRequestAuthCache.java       | 188 ---------------------
 20 files changed, 527 insertions(+), 325 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
index 88f73b2..5de4511 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthentication.java
@@ -26,14 +26,19 @@
  */
 package org.apache.hc.client5.testing.async;
 
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
 import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
+import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
@@ -42,6 +47,7 @@ import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
 import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -52,7 +58,9 @@ import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
@@ -64,9 +72,12 @@ import org.apache.hc.core5.http.impl.HttpProcessors;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.http2.config.H2Config;
 import org.apache.hc.core5.http2.impl.H2Processors;
 import org.apache.hc.core5.net.URIAuthority;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -104,6 +115,10 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
 
     abstract void setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy);
 
+    abstract void addResponseInterceptor(HttpResponseInterceptor responseInterceptor);
+
+    abstract void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor);
+
     @Test
     public void testBasicAuthenticationNoCreds() throws Exception {
         server.register("*", AsyncEchoHandler::new);
@@ -271,6 +286,69 @@ public abstract class AbstractHttpAsyncClientAuthentication<T extends CloseableH
     }
 
     @Test
+    public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
+        server.register("*", AsyncEchoHandler::new);
+
+        final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
+        setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        addResponseInterceptor((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
+
+        final HttpHost target = start();
+
+        final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+                .add(target, "test", "test".toCharArray())
+                .build();
+
+        final AuthCache authCache = new BasicAuthCache();
+
+        for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
+            final HttpClientContext context = HttpClientContext.create();
+            context.setAuthCache(authCache);
+            context.setCredentialsProvider(credentialsProvider);
+            final Future<SimpleHttpResponse> future = httpclient.execute(SimpleRequestBuilder.get()
+                    .setHttpHost(target)
+                    .setPath(requestPath)
+                    .build(), context, null);
+            final HttpResponse response = future.get();
+            Assert.assertNotNull(response);
+            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+        }
+
+        // There should be only single auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
+
+        responseQueue.clear();
+        authCache.clear();
+        Mockito.reset(authStrategy);
+
+        for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/"}) {
+            final HttpClientContext context = HttpClientContext.create();
+            context.setCredentialsProvider(credentialsProvider);
+            context.setAuthCache(authCache);
+            final Future<SimpleHttpResponse> future = httpclient.execute(SimpleRequestBuilder.get()
+                    .setHttpHost(target)
+                    .setPath(requestPath)
+                    .build(), context, null);
+            final HttpResponse response = future.get();
+            Assert.assertNotNull(response);
+            Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+        }
+
+        // There should be an auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 401, 200, 401, 200)));
+    }
+
+    @Test
     public void testAuthenticationUserinfoInRequestSuccess() throws Exception {
         server.register("*", AsyncEchoHandler::new);
         final HttpHost target = start();
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
index 52adf5a..94b6766 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestH2ClientAuthentication.java
@@ -38,6 +38,8 @@ import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
 import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
 import org.apache.hc.client5.testing.SSLTestContexts;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
 import org.apache.hc.core5.http.config.Lookup;
@@ -93,6 +95,16 @@ public class TestH2ClientAuthentication extends AbstractHttpAsyncClientAuthentic
     }
 
     @Override
+    void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
+        clientBuilder.addResponseInterceptorLast(responseInterceptor);
+    }
+
+    @Override
+    void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
+        clientBuilder.addRequestInterceptorLast(requestInterceptor);
+    }
+
+    @Override
     protected CloseableHttpAsyncClient createClient() throws Exception {
         return clientBuilder.build();
     }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
index d8ff129..1459237 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestHttp1ClientAuthentication.java
@@ -51,7 +51,9 @@ import org.apache.hc.client5.testing.SSLTestContexts;
 import org.apache.hc.core5.http.HeaderElements;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
 import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.HttpVersion;
 import org.apache.hc.core5.http.URIScheme;
@@ -133,6 +135,16 @@ public class TestHttp1ClientAuthentication extends AbstractHttpAsyncClientAuthen
     }
 
     @Override
+    void addResponseInterceptor(final HttpResponseInterceptor responseInterceptor) {
+        clientBuilder.addResponseInterceptorLast(responseInterceptor);
+    }
+
+    @Override
+    void addRequestInterceptor(final HttpRequestInterceptor requestInterceptor) {
+        clientBuilder.addRequestInterceptorLast(requestInterceptor);
+    }
+
+    @Override
     protected CloseableHttpAsyncClient createClient() throws Exception {
         return clientBuilder.build();
     }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
index 1e88a46..0af41e1 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
@@ -30,8 +30,12 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
@@ -63,6 +67,7 @@ import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.config.Registry;
 import org.apache.hc.core5.http.config.RegistryBuilder;
@@ -74,7 +79,10 @@ import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicHeader;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.support.BasicResponseBuilder;
 import org.apache.hc.core5.net.URIAuthority;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -267,6 +275,9 @@ public class TestClientAuthentication extends LocalServerTestBase {
         this.server.registerHandler("*", new EchoHandler());
         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
         this.clientBuilder.setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        this.clientBuilder.addResponseInterceptorLast((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
 
         final HttpHost target = start();
 
@@ -286,6 +297,69 @@ public class TestClientAuthentication extends LocalServerTestBase {
         }
 
         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200, 200)));
+    }
+
+    @Test
+    public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
+        this.server.registerHandler("*", new EchoHandler());
+        final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
+        this.clientBuilder.setTargetAuthenticationStrategy(authStrategy);
+        final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
+        this.clientBuilder.addResponseInterceptorLast((response, entity, context)
+                -> responseQueue.add(BasicResponseBuilder.copy(response).build()));
+
+        final HttpHost target = start();
+
+        final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+                .add(target, "test", "test".toCharArray())
+                .build();
+
+        final AuthCache authCache = new BasicAuthCache();
+        final HttpClientContext context = HttpClientContext.create();
+        context.setAuthCache(authCache);
+        context.setCredentialsProvider(credentialsProvider);
+
+        for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
+            final HttpGet httpget = new HttpGet(requestPath);
+            try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
+                final HttpEntity entity1 = response.getEntity();
+                Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+                Assert.assertNotNull(entity1);
+                EntityUtils.consume(entity1);
+            }
+        }
+
+        // There should be only single auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
+
+        responseQueue.clear();
+        authCache.clear();
+        Mockito.reset(authStrategy);
+
+        for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/", "/buh/a"}) {
+            final HttpGet httpget = new HttpGet(requestPath);
+            try (final ClassicHttpResponse response = this.httpclient.execute(target, httpget, context)) {
+                final HttpEntity entity1 = response.getEntity();
+                Assert.assertEquals(HttpStatus.SC_OK, response.getCode());
+                Assert.assertNotNull(entity1);
+                EntityUtils.consume(entity1);
+            }
+        }
+
+        // There should be an auth strategy call for all successful message exchanges
+        Mockito.verify(authStrategy, Mockito.times(2)).select(Mockito.any(), Mockito.any(), Mockito.any());
+
+        MatcherAssert.assertThat(
+                responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
+                CoreMatchers.equalTo(Arrays.asList(200, 401, 200, 200, 401, 200)));
     }
 
     @Test
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
index 26ddb42..f57e019 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/SchemePortResolver.java
@@ -29,6 +29,7 @@ package org.apache.hc.client5.http;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.net.NamedEndpoint;
 
 /**
  * Strategy for default port resolution for protocol schemes.
@@ -43,4 +44,13 @@ public interface SchemePortResolver {
      */
     int resolve(HttpHost host);
 
+    /**
+     * Returns the actual port for the host based on the protocol scheme.
+     *
+     * @since 5.2
+     */
+    default int resolve(String scheme, NamedEndpoint endpoint) {
+        return resolve(new HttpHost(scheme, endpoint));
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
index f566b39..e13bde7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthCache.java
@@ -36,12 +36,73 @@ import org.apache.hc.core5.http.HttpHost;
  */
 public interface AuthCache {
 
+    /**
+     * Stores the authentication state with the given authentication scope in the cache.
+     *
+     * @param host the authentication authority.
+     * @param authScheme the cacheable authentication state.
+     */
     void put(HttpHost host, AuthScheme authScheme);
 
+    /**
+     * Returns the authentication state with the given authentication scope from the cache
+     * if available.
+     *
+     * @param host the authentication authority.
+     * @return the authentication state ir {@code null} if not available in the cache.
+     */
     AuthScheme get(HttpHost host);
 
+    /**
+     * Removes the authentication state with the given authentication scope from the cache
+     * if found.
+     *
+     * @param host the authentication authority.
+     */
     void remove(HttpHost host);
 
     void clear();
 
+    /**
+     * Stores the authentication state with the given authentication scope in the cache.
+     *
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     * @param authScheme the cacheable authentication state.
+     *
+     * @since 5.2
+     */
+    default void put(HttpHost host, String pathPrefix, AuthScheme authScheme) {
+        put(host, authScheme);
+    }
+
+    /**
+     * Returns the authentication state with the given authentication scope from the cache
+     * if available.
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     * @return the authentication state ir {@code null} if not available in the cache.
+     *
+     * @since 5.2
+     */
+    default AuthScheme get(HttpHost host, String pathPrefix) {
+        return get(host);
+    }
+
+    /**
+     * Removes the authentication state with the given authentication scope from the cache
+     * if found.
+     *
+     * @param host the authentication authority.
+     * @param pathPrefix the path prefix (the path component up to the last segment separator).
+     *                   Can be {@code null}.
+     *
+     * @since 5.2
+     */
+    default void remove(HttpHost host, String pathPrefix) {
+        remove(host);
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
index 4709149..2aaf1fb 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
@@ -47,6 +47,7 @@ public class AuthExchange {
     private State state;
     private AuthScheme authScheme;
     private Queue<AuthScheme> authOptions;
+    private String pathPrefix;
 
     public AuthExchange() {
         super();
@@ -57,6 +58,7 @@ public class AuthExchange {
         this.state = State.UNCHALLENGED;
         this.authOptions = null;
         this.authScheme = null;
+        this.pathPrefix = null;
     }
 
     public State getState() {
@@ -82,6 +84,20 @@ public class AuthExchange {
     }
 
     /**
+     * @since 5.2
+     */
+    public String getPathPrefix() {
+        return pathPrefix;
+    }
+
+    /**
+     * @since 5.2
+     */
+    public void setPathPrefix(final String pathPrefix) {
+        this.pathPrefix = pathPrefix;
+    }
+
+    /**
      * Resets the auth state with {@link AuthScheme} and clears auth options.
      *
      * @param authScheme auth scheme. May not be null.
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
index ca0fdd4..a25f563 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
@@ -39,8 +39,8 @@ import org.apache.hc.client5.http.AuthenticationStrategy;
 import org.apache.hc.client5.http.auth.AuthChallenge;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.annotation.Contract;
@@ -117,7 +117,7 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy {
                 options.add(authScheme);
             } else {
                 if (LOG.isDebugEnabled()) {
-                    LOG.debug("{}, Challenge for {} authentication scheme not available", exchangeId, schemeName);
+                    LOG.debug("{} Challenge for {} authentication scheme not available", exchangeId, schemeName);
                 }
             }
         }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
index 144ae5f..c65e65d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultSchemePortResolver.java
@@ -31,6 +31,7 @@ import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.net.NamedEndpoint;
 import org.apache.hc.core5.util.Args;
 
 /**
@@ -46,14 +47,19 @@ public class DefaultSchemePortResolver implements SchemePortResolver {
     @Override
     public int resolve(final HttpHost host) {
         Args.notNull(host, "HTTP host");
-        final int port = host.getPort();
+        return resolve(host.getSchemeName(), host);
+    }
+
+    @Override
+    public int resolve(final String scheme, final NamedEndpoint endpoint) {
+        Args.notNull(endpoint, "Endpoint");
+        final int port = endpoint.getPort();
         if (port > 0) {
             return port;
         }
-        final String name = host.getSchemeName();
-        if (URIScheme.HTTP.same(name)) {
+        if (URIScheme.HTTP.same(scheme)) {
             return 80;
-        } else if (URIScheme.HTTPS.same(name)) {
+        } else if (URIScheme.HTTPS.same(scheme)) {
             return 443;
         } else {
             return -1;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
similarity index 51%
rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java
rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
index 70f820c..50e879f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ProtocolSupport.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java
@@ -26,45 +26,48 @@
  */
 package org.apache.hc.client5.http.impl;
 
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
 import org.apache.hc.core5.annotation.Internal;
 import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.URIScheme;
-import org.apache.hc.core5.net.URIAuthority;
+import org.apache.hc.core5.net.PercentCodec;
+import org.apache.hc.core5.net.URIBuilder;
 
 /**
- * Protocol support methods.
+ * Protocol support methods. For internal use only.
  *
- * @since 5.1
+ * @since 5.2
  */
 @Internal
-public final class ProtocolSupport {
+public final class RequestSupport {
 
-    public static String getRequestUri(final HttpRequest request) {
-        final URIAuthority authority = request.getAuthority();
-        if (authority != null) {
-            final StringBuilder buf = new StringBuilder();
-            final String scheme = request.getScheme();
-            buf.append(scheme != null ? scheme : URIScheme.HTTP.id);
-            buf.append("://");
-            if (authority.getUserInfo() != null) {
-                buf.append(authority.getUserInfo());
-                buf.append("@");
-            }
-            buf.append(authority.getHostName());
-            if (authority.getPort() != -1) {
-                buf.append(":");
-                buf.append(authority.getPort());
-            }
-            final String path = request.getPath();
-            if (path == null || !path.startsWith("/")) {
-                buf.append("/");
+    public static String extractPathPrefix(final HttpRequest request) {
+        final String path = request.getPath();
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(path);
+            uriBuilder.setFragment(null);
+            uriBuilder.clearParameters();
+            uriBuilder.normalizeSyntax();
+            final List<String> pathSegments = uriBuilder.getPathSegments();
+
+            if (!pathSegments.isEmpty()) {
+                pathSegments.remove(pathSegments.size() - 1);
             }
-            if (path != null) {
-                buf.append(path);
+            if (pathSegments.isEmpty()) {
+                return "/";
+            } else {
+                final StringBuilder buf = new StringBuilder();
+                buf.append('/');
+                for (final String pathSegment : pathSegments) {
+                    PercentCodec.encode(buf, pathSegment, StandardCharsets.US_ASCII);
+                    buf.append('/');
+                }
+                return buf.toString();
             }
-            return buf.toString();
-        } else {
-            return request.getPath();
+        } catch (final URISyntaxException ex) {
+            return path;
         }
     }
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
index 4f83962..17d75c6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
@@ -379,7 +379,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
         final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
 
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext);
+            authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
         }
 
         final HttpRequest connect = new BasicHttpRequest(Method.CONNECT, nextHop, nextHop.toHostString());
@@ -444,9 +444,9 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -455,7 +455,7 @@ public final class AsyncConnectExec implements AsyncExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
index 4fe3637..7e28534 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
@@ -43,6 +43,7 @@ import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.CredentialsStore;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.AuthSupport;
+import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -148,23 +149,36 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
         }
 
         final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
+        final String pathPrefix = RequestSupport.extractPathPrefix(request);
         final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target);
         final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange();
 
+        if (!targetAuthExchange.isConnectionBased() &&
+                targetAuthExchange.getPathPrefix() != null &&
+                !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
+            // force re-authentication if the current path prefix does not match
+            // that of the previous authentication exchange.
+            targetAuthExchange.reset();
+        }
+        if (targetAuthExchange.getPathPrefix() == null) {
+            targetAuthExchange.setPathPrefix(pathPrefix);
+        }
+
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(target, targetAuthExchange, clientContext);
+            authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext);
             if (proxy != null) {
-                authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext);
+                authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext);
             }
         }
 
         final AtomicBoolean challenged = new AtomicBoolean(false);
-        internalExecute(target, targetAuthExchange, proxyAuthExchange,
+        internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
                 challenged, request, entityProducer, scope, chain, asyncExecCallback);
     }
 
     private void internalExecute(
             final HttpHost target,
+            final String pathPrefix,
             final AuthExchange targetAuthExchange,
             final AuthExchange proxyAuthExchange,
             final AtomicBoolean challenged,
@@ -216,6 +230,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         proxyAuthExchange,
                         proxy != null ? proxy : target,
                         target,
+                        pathPrefix,
                         response,
                         clientContext)) {
                     challenged.set(true);
@@ -267,7 +282,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                             if (entityProducer != null) {
                                 entityProducer.releaseResources();
                             }
-                            internalExecute(target, targetAuthExchange, proxyAuthExchange,
+                            internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange,
                                     challenged, request, entityProducer, scope, chain, asyncExecCallback);
                         } catch (final HttpException | IOException ex) {
                             asyncExecCallback.failed(ex);
@@ -298,6 +313,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
             final AuthExchange proxyAuthExchange,
             final HttpHost proxy,
             final HttpHost target,
+            final String pathPrefix,
             final HttpResponse response,
             final HttpClientContext context) {
         final RequestConfig config = context.getRequestConfig();
@@ -307,9 +323,9 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (targetAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
                 }
             }
 
@@ -318,9 +334,9 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -329,7 +345,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         targetAuthStrategy, targetAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
                 }
 
                 return updated;
@@ -339,7 +355,7 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
index 01f959a..10ce0b6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java
@@ -59,32 +59,39 @@ public final class AuthCacheKeeper {
     }
 
     public void updateOnChallenge(final HttpHost host,
+                                  final String pathPrefix,
                                   final AuthExchange authExchange,
                                   final HttpContext context) {
-        clearCache(host, HttpClientContext.adapt(context));
+        clearCache(host, pathPrefix, HttpClientContext.adapt(context));
     }
 
     public void updateOnNoChallenge(final HttpHost host,
+                                    final String pathPrefix,
                                     final AuthExchange authExchange,
                                     final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.SUCCESS) {
-            updateCache(host, authExchange.getAuthScheme(), HttpClientContext.adapt(context));
+            updateCache(host, pathPrefix, authExchange.getAuthScheme(), HttpClientContext.adapt(context));
         }
     }
 
     public void updateOnResponse(final HttpHost host,
+                                 final String pathPrefix,
                                  final AuthExchange authExchange,
                                  final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.FAILURE) {
-            clearCache(host, HttpClientContext.adapt(context));
+            clearCache(host, pathPrefix, HttpClientContext.adapt(context));
         }
     }
 
     public void loadPreemptively(final HttpHost host,
+                                 final String pathPrefix,
                                  final AuthExchange authExchange,
                                  final HttpContext context) {
         if (authExchange.getState() == AuthExchange.State.UNCHALLENGED) {
-            final AuthScheme authScheme = loadFromCache(host, HttpClientContext.adapt(context));
+            AuthScheme authScheme = loadFromCache(host, pathPrefix, HttpClientContext.adapt(context));
+            if (authScheme == null && pathPrefix != null) {
+                authScheme = loadFromCache(host, null, HttpClientContext.adapt(context));
+            }
             if (authScheme != null) {
                 authExchange.select(authScheme);
             }
@@ -92,14 +99,16 @@ public final class AuthCacheKeeper {
     }
 
     private AuthScheme loadFromCache(final HttpHost host,
+                                     final String pathPrefix,
                                      final HttpClientContext clientContext) {
         final AuthCache authCache = clientContext.getAuthCache();
         if (authCache != null) {
-            final AuthScheme authScheme = authCache.get(host);
+            final AuthScheme authScheme = authCache.get(host, pathPrefix);
             if (authScheme != null) {
                 if (LOG.isDebugEnabled()) {
                     final String exchangeId = clientContext.getExchangeId();
-                    LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), host);
+                    LOG.debug("{} Re-using cached '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host,
+                            pathPrefix != null ? pathPrefix : "");
                 }
                 return authScheme;
             }
@@ -108,6 +117,7 @@ public final class AuthCacheKeeper {
     }
 
     private void updateCache(final HttpHost host,
+                             final String pathPrefix,
                              final AuthScheme authScheme,
                              final HttpClientContext clientContext) {
         final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null;
@@ -119,21 +129,24 @@ public final class AuthCacheKeeper {
             }
             if (LOG.isDebugEnabled()) {
                 final String exchangeId = clientContext.getExchangeId();
-                LOG.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host);
+                LOG.debug("{} Caching '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host,
+                        pathPrefix != null ? pathPrefix : "");
             }
-            authCache.put(host, authScheme);
+            authCache.put(host, pathPrefix, authScheme);
         }
     }
 
     private void clearCache(final HttpHost host,
+                            final String pathPrefix,
                             final HttpClientContext clientContext) {
         final AuthCache authCache = clientContext.getAuthCache();
         if (authCache != null) {
             if (LOG.isDebugEnabled()) {
                 final String exchangeId = clientContext.getExchangeId();
-                LOG.debug("{} Clearing cached auth scheme for {}", exchangeId, host);
+                LOG.debug("{} Clearing cached auth scheme for {}{}", exchangeId, host,
+                        pathPrefix != null ? pathPrefix : "");
             }
-            authCache.remove(host);
+            authCache.remove(host, pathPrefix);
         }
     }
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
index c22c57b..c0c92c7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BasicAuthCache.java
@@ -32,6 +32,7 @@ import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -39,11 +40,12 @@ import org.apache.hc.client5.http.SchemePortResolver;
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
-import org.apache.hc.client5.http.routing.RoutingSupport;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.net.NamedEndpoint;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.LangUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,7 +64,65 @@ public class BasicAuthCache implements AuthCache {
 
     private static final Logger LOG = LoggerFactory.getLogger(BasicAuthCache.class);
 
-    private final Map<HttpHost, byte[]> map;
+    static class Key {
+
+        final String scheme;
+        final String host;
+        final int port;
+        final String pathPrefix;
+
+        Key(final String scheme, final String host, final int port, final String pathPrefix) {
+            Args.notBlank(scheme, "Scheme");
+            Args.notBlank(host, "Scheme");
+            this.scheme = scheme.toLowerCase(Locale.ROOT);
+            this.host = host.toLowerCase(Locale.ROOT);
+            this.port = port;
+            this.pathPrefix = pathPrefix;
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof Key) {
+                final Key that = (Key) obj;
+                return this.scheme.equals(that.scheme) &&
+                        this.host.equals(that.host) &&
+                        this.port == that.port &&
+                        LangUtils.equals(this.pathPrefix, that.pathPrefix);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = LangUtils.HASH_SEED;
+            hash = LangUtils.hashCode(hash, this.scheme);
+            hash = LangUtils.hashCode(hash, this.host);
+            hash = LangUtils.hashCode(hash, this.port);
+            hash = LangUtils.hashCode(hash, this.pathPrefix);
+            return hash;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder buf = new StringBuilder();
+            buf.append(scheme).append("://").append(host);
+            if (port >= 0) {
+                buf.append(":").append(port);
+            }
+            if (pathPrefix != null) {
+                if (!pathPrefix.startsWith("/")) {
+                    buf.append("/");
+                }
+                buf.append(pathPrefix);
+            }
+            return buf.toString();
+        }
+    }
+
+    private final Map<Key, byte[]> map;
     private final SchemePortResolver schemePortResolver;
 
     /**
@@ -80,8 +140,27 @@ public class BasicAuthCache implements AuthCache {
         this(null);
     }
 
+    private Key key(final String scheme, final NamedEndpoint authority, final String pathPrefix) {
+        return new Key(scheme, authority.getHostName(), schemePortResolver.resolve(scheme, authority), pathPrefix);
+    }
+
     @Override
     public void put(final HttpHost host, final AuthScheme authScheme) {
+        put(host, null, authScheme);
+    }
+
+    @Override
+    public AuthScheme get(final HttpHost host) {
+        return get(host, null);
+    }
+
+    @Override
+    public void remove(final HttpHost host) {
+        remove(host, null);
+    }
+
+    @Override
+    public void put(final HttpHost host, final String pathPrefix, final AuthScheme authScheme) {
         Args.notNull(host, "HTTP host");
         if (authScheme == null) {
             return;
@@ -92,8 +171,7 @@ public class BasicAuthCache implements AuthCache {
                 try (final ObjectOutputStream out = new ObjectOutputStream(buf)) {
                     out.writeObject(authScheme);
                 }
-                final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-                this.map.put(key, buf.toByteArray());
+                this.map.put(key(host.getSchemeName(), host, pathPrefix), buf.toByteArray());
             } catch (final IOException ex) {
                 if (LOG.isWarnEnabled()) {
                     LOG.warn("Unexpected I/O error while serializing auth scheme", ex);
@@ -107,10 +185,9 @@ public class BasicAuthCache implements AuthCache {
     }
 
     @Override
-    public AuthScheme get(final HttpHost host) {
+    public AuthScheme get(final HttpHost host, final String pathPrefix) {
         Args.notNull(host, "HTTP host");
-        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-        final byte[] bytes = this.map.get(key);
+        final byte[] bytes = this.map.get(key(host.getSchemeName(), host, pathPrefix));
         if (bytes != null) {
             try {
                 final ByteArrayInputStream buf = new ByteArrayInputStream(bytes);
@@ -131,10 +208,9 @@ public class BasicAuthCache implements AuthCache {
     }
 
     @Override
-    public void remove(final HttpHost host) {
+    public void remove(final HttpHost host, final String pathPrefix) {
         Args.notNull(host, "HTTP host");
-        final HttpHost key = RoutingSupport.normalize(host, schemePortResolver);
-        this.map.remove(key);
+        this.map.remove(key(host.getSchemeName(), host, pathPrefix));
     }
 
     @Override
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
index 5b80e21..45c0c5e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
@@ -215,7 +215,7 @@ public final class ConnectExec implements ExecChainHandler {
         final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
 
         if (authCacheKeeper != null) {
-            authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context);
+            authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
         }
 
         ClassicHttpResponse response = null;
@@ -254,9 +254,9 @@ public final class ConnectExec implements ExecChainHandler {
 
                 if (authCacheKeeper != null) {
                     if (proxyAuthRequested) {
-                        authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                     } else {
-                        authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                     }
                 }
 
@@ -265,7 +265,7 @@ public final class ConnectExec implements ExecChainHandler {
                             proxyAuthStrategy, proxyAuthExchange, context);
 
                     if (authCacheKeeper != null) {
-                        authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                        authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                     }
                     if (updated) {
                         // Retry request
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index 556f7a6..95f9545 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
@@ -42,6 +42,7 @@ import org.apache.hc.client5.http.classic.ExecChainHandler;
 import org.apache.hc.client5.http.classic.ExecRuntime;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.AuthSupport;
+import org.apache.hc.client5.http.impl.RequestSupport;
 import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
 import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -151,14 +152,26 @@ public final class ProtocolExec implements ExecChainHandler {
             }
 
             final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority());
+            final String pathPrefix = RequestSupport.extractPathPrefix(request);
 
             final AuthExchange targetAuthExchange = context.getAuthExchange(target);
             final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
 
+            if (!targetAuthExchange.isConnectionBased() &&
+                    targetAuthExchange.getPathPrefix() != null &&
+                    !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) {
+                // force re-authentication if the current path prefix does not match
+                // that of the previous authentication exchange.
+                targetAuthExchange.reset();
+            }
+            if (targetAuthExchange.getPathPrefix() == null) {
+                targetAuthExchange.setPathPrefix(pathPrefix);
+            }
+
             if (authCacheKeeper != null) {
-                authCacheKeeper.loadPreemptively(target, targetAuthExchange, context);
+                authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, context);
                 if (proxy != null) {
-                    authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -206,6 +219,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         proxyAuthExchange,
                         proxy != null ? proxy : target,
                         target,
+                        pathPrefix,
                         response,
                         context)) {
                     // Make sure the response body is fully consumed, if present
@@ -259,6 +273,7 @@ public final class ProtocolExec implements ExecChainHandler {
             final AuthExchange proxyAuthExchange,
             final HttpHost proxy,
             final HttpHost target,
+            final String pathPrefix,
             final HttpResponse response,
             final HttpClientContext context) {
         final RequestConfig config = context.getRequestConfig();
@@ -268,9 +283,9 @@ public final class ProtocolExec implements ExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (targetAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context);
                 }
             }
 
@@ -279,9 +294,9 @@ public final class ProtocolExec implements ExecChainHandler {
 
             if (authCacheKeeper != null) {
                 if (proxyAuthRequested) {
-                    authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context);
                 } else {
-                    authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context);
                 }
             }
 
@@ -290,7 +305,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         targetAuthStrategy, targetAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(target, targetAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
                 }
 
                 return updated;
@@ -300,7 +315,7 @@ public final class ProtocolExec implements ExecChainHandler {
                         proxyAuthStrategy, proxyAuthExchange, context);
 
                 if (authCacheKeeper != null) {
-                    authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context);
+                    authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
                 }
 
                 return updated;
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java
deleted file mode 100644
index 0af8432..0000000
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestProtocolSupport.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl;
-
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.Method;
-import org.apache.hc.core5.http.message.BasicHttpRequest;
-import org.apache.hc.core5.net.URIAuthority;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.MatcherAssert;
-import org.junit.Test;
-
-/**
- * Simple tests for {@link ProtocolSupport}.
- */
-public class TestProtocolSupport {
-
-    @Test
-    public void testGetRequestUri() {
-        final HttpRequest request = new BasicHttpRequest(Method.GET, "");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("/"));
-        request.setAuthority(new URIAuthority("testUser", "localhost", 8080));
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("http://testUser@localhost:8080/"));
-        request.setScheme("https");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/"));
-        request.setPath("blah");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah"));
-        request.setPath("/blah/blah");
-        MatcherAssert.assertThat(ProtocolSupport.getRequestUri(request), CoreMatchers.equalTo("https://testUser@localhost:8080/blah/blah"));
-    }
-}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java
new file mode 100644
index 0000000..35e787b
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java
@@ -0,0 +1,53 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl;
+
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Simple tests for {@link RequestSupport}.
+ */
+public class TestRequestSupport {
+
+    @Test
+    public void testPathPrefixExtraction() {
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/../aaaa/")));
+        Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/cccc")));
+        Assert.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/")));
+        Assert.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb?////")));
+        Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aa%2faa/bbbb")));
+        Assert.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/a%61%2fa%61/bbbb")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa")));
+        Assert.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "")));
+    }
+
+}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
index b57fa6d..3486065 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBasicAuthCache.java
@@ -67,7 +67,7 @@ public class TestBasicAuthCache {
     }
 
     @Test
-    public void testStoreNonserializable() throws Exception {
+    public void testStoreNonSerializable() throws Exception {
         final BasicAuthCache cache = new BasicAuthCache();
         final AuthScheme authScheme = new NTLMScheme();
         cache.put(new HttpHost("localhost", 80), authScheme);
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java
deleted file mode 100644
index d6959c1..0000000
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestRequestAuthCache.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.auth;
-
-import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.auth.AuthCache;
-import org.apache.hc.client5.http.auth.AuthExchange;
-import org.apache.hc.client5.http.auth.AuthScope;
-import org.apache.hc.client5.http.auth.Credentials;
-import org.apache.hc.client5.http.auth.CredentialsProvider;
-import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
-import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.protocol.RequestAuthCache;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.HttpRequestInterceptor;
-import org.apache.hc.core5.http.message.BasicHttpRequest;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class TestRequestAuthCache {
-
-    private HttpHost target;
-    private HttpHost proxy;
-    private Credentials creds1;
-    private Credentials creds2;
-    private AuthScope authscope1;
-    private AuthScope authscope2;
-    private BasicScheme authscheme1;
-    private BasicScheme authscheme2;
-    private CredentialsProvider credProvider;
-
-    @Before
-    public void setUp() {
-        this.target = new HttpHost("localhost", 80);
-        this.proxy = new HttpHost("localhost", 8080);
-
-        this.creds1 = new UsernamePasswordCredentials("user1", "secret1".toCharArray());
-        this.creds2 = new UsernamePasswordCredentials("user2", "secret2".toCharArray());
-        this.authscope1 = new AuthScope(this.target);
-        this.authscope2 = new AuthScope(this.proxy);
-        this.authscheme1 = new BasicScheme();
-        this.authscheme2 = new BasicScheme();
-
-        this.credProvider = CredentialsProviderBuilder.create()
-                .add(this.authscope1, this.creds1)
-                .add(this.authscope2, this.creds2)
-                .build();
-    }
-
-    @Test
-    public void testRequestParameterCheck() throws Exception {
-        final HttpClientContext context = HttpClientContext.create();
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        Assert.assertThrows(NullPointerException.class, () ->
-                interceptor.process(null, null, context));
-    }
-
-    @Test
-    public void testContextParameterCheck() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        Assert.assertThrows(NullPointerException.class, () ->
-                interceptor.process(request, null, null));
-    }
-
-    @Test
-    public void testPreemptiveTargetAndProxyAuth() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        authCache.put(this.target, this.authscheme1);
-        authCache.put(this.proxy, this.authscheme2);
-
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNotNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNotNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testCredentialsProviderNotSet() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, null);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        authCache.put(this.target, this.authscheme1);
-        authCache.put(this.proxy, this.authscheme2);
-
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testAuthCacheNotSet() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-        context.setAttribute(HttpClientContext.AUTH_CACHE, null);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-    @Test
-    public void testAuthCacheEmpty() throws Exception {
-        final HttpRequest request = new BasicHttpRequest("GET", "/");
-
-        final HttpClientContext context = HttpClientContext.create();
-        context.setAttribute(HttpClientContext.CREDS_PROVIDER, this.credProvider);
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, new HttpRoute(this.target, null, this.proxy, false));
-
-        final AuthCache authCache = new BasicAuthCache();
-        context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
-
-        final HttpRequestInterceptor interceptor = new RequestAuthCache();
-        interceptor.process(request, null, context);
-
-        final AuthExchange targetAuthExchange = context.getAuthExchange(this.target);
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(this.proxy);
-
-        Assert.assertNotNull(targetAuthExchange);
-        Assert.assertNull(targetAuthExchange.getAuthScheme());
-        Assert.assertNotNull(proxyAuthExchange);
-        Assert.assertNull(proxyAuthExchange.getAuthScheme());
-    }
-
-}