View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.testing.async;
28  
29  import static org.hamcrest.MatcherAssert.assertThat;
30  
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.Queue;
34  import java.util.concurrent.ConcurrentLinkedQueue;
35  import java.util.concurrent.ExecutionException;
36  import java.util.concurrent.Future;
37  import java.util.concurrent.atomic.AtomicLong;
38  import java.util.function.Consumer;
39  import java.util.stream.Collectors;
40  
41  import org.apache.hc.client5.http.AuthenticationStrategy;
42  import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
43  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
44  import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
45  import org.apache.hc.client5.http.auth.AuthCache;
46  import org.apache.hc.client5.http.auth.AuthSchemeFactory;
47  import org.apache.hc.client5.http.auth.AuthScope;
48  import org.apache.hc.client5.http.auth.CredentialsProvider;
49  import org.apache.hc.client5.http.auth.StandardAuthScheme;
50  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
51  import org.apache.hc.client5.http.config.RequestConfig;
52  import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
53  import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
54  import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
55  import org.apache.hc.client5.http.impl.auth.BasicScheme;
56  import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
57  import org.apache.hc.client5.http.protocol.HttpClientContext;
58  import org.apache.hc.client5.testing.BasicTestAuthenticator;
59  import org.apache.hc.client5.testing.auth.Authenticator;
60  import org.apache.hc.core5.function.Decorator;
61  import org.apache.hc.core5.http.ContentType;
62  import org.apache.hc.core5.http.HttpHeaders;
63  import org.apache.hc.core5.http.HttpHost;
64  import org.apache.hc.core5.http.HttpRequestInterceptor;
65  import org.apache.hc.core5.http.HttpResponse;
66  import org.apache.hc.core5.http.HttpResponseInterceptor;
67  import org.apache.hc.core5.http.HttpStatus;
68  import org.apache.hc.core5.http.ProtocolException;
69  import org.apache.hc.core5.http.URIScheme;
70  import org.apache.hc.core5.http.config.Lookup;
71  import org.apache.hc.core5.http.config.Registry;
72  import org.apache.hc.core5.http.config.RegistryBuilder;
73  import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
74  import org.apache.hc.core5.http.support.BasicResponseBuilder;
75  import org.apache.hc.core5.net.URIAuthority;
76  import org.apache.hc.core5.testing.nio.H2TestServer;
77  import org.hamcrest.CoreMatchers;
78  import org.junit.jupiter.api.Assertions;
79  import org.junit.jupiter.api.Test;
80  import org.mockito.Mockito;
81  
82  public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends CloseableHttpAsyncClient> extends AbstractIntegrationTestBase {
83  
84      public AbstractHttpAsyncClientAuthenticationTest(final URIScheme scheme) {
85          super(scheme);
86      }
87  
88      abstract protected H2TestServer startServer(final Decorator<AsyncServerExchangeHandler> exchangeHandlerDecorator) throws Exception;
89  
90      protected H2TestServer startServer() throws Exception {
91          return startServer(requestHandler -> new AuthenticatingAsyncDecorator(requestHandler, new BasicTestAuthenticator("test:test", "test realm")));
92      }
93  
94      interface TestClientBuilder {
95  
96          TestClientBuilder setDefaultAuthSchemeRegistry(Lookup<AuthSchemeFactory> authSchemeRegistry);
97  
98          TestClientBuilder setTargetAuthenticationStrategy(AuthenticationStrategy targetAuthStrategy);
99  
100         TestClientBuilder addResponseInterceptor(HttpResponseInterceptor responseInterceptor);
101 
102         TestClientBuilder addRequestInterceptor(HttpRequestInterceptor requestInterceptor);
103 
104     }
105 
106     abstract protected T startClientCustom(final Consumer<TestClientBuilder> clientCustomizer) throws Exception;
107 
108     T startClient() throws Exception {
109         return startClientCustom(c -> {});
110     }
111 
112     @Test
113     public void testBasicAuthenticationNoCreds() throws Exception {
114         final H2TestServer server = startServer();
115         server.register("*", AsyncEchoHandler::new);
116         final HttpHost target = targetHost();
117 
118         final T client = startClient();
119 
120         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
121         final HttpClientContext context = HttpClientContext.create();
122         context.setCredentialsProvider(credsProvider);
123 
124         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
125                         .setHttpHost(target)
126                         .setPath("/")
127                         .build(), context, null);
128         final HttpResponse response = future.get();
129 
130         Assertions.assertNotNull(response);
131         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
132         Mockito.verify(credsProvider).getCredentials(
133                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
134     }
135 
136     @Test
137     public void testBasicAuthenticationFailure() throws Exception {
138         final H2TestServer server = startServer();
139         server.register("*", AsyncEchoHandler::new);
140         final HttpHost target = targetHost();
141 
142         final T client = startClient();
143 
144         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
145         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
146                 .thenReturn(new UsernamePasswordCredentials("test", "all-wrong".toCharArray()));
147         final HttpClientContext context = HttpClientContext.create();
148         context.setCredentialsProvider(credsProvider);
149 
150         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
151                         .setHttpHost(target)
152                         .setPath("/")
153                         .build(), context, null);
154         final HttpResponse response = future.get();
155 
156         Assertions.assertNotNull(response);
157         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
158         Mockito.verify(credsProvider).getCredentials(
159                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
160     }
161 
162     @Test
163     public void testBasicAuthenticationSuccess() throws Exception {
164         final H2TestServer server = startServer();
165         server.register("*", AsyncEchoHandler::new);
166         final HttpHost target = targetHost();
167 
168         final T client = startClient();
169 
170         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
171         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
172                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
173         final HttpClientContext context = HttpClientContext.create();
174         context.setCredentialsProvider(credsProvider);
175 
176         final Future<SimpleHttpResponse> future = client.execute(
177                 SimpleRequestBuilder.get()
178                         .setHttpHost(target)
179                         .setPath("/")
180                         .build(), context, null);
181         final HttpResponse response = future.get();
182 
183         Assertions.assertNotNull(response);
184         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
185         Mockito.verify(credsProvider).getCredentials(
186                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
187     }
188 
189     @Test
190     public void testBasicAuthenticationWithEntitySuccess() throws Exception {
191         final H2TestServer server = startServer();
192         server.register("*", AsyncEchoHandler::new);
193         final HttpHost target = targetHost();
194 
195         final T client = startClient();
196 
197         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
198         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
199                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
200         final HttpClientContext context = HttpClientContext.create();
201         context.setCredentialsProvider(credsProvider);
202         final Future<SimpleHttpResponse> future = client.execute(
203                 SimpleRequestBuilder.put()
204                         .setHttpHost(target)
205                         .setPath("/")
206                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
207                         .build(), context, null);
208         final HttpResponse response = future.get();
209 
210         Assertions.assertNotNull(response);
211         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
212         Mockito.verify(credsProvider).getCredentials(
213                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
214     }
215 
216     @Test
217     public void testBasicAuthenticationExpectationFailure() throws Exception {
218         final H2TestServer server = startServer();
219         server.register("*", AsyncEchoHandler::new);
220         final HttpHost target = targetHost();
221 
222         final T client = startClient();
223 
224         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
225         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
226                 .thenReturn(new UsernamePasswordCredentials("test", "all-wrong".toCharArray()));
227         final HttpClientContext context = HttpClientContext.create();
228         context.setCredentialsProvider(credsProvider);
229         context.setRequestConfig(RequestConfig.custom().setExpectContinueEnabled(true).build());
230         final Future<SimpleHttpResponse> future = client.execute(
231                 SimpleRequestBuilder.put()
232                         .setHttpHost(target)
233                         .setPath("/")
234                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
235                         .build(), context, null);
236         final HttpResponse response = future.get();
237 
238         Assertions.assertNotNull(response);
239         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
240     }
241 
242     @Test
243     public void testBasicAuthenticationExpectationSuccess() throws Exception {
244         final H2TestServer server = startServer();
245         server.register("*", AsyncEchoHandler::new);
246         final HttpHost target = targetHost();
247 
248         final T client = startClient();
249 
250         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
251         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
252                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
253         final HttpClientContext context = HttpClientContext.create();
254         context.setCredentialsProvider(credsProvider);
255         context.setRequestConfig(RequestConfig.custom().setExpectContinueEnabled(true).build());
256         final Future<SimpleHttpResponse> future = client.execute(
257                 SimpleRequestBuilder.put()
258                         .setHttpHost(target)
259                         .setPath("/")
260                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
261                         .build(), context, null);
262         final HttpResponse response = future.get();
263 
264         Assertions.assertNotNull(response);
265         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
266         Mockito.verify(credsProvider).getCredentials(
267                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
268     }
269 
270     @Test
271     public void testBasicAuthenticationCredentialsCaching() throws Exception {
272         final H2TestServer server = startServer();
273         server.register("*", AsyncEchoHandler::new);
274         final HttpHost target = targetHost();
275 
276         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
277         final T client = startClientCustom(builder ->  builder.setTargetAuthenticationStrategy(authStrategy));
278 
279         final HttpClientContext context = HttpClientContext.create();
280         context.setCredentialsProvider(CredentialsProviderBuilder.create()
281                 .add(target, "test", "test".toCharArray())
282                 .build());
283 
284         for (int i = 0; i < 5; i++) {
285             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
286                     .setHttpHost(target)
287                     .setPath("/")
288                     .build(), context, null);
289             final HttpResponse response = future.get();
290             Assertions.assertNotNull(response);
291             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
292         }
293 
294         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
295     }
296 
297     @Test
298     public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
299         final H2TestServer server = startServer();
300         server.register("*", AsyncEchoHandler::new);
301         final HttpHost target = targetHost();
302 
303         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
304         final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
305 
306         final T client = startClientCustom(builder -> builder
307                 .setTargetAuthenticationStrategy(authStrategy)
308                 .addResponseInterceptor((response, entity, context)
309                         -> responseQueue.add(BasicResponseBuilder.copy(response).build())));
310 
311         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
312                 .add(target, "test", "test".toCharArray())
313                 .build();
314 
315         final AuthCache authCache = new BasicAuthCache();
316 
317         for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
318             final HttpClientContext context = HttpClientContext.create();
319             context.setAuthCache(authCache);
320             context.setCredentialsProvider(credentialsProvider);
321             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
322                     .setHttpHost(target)
323                     .setPath(requestPath)
324                     .build(), context, null);
325             final HttpResponse response = future.get();
326             Assertions.assertNotNull(response);
327             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
328         }
329 
330         // There should be only single auth strategy call for all successful message exchanges
331         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
332 
333         assertThat(
334                 responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
335                 CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
336 
337         responseQueue.clear();
338         authCache.clear();
339         Mockito.reset(authStrategy);
340 
341         for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/"}) {
342             final HttpClientContext context = HttpClientContext.create();
343             context.setCredentialsProvider(credentialsProvider);
344             context.setAuthCache(authCache);
345             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
346                     .setHttpHost(target)
347                     .setPath(requestPath)
348                     .build(), context, null);
349             final HttpResponse response = future.get();
350             Assertions.assertNotNull(response);
351             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
352         }
353 
354         // There should be an auth strategy call for all successful message exchanges
355         Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any());
356 
357         assertThat(
358                 responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
359                 CoreMatchers.equalTo(Arrays.asList(401, 200, 401, 200, 401, 200)));
360     }
361 
362     @Test
363     public void testAuthenticationUserinfoInRequestFailure() throws Exception {
364         final H2TestServer server = startServer();
365         server.register("*", AsyncEchoHandler::new);
366         final HttpHost target = targetHost();
367 
368         final T client = startClient();
369 
370         final HttpClientContext context = HttpClientContext.create();
371         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
372                         .setScheme(target.getSchemeName())
373                         .setAuthority(new URIAuthority("test:test", target.getHostName(), target.getPort()))
374                         .setPath("/")
375                         .build(), context, null);
376         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> future.get());
377         assertThat(exception.getCause(), CoreMatchers.instanceOf(ProtocolException.class));
378     }
379 
380     @Test
381     public void testReauthentication() throws Exception {
382         final Authenticator authenticator = new BasicTestAuthenticator("test:test", "test realm") {
383 
384             private final AtomicLong count = new AtomicLong(0);
385 
386             @Override
387             public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
388                 final boolean authenticated = super.authenticate(authority, requestUri, credentials);
389                 if (authenticated) {
390                     return this.count.incrementAndGet() % 4 != 0;
391                 }
392                 return false;
393             }
394         };
395 
396         final H2TestServer server = startServer(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, authenticator) {
397 
398             @Override
399             protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
400                 unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
401                 unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
402             }
403 
404         });
405         server.register("*", AsyncEchoHandler::new);
406         final HttpHost target = targetHost();
407 
408         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
409         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
410                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
411 
412         final Registry<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
413                 .register("MyBasic", context -> new BasicScheme() {
414 
415                     private static final long serialVersionUID = 1L;
416 
417                     @Override
418                     public String getName() {
419                         return "MyBasic";
420                     }
421 
422                 })
423                 .build();
424 
425         final T client = startClientCustom(builder -> builder.setDefaultAuthSchemeRegistry(authSchemeRegistry));
426 
427         final RequestConfig config = RequestConfig.custom()
428                 .setTargetPreferredAuthSchemes(Collections.singletonList("MyBasic"))
429                 .build();
430         final HttpClientContext context = HttpClientContext.create();
431         context.setCredentialsProvider(credsProvider);
432 
433         for (int i = 0; i < 10; i++) {
434             final SimpleHttpRequest request = SimpleRequestBuilder.get()
435                         .setHttpHost(target)
436                         .setPath("/")
437                         .build();
438             request.setConfig(config);
439             final Future<SimpleHttpResponse> future = client.execute(request, context, null);
440             final SimpleHttpResponse response = future.get();
441             Assertions.assertNotNull(response);
442             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
443         }
444     }
445 
446     @Test
447     public void testAuthenticationFallback() throws Exception {
448         final H2TestServer server = startServer(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
449 
450             @Override
451             protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
452                 unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
453             }
454 
455         });
456         server.register("*", AsyncEchoHandler::new);
457         final HttpHost target = targetHost();
458 
459         final T client = startClient();
460 
461         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
462         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
463                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
464         final HttpClientContext context = HttpClientContext.create();
465         context.setCredentialsProvider(credsProvider);
466 
467         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
468                         .setHttpHost(target)
469                         .setPath("/")
470                         .build(), context, null);
471         final SimpleHttpResponse response = future.get();
472         Assertions.assertNotNull(response);
473         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
474         Mockito.verify(credsProvider).getCredentials(
475                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
476     }
477 
478 }