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.security.SecureRandom;
32  import java.util.Arrays;
33  import java.util.Collections;
34  import java.util.Queue;
35  import java.util.concurrent.ConcurrentLinkedQueue;
36  import java.util.concurrent.ExecutionException;
37  import java.util.concurrent.Future;
38  import java.util.concurrent.atomic.AtomicLong;
39  import java.util.stream.Collectors;
40  
41  import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
42  import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
43  import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
44  import org.apache.hc.client5.http.auth.AuthCache;
45  import org.apache.hc.client5.http.auth.AuthSchemeFactory;
46  import org.apache.hc.client5.http.auth.AuthScope;
47  import org.apache.hc.client5.http.auth.BearerToken;
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.auth.BasicAuthCache;
54  import org.apache.hc.client5.http.impl.auth.BasicScheme;
55  import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
56  import org.apache.hc.client5.http.protocol.HttpClientContext;
57  import org.apache.hc.client5.testing.BasicTestAuthenticator;
58  import org.apache.hc.client5.testing.auth.Authenticator;
59  import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
60  import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel;
61  import org.apache.hc.client5.testing.extension.async.ServerProtocolLevel;
62  import org.apache.hc.client5.testing.extension.async.TestAsyncClient;
63  import org.apache.hc.core5.http.ContentType;
64  import org.apache.hc.core5.http.HttpHeaders;
65  import org.apache.hc.core5.http.HttpHost;
66  import org.apache.hc.core5.http.HttpResponse;
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.Registry;
71  import org.apache.hc.core5.http.config.RegistryBuilder;
72  import org.apache.hc.core5.http.support.BasicResponseBuilder;
73  import org.apache.hc.core5.net.URIAuthority;
74  import org.hamcrest.CoreMatchers;
75  import org.junit.jupiter.api.Assertions;
76  import org.junit.jupiter.api.Test;
77  import org.mockito.Mockito;
78  
79  public abstract class AbstractHttpAsyncClientAuthenticationTest extends AbstractIntegrationTestBase {
80  
81      public AbstractHttpAsyncClientAuthenticationTest(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) {
82          super(scheme, clientProtocolLevel, serverProtocolLevel);
83      }
84  
85      @Test
86      public void testBasicAuthenticationNoCreds() throws Exception {
87          final HttpHost target = startServer();
88          configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
89  
90          final TestAsyncClient client = startClient();
91  
92          final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
93          final HttpClientContext context = HttpClientContext.create();
94          context.setCredentialsProvider(credsProvider);
95  
96          final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
97                          .setHttpHost(target)
98                          .setPath("/")
99                          .build(), context, null);
100         final HttpResponse response = future.get();
101 
102         Assertions.assertNotNull(response);
103         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
104         Mockito.verify(credsProvider).getCredentials(
105                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
106     }
107 
108     @Test
109     public void testBasicAuthenticationFailure() throws Exception {
110         final HttpHost target = startServer();
111         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
112 
113         final TestAsyncClient client = startClient();
114 
115         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
116         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
117                 .thenReturn(new UsernamePasswordCredentials("test", "all-wrong".toCharArray()));
118         final HttpClientContext context = HttpClientContext.create();
119         context.setCredentialsProvider(credsProvider);
120 
121         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
122                         .setHttpHost(target)
123                         .setPath("/")
124                         .build(), context, null);
125         final HttpResponse response = future.get();
126 
127         Assertions.assertNotNull(response);
128         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
129         Mockito.verify(credsProvider).getCredentials(
130                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
131     }
132 
133     @Test
134     public void testBasicAuthenticationSuccess() throws Exception {
135         final HttpHost target = startServer();
136         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
137 
138         final TestAsyncClient client = startClient();
139 
140         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
141         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
142                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
143         final HttpClientContext context = HttpClientContext.create();
144         context.setCredentialsProvider(credsProvider);
145 
146         final Future<SimpleHttpResponse> future = client.execute(
147                 SimpleRequestBuilder.get()
148                         .setHttpHost(target)
149                         .setPath("/")
150                         .build(), context, null);
151         final HttpResponse response = future.get();
152 
153         Assertions.assertNotNull(response);
154         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
155         Mockito.verify(credsProvider).getCredentials(
156                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
157     }
158 
159     @Test
160     public void testBasicAuthenticationWithEntitySuccess() throws Exception {
161         final HttpHost target = startServer();
162         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
163 
164         final TestAsyncClient client = startClient();
165 
166         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
167         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
168                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
169         final HttpClientContext context = HttpClientContext.create();
170         context.setCredentialsProvider(credsProvider);
171         final Future<SimpleHttpResponse> future = client.execute(
172                 SimpleRequestBuilder.put()
173                         .setHttpHost(target)
174                         .setPath("/")
175                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
176                         .build(), context, null);
177         final HttpResponse response = future.get();
178 
179         Assertions.assertNotNull(response);
180         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
181         Mockito.verify(credsProvider).getCredentials(
182                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
183     }
184 
185     @Test
186     public void testBasicAuthenticationExpectationFailure() throws Exception {
187         final HttpHost target = startServer();
188         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
189 
190         final TestAsyncClient client = startClient();
191 
192         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
193         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
194                 .thenReturn(new UsernamePasswordCredentials("test", "all-wrong".toCharArray()));
195         final HttpClientContext context = HttpClientContext.create();
196         context.setCredentialsProvider(credsProvider);
197         context.setRequestConfig(RequestConfig.custom().setExpectContinueEnabled(true).build());
198         final Future<SimpleHttpResponse> future = client.execute(
199                 SimpleRequestBuilder.put()
200                         .setHttpHost(target)
201                         .setPath("/")
202                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
203                         .build(), context, null);
204         final HttpResponse response = future.get();
205 
206         Assertions.assertNotNull(response);
207         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
208     }
209 
210     @Test
211     public void testBasicAuthenticationExpectationSuccess() throws Exception {
212         final HttpHost target = startServer();
213         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
214 
215         final TestAsyncClient client = startClient();
216 
217         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
218         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
219                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
220         final HttpClientContext context = HttpClientContext.create();
221         context.setCredentialsProvider(credsProvider);
222         context.setRequestConfig(RequestConfig.custom().setExpectContinueEnabled(true).build());
223         final Future<SimpleHttpResponse> future = client.execute(
224                 SimpleRequestBuilder.put()
225                         .setHttpHost(target)
226                         .setPath("/")
227                         .setBody("Some important stuff", ContentType.TEXT_PLAIN)
228                         .build(), context, null);
229         final HttpResponse response = future.get();
230 
231         Assertions.assertNotNull(response);
232         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
233         Mockito.verify(credsProvider).getCredentials(
234                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
235     }
236 
237     @Test
238     public void testBasicAuthenticationCredentialsCaching() throws Exception {
239         final HttpHost target = startServer();
240         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
241 
242         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
243         configureClient(builder ->  builder.setTargetAuthenticationStrategy(authStrategy));
244         final TestAsyncClient client = startClient();
245 
246         final HttpClientContext context = HttpClientContext.create();
247         context.setCredentialsProvider(CredentialsProviderBuilder.create()
248                 .add(target, "test", "test".toCharArray())
249                 .build());
250 
251         for (int i = 0; i < 5; i++) {
252             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
253                     .setHttpHost(target)
254                     .setPath("/")
255                     .build(), context, null);
256             final HttpResponse response = future.get();
257             Assertions.assertNotNull(response);
258             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
259         }
260 
261         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
262     }
263 
264     @Test
265     public void testBasicAuthenticationCredentialsCachingByPathPrefix() throws Exception {
266         final HttpHost target = startServer();
267         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
268 
269         final DefaultAuthenticationStrategy authStrategy = Mockito.spy(new DefaultAuthenticationStrategy());
270         final Queue<HttpResponse> responseQueue = new ConcurrentLinkedQueue<>();
271 
272         configureClient(builder -> builder
273                 .setTargetAuthenticationStrategy(authStrategy)
274                 .addResponseInterceptorFirst((response, entity, context)
275                         -> responseQueue.add(BasicResponseBuilder.copy(response).build())));
276         final TestAsyncClient client = startClient();
277 
278         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
279                 .add(target, "test", "test".toCharArray())
280                 .build();
281 
282         final AuthCache authCache = new BasicAuthCache();
283 
284         for (final String requestPath: new String[] {"/blah/a", "/blah/b?huh", "/blah/c", "/bl%61h/%61"}) {
285             final HttpClientContext context = HttpClientContext.create();
286             context.setAuthCache(authCache);
287             context.setCredentialsProvider(credentialsProvider);
288             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
289                     .setHttpHost(target)
290                     .setPath(requestPath)
291                     .build(), context, null);
292             final HttpResponse response = future.get();
293             Assertions.assertNotNull(response);
294             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
295         }
296 
297         // There should be only single auth strategy call for all successful message exchanges
298         Mockito.verify(authStrategy).select(Mockito.any(), Mockito.any(), Mockito.any());
299 
300         assertThat(
301                 responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
302                 CoreMatchers.equalTo(Arrays.asList(401, 200, 200, 200, 200)));
303 
304         responseQueue.clear();
305         authCache.clear();
306         Mockito.reset(authStrategy);
307 
308         for (final String requestPath: new String[] {"/blah/a", "/yada/a", "/blah/blah/"}) {
309             final HttpClientContext context = HttpClientContext.create();
310             context.setCredentialsProvider(credentialsProvider);
311             context.setAuthCache(authCache);
312             final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
313                     .setHttpHost(target)
314                     .setPath(requestPath)
315                     .build(), context, null);
316             final HttpResponse response = future.get();
317             Assertions.assertNotNull(response);
318             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
319         }
320 
321         // There should be an auth strategy call for all successful message exchanges
322         Mockito.verify(authStrategy, Mockito.times(3)).select(Mockito.any(), Mockito.any(), Mockito.any());
323 
324         assertThat(
325                 responseQueue.stream().map(HttpResponse::getCode).collect(Collectors.toList()),
326                 CoreMatchers.equalTo(Arrays.asList(401, 200, 401, 200, 401, 200)));
327     }
328 
329     @Test
330     public void testAuthenticationUserinfoInRequestFailure() throws Exception {
331         final HttpHost target = startServer();
332         configureServer(bootstrap -> bootstrap.register("*", AsyncEchoHandler::new));
333 
334         final TestAsyncClient client = startClient();
335 
336         final HttpClientContext context = HttpClientContext.create();
337         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
338                         .setScheme(target.getSchemeName())
339                         .setAuthority(new URIAuthority("test:test", target.getHostName(), target.getPort()))
340                         .setPath("/")
341                         .build(), context, null);
342         final ExecutionException exception = Assertions.assertThrows(ExecutionException.class, () -> future.get());
343         assertThat(exception.getCause(), CoreMatchers.instanceOf(ProtocolException.class));
344     }
345 
346     @Test
347     public void testReauthentication() throws Exception {
348         final Authenticator authenticator = new BasicTestAuthenticator("test:test", "test realm") {
349 
350             private final AtomicLong count = new AtomicLong(0);
351 
352             @Override
353             public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
354                 final boolean authenticated = super.authenticate(authority, requestUri, credentials);
355                 if (authenticated) {
356                     return this.count.incrementAndGet() % 4 != 0;
357                 }
358                 return false;
359             }
360         };
361 
362         final HttpHost target = startServer();
363         configureServer(bootstrap -> bootstrap
364                 .register("*", AsyncEchoHandler::new)
365                 .setExchangeHandlerDecorator(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, authenticator) {
366 
367                     @Override
368                     protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
369                         unauthorized.removeHeaders(HttpHeaders.WWW_AUTHENTICATE);
370                         unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, "MyBasic realm=\"test realm\"");
371                     }
372 
373                 }));
374 
375         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
376         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
377                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
378 
379         final Registry<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
380                 .register("MyBasic", context -> new BasicScheme() {
381 
382                     private static final long serialVersionUID = 1L;
383 
384                     @Override
385                     public String getName() {
386                         return "MyBasic";
387                     }
388 
389                 })
390                 .build();
391 
392         configureClient(builder -> builder.setDefaultAuthSchemeRegistry(authSchemeRegistry));
393         final TestAsyncClient client = startClient();
394 
395 
396         final RequestConfig config = RequestConfig.custom()
397                 .setTargetPreferredAuthSchemes(Collections.singletonList("MyBasic"))
398                 .build();
399         final HttpClientContext context = HttpClientContext.create();
400         context.setCredentialsProvider(credsProvider);
401 
402         for (int i = 0; i < 10; i++) {
403             final SimpleHttpRequest request = SimpleRequestBuilder.get()
404                         .setHttpHost(target)
405                         .setPath("/")
406                         .build();
407             request.setConfig(config);
408             final Future<SimpleHttpResponse> future = client.execute(request, context, null);
409             final SimpleHttpResponse response = future.get();
410             Assertions.assertNotNull(response);
411             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
412         }
413     }
414 
415     @Test
416     public void testAuthenticationFallback() throws Exception {
417         final HttpHost target = startServer();
418         configureServer(bootstrap -> bootstrap
419                 .register("*", AsyncEchoHandler::new)
420                 .setExchangeHandlerDecorator(exchangeHandler -> new AuthenticatingAsyncDecorator(exchangeHandler, new BasicTestAuthenticator("test:test", "test realm")) {
421 
422                     @Override
423                     protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
424                         unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"test realm\" invalid");
425                     }
426 
427                 }));
428 
429         final TestAsyncClient client = startClient();
430 
431         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
432         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
433                 .thenReturn(new UsernamePasswordCredentials("test", "test".toCharArray()));
434         final HttpClientContext context = HttpClientContext.create();
435         context.setCredentialsProvider(credsProvider);
436 
437         final Future<SimpleHttpResponse> future = client.execute(SimpleRequestBuilder.get()
438                         .setHttpHost(target)
439                         .setPath("/")
440                         .build(), context, null);
441         final SimpleHttpResponse response = future.get();
442         Assertions.assertNotNull(response);
443         Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
444         Mockito.verify(credsProvider).getCredentials(
445                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
446     }
447 
448     private final static String CHARS = "0123456789abcdef";
449 
450     @Test
451     public void testBearerTokenAuthentication() throws Exception {
452         final SecureRandom secureRandom = SecureRandom.getInstanceStrong();
453         secureRandom.setSeed(System.currentTimeMillis());
454         final StringBuilder buf = new StringBuilder();
455         for (int i = 0; i < 16; i++) {
456             buf.append(CHARS.charAt(secureRandom.nextInt(CHARS.length() - 1)));
457         }
458         final String token = buf.toString();
459 
460         final HttpHost target = startServer();
461         configureServer(bootstrap -> bootstrap
462                 .register("*", AsyncEchoHandler::new)
463                 .setExchangeHandlerDecorator(requestHandler ->
464                         new AuthenticatingAsyncDecorator(
465                                 requestHandler,
466                                 new BearerAuthenticationHandler(),
467                                 new BasicTestAuthenticator(token, "test realm"))));
468 
469         final TestAsyncClient client = startClient();
470 
471         final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
472         final HttpClientContext context1 = HttpClientContext.create();
473         context1.setCredentialsProvider(credsProvider);
474 
475         final Future<SimpleHttpResponse> future1 = client.execute(SimpleRequestBuilder.get()
476                 .setHttpHost(target)
477                 .setPath("/")
478                 .build(), context1, null);
479         final SimpleHttpResponse response1 = future1.get();
480         Assertions.assertNotNull(response1);
481         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode());
482         Mockito.verify(credsProvider).getCredentials(
483                 Mockito.eq(new AuthScope(target, "test realm", "bearer")), Mockito.any());
484 
485         final HttpClientContext context2 = HttpClientContext.create();
486         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
487                 .thenReturn(new BearerToken(token));
488         context2.setCredentialsProvider(credsProvider);
489 
490         final Future<SimpleHttpResponse> future2 = client.execute(SimpleRequestBuilder.get()
491                 .setHttpHost(target)
492                 .setPath("/")
493                 .build(), context2, null);
494         final SimpleHttpResponse response2 = future2.get();
495         Assertions.assertNotNull(response2);
496         Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
497 
498         final HttpClientContext context3 = HttpClientContext.create();
499         Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
500                 .thenReturn(new BearerToken(token + "-expired"));
501         context3.setCredentialsProvider(credsProvider);
502 
503         final Future<SimpleHttpResponse> future3 = client.execute(SimpleRequestBuilder.get()
504                 .setHttpHost(target)
505                 .setPath("/")
506                 .build(), context3, null);
507         final SimpleHttpResponse response3 = future3.get();
508         Assertions.assertNotNull(response3);
509         Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response3.getCode());
510     }
511 
512 }