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.http.impl.auth;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.ByteArrayOutputStream;
31  import java.io.IOException;
32  import java.io.ObjectInputStream;
33  import java.io.ObjectOutputStream;
34  import java.nio.charset.StandardCharsets;
35  import java.security.MessageDigest;
36  import java.util.HashMap;
37  import java.util.List;
38  import java.util.Map;
39  
40  import org.apache.hc.client5.http.auth.AuthChallenge;
41  import org.apache.hc.client5.http.auth.AuthScheme;
42  import org.apache.hc.client5.http.auth.AuthScope;
43  import org.apache.hc.client5.http.auth.AuthenticationException;
44  import org.apache.hc.client5.http.auth.ChallengeType;
45  import org.apache.hc.client5.http.auth.CredentialsProvider;
46  import org.apache.hc.client5.http.auth.MalformedChallengeException;
47  import org.apache.hc.client5.http.auth.StandardAuthScheme;
48  import org.apache.hc.core5.http.ClassicHttpRequest;
49  import org.apache.hc.core5.http.ContentType;
50  import org.apache.hc.core5.http.HeaderElement;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpRequest;
53  import org.apache.hc.core5.http.ParseException;
54  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
55  import org.apache.hc.core5.http.io.entity.StringEntity;
56  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
57  import org.apache.hc.core5.http.message.BasicHeaderValueParser;
58  import org.apache.hc.core5.http.message.BasicHttpRequest;
59  import org.apache.hc.core5.http.message.ParserCursor;
60  import org.apache.hc.core5.util.CharArrayBuffer;
61  import org.junit.jupiter.api.Assertions;
62  import org.junit.jupiter.api.Test;
63  
64  /**
65   * Test Methods for DigestScheme Authentication.
66   */
67  public class TestDigestScheme {
68  
69      private static AuthChallenge parse(final String s) throws ParseException {
70          final CharArrayBuffer buffer = new CharArrayBuffer(s.length());
71          buffer.append(s);
72          final ParserCursor cursor = new ParserCursor(0, buffer.length());
73          final List<AuthChallenge> authChallenges = AuthChallengeParser.INSTANCE.parse(ChallengeType.TARGET, buffer, cursor);
74          Assertions.assertEquals(1, authChallenges.size());
75          return authChallenges.get(0);
76      }
77  
78      @Test
79      public void testDigestAuthenticationEmptyChallenge1() throws Exception {
80          final AuthChallenge authChallenge = parse(StandardAuthScheme.DIGEST);
81          final AuthScheme authscheme = new DigestScheme();
82          Assertions.assertThrows(MalformedChallengeException.class, () ->
83                  authscheme.processChallenge(authChallenge, null));
84      }
85  
86      @Test
87      public void testDigestAuthenticationEmptyChallenge2() throws Exception {
88          final AuthChallenge authChallenge = parse(StandardAuthScheme.DIGEST + " ");
89          final AuthScheme authscheme = new DigestScheme();
90          Assertions.assertThrows(MalformedChallengeException.class, () ->
91                  authscheme.processChallenge(authChallenge, null));
92      }
93  
94      @Test
95      public void testDigestAuthenticationWithDefaultCreds() throws Exception {
96          final HttpRequest request = new BasicHttpRequest("Simple", "/");
97          final HttpHost host = new HttpHost("somehost", 80);
98          final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
99                  .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
100                 .build();
101 
102         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
103         final AuthChallenge authChallenge = parse(challenge);
104         final DigestScheme authscheme = new DigestScheme();
105         authscheme.processChallenge(authChallenge, null);
106 
107         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
108         final String authResponse = authscheme.generateAuthResponse(host, request, null);
109         Assertions.assertTrue(authscheme.isChallengeComplete());
110         Assertions.assertFalse(authscheme.isConnectionBased());
111 
112         final Map<String, String> table = parseAuthResponse(authResponse);
113         Assertions.assertEquals("username", table.get("username"));
114         Assertions.assertEquals("realm1", table.get("realm"));
115         Assertions.assertEquals("/", table.get("uri"));
116         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
117         Assertions.assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response"));
118     }
119 
120     @Test
121     public void testDigestAuthentication() throws Exception {
122         final HttpRequest request = new BasicHttpRequest("Simple", "/");
123         final HttpHost host = new HttpHost("somehost", 80);
124         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
125                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
126                 .build();
127 
128         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
129         final AuthChallenge authChallenge = parse(challenge);
130         final DigestScheme authscheme = new DigestScheme();
131         authscheme.processChallenge(authChallenge, null);
132 
133         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
134         final String authResponse = authscheme.generateAuthResponse(host, request, null);
135 
136         final Map<String, String> table = parseAuthResponse(authResponse);
137         Assertions.assertEquals("username", table.get("username"));
138         Assertions.assertEquals("realm1", table.get("realm"));
139         Assertions.assertEquals("/", table.get("uri"));
140         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
141         Assertions.assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response"));
142     }
143 
144     @Test
145     public void testDigestAuthenticationInvalidInput() throws Exception {
146         final HttpHost host = new HttpHost("somehost", 80);
147         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
148                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
149                 .build();
150 
151         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
152         final AuthChallenge authChallenge = parse(challenge);
153         final DigestScheme authscheme = new DigestScheme();
154         authscheme.processChallenge(authChallenge, null);
155 
156         Assertions.assertThrows(NullPointerException.class, () ->
157                 authscheme.isResponseReady(null, credentialsProvider, null));
158         Assertions.assertThrows(NullPointerException.class, () ->
159                 authscheme.isResponseReady(host, null, null));
160         Assertions.assertThrows(NullPointerException.class, () ->
161                 authscheme.generateAuthResponse(host, null, null));
162     }
163 
164     @Test
165     public void testDigestAuthenticationWithSHA() throws Exception {
166         final HttpRequest request = new BasicHttpRequest("Simple", "/");
167         final HttpHost host = new HttpHost("somehost", 80);
168         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
169                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
170                 .build();
171 
172         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", " +
173                 "nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
174                 "algorithm=SHA";
175         final AuthChallenge authChallenge = parse(challenge);
176         final DigestScheme authscheme = new DigestScheme();
177         authscheme.processChallenge(authChallenge, null);
178 
179         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
180         final String authResponse = authscheme.generateAuthResponse(host, request, null);
181 
182         final Map<String, String> table = parseAuthResponse(authResponse);
183         Assertions.assertEquals("username", table.get("username"));
184         Assertions.assertEquals("realm1", table.get("realm"));
185         Assertions.assertEquals("/", table.get("uri"));
186         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
187         Assertions.assertEquals("8769e82e4e28ecc040b969562b9050580c6d186d", table.get("response"));
188     }
189 
190     @Test
191     public void testDigestAuthenticationWithQueryStringInDigestURI() throws Exception {
192         final HttpRequest request = new BasicHttpRequest("Simple", "/?param=value");
193         final HttpHost host = new HttpHost("somehost", 80);
194         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
195                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
196                 .build();
197 
198         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
199         final AuthChallenge authChallenge = parse(challenge);
200         final DigestScheme authscheme = new DigestScheme();
201         authscheme.processChallenge(authChallenge, null);
202 
203         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
204         final String authResponse = authscheme.generateAuthResponse(host, request, null);
205 
206         final Map<String, String> table = parseAuthResponse(authResponse);
207         Assertions.assertEquals("username", table.get("username"));
208         Assertions.assertEquals("realm1", table.get("realm"));
209         Assertions.assertEquals("/?param=value", table.get("uri"));
210         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
211         Assertions.assertEquals("a847f58f5fef0bc087bcb9c3eb30e042", table.get("response"));
212     }
213 
214     @Test
215     public void testDigestAuthenticationNoRealm() throws Exception {
216         final HttpRequest request = new BasicHttpRequest("Simple", "/");
217         final HttpHost host = new HttpHost("somehost", 80);
218         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
219                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
220                 .build();
221 
222         final String challenge = StandardAuthScheme.DIGEST + " no-realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
223         final AuthChallenge authChallenge = parse(challenge);
224         final DigestScheme authscheme = new DigestScheme();
225         authscheme.processChallenge(authChallenge, null);
226 
227         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
228         Assertions.assertThrows(AuthenticationException.class, () ->
229                 authscheme.generateAuthResponse(host, request, null));
230     }
231 
232     @Test
233     public void testDigestAuthenticationNoNonce() throws Exception {
234         final HttpRequest request = new BasicHttpRequest("Simple", "/");
235         final HttpHost host = new HttpHost("somehost", 80);
236         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
237                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
238                 .build();
239 
240         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", no-nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
241         final AuthChallenge authChallenge = parse(challenge);
242         final DigestScheme authscheme = new DigestScheme();
243         authscheme.processChallenge(authChallenge, null);
244 
245         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
246         Assertions.assertThrows(AuthenticationException.class, () ->
247                 authscheme.generateAuthResponse(host, request, null));
248     }
249 
250     @Test
251     public void testDigestAuthenticationNoAlgorithm() throws Exception {
252         final HttpRequest request = new BasicHttpRequest("Simple", "/");
253         final HttpHost host = new HttpHost("somehost", 80);
254         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
255                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
256                 .build();
257 
258         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\"";
259         final AuthChallenge authChallenge = parse(challenge);
260         final DigestScheme authscheme = new DigestScheme();
261         authscheme.processChallenge(authChallenge, null);
262 
263         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
264         final String authResponse = authscheme.generateAuthResponse(host, request, null);
265 
266         final Map<String, String> table = parseAuthResponse(authResponse);
267         Assertions.assertNull(table.get("algorithm"));
268     }
269 
270     @Test
271     public void testDigestAuthenticationMD5Algorithm() throws Exception {
272         final HttpRequest request = new BasicHttpRequest("Simple", "/");
273         final HttpHost host = new HttpHost("somehost", 80);
274         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
275                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
276                 .build();
277 
278         final String challenge = StandardAuthScheme.DIGEST
279                 + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\""
280                 + ", algorithm=MD5";
281         final AuthChallenge authChallenge = parse(challenge);
282         final DigestScheme authscheme = new DigestScheme();
283         authscheme.processChallenge(authChallenge, null);
284 
285         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
286         final String authResponse = authscheme.generateAuthResponse(host, request, null);
287 
288         final Map<String, String> table = parseAuthResponse(authResponse);
289         Assertions.assertEquals("MD5", table.get("algorithm"));
290     }
291 
292     /**
293      * Test digest authentication using the MD5-sess algorithm.
294      */
295     @Test
296     public void testDigestAuthenticationMD5Sess() throws Exception {
297         // Example using Digest auth with MD5-sess
298 
299         final String realm="realm";
300         final String username="username";
301         final String password="password";
302         final String nonce="e273f1776275974f1a120d8b92c5b3cb";
303 
304         final HttpRequest request = new BasicHttpRequest("Simple", "/");
305         final HttpHost host = new HttpHost("somehost", 80);
306         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
307                 .add(new AuthScope(host, realm, null), username, password.toCharArray())
308                 .build();
309 
310         final String challenge=StandardAuthScheme.DIGEST + " realm=\"" + realm + "\", "
311             + "nonce=\"" + nonce + "\", "
312             + "opaque=\"SomeString\", "
313             + "stale=false, "
314             + "algorithm=MD5-sess, "
315             + "qop=\"auth,auth-int\""; // we pass both but expect auth to be used
316 
317         final AuthChallenge authChallenge = parse(challenge);
318 
319         final DigestScheme authscheme = new DigestScheme();
320         authscheme.processChallenge(authChallenge, null);
321 
322         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
323         final String authResponse = authscheme.generateAuthResponse(host, request, null);
324 
325         Assertions.assertTrue(authResponse.indexOf("nc=00000001") > 0); // test for quotes
326         Assertions.assertTrue(authResponse.indexOf("qop=auth") > 0); // test for quotes
327 
328         final Map<String, String> table = parseAuthResponse(authResponse);
329         Assertions.assertEquals(username, table.get("username"));
330         Assertions.assertEquals(realm, table.get("realm"));
331         Assertions.assertEquals("MD5-sess", table.get("algorithm"));
332         Assertions.assertEquals("/", table.get("uri"));
333         Assertions.assertEquals(nonce, table.get("nonce"));
334         Assertions.assertEquals(1, Integer.parseInt(table.get("nc"),16));
335         Assertions.assertNotNull(table.get("cnonce"));
336         Assertions.assertEquals("SomeString", table.get("opaque"));
337         Assertions.assertEquals("auth", table.get("qop"));
338         //@TODO: add better check
339         Assertions.assertNotNull(table.get("response"));
340     }
341 
342     /**
343      * Test digest authentication using the MD5-sess algorithm.
344      */
345     @Test
346     public void testDigestAuthenticationMD5SessNoQop() throws Exception {
347         // Example using Digest auth with MD5-sess
348 
349         final String realm="realm";
350         final String username="username";
351         final String password="password";
352         final String nonce="e273f1776275974f1a120d8b92c5b3cb";
353 
354         final HttpRequest request = new BasicHttpRequest("Simple", "/");
355         final HttpHost host = new HttpHost("somehost", 80);
356         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
357                 .add(new AuthScope(host, realm, null), username, password.toCharArray())
358                 .build();
359 
360         final String challenge=StandardAuthScheme.DIGEST + " realm=\"" + realm + "\", "
361             + "nonce=\"" + nonce + "\", "
362             + "opaque=\"SomeString\", "
363             + "stale=false, "
364             + "algorithm=MD5-sess";
365 
366         final AuthChallenge authChallenge = parse(challenge);
367 
368         final DigestScheme authscheme = new DigestScheme();
369         authscheme.processChallenge(authChallenge, null);
370         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
371         final String authResponse = authscheme.generateAuthResponse(host, request, null);
372 
373         final Map<String, String> table = parseAuthResponse(authResponse);
374         Assertions.assertEquals(username, table.get("username"));
375         Assertions.assertEquals(realm, table.get("realm"));
376         Assertions.assertEquals("MD5-sess", table.get("algorithm"));
377         Assertions.assertEquals("/", table.get("uri"));
378         Assertions.assertEquals(nonce, table.get("nonce"));
379         Assertions.assertNull(table.get("nc"));
380         Assertions.assertEquals("SomeString", table.get("opaque"));
381         Assertions.assertNull(table.get("qop"));
382         //@TODO: add better check
383         Assertions.assertNotNull(table.get("response"));
384     }
385 
386     /**
387      * Test digest authentication with unknown qop value
388      */
389     @Test
390     public void testDigestAuthenticationMD5SessUnknownQop() throws Exception {
391         // Example using Digest auth with MD5-sess
392 
393         final String realm="realm";
394         final String username="username";
395         final String password="password";
396         final String nonce="e273f1776275974f1a120d8b92c5b3cb";
397 
398         final HttpRequest request = new BasicHttpRequest("Simple", "/");
399         final HttpHost host = new HttpHost("somehost", 80);
400         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
401                 .add(new AuthScope(host, realm, null), username, password.toCharArray())
402                 .build();
403 
404         final String challenge=StandardAuthScheme.DIGEST + " realm=\"" + realm + "\", "
405             + "nonce=\"" + nonce + "\", "
406             + "opaque=\"SomeString\", "
407             + "stale=false, "
408             + "algorithm=MD5-sess, "
409             + "qop=\"stuff\"";
410 
411         final AuthChallenge authChallenge = parse(challenge);
412 
413         final DigestScheme authscheme = new DigestScheme();
414         authscheme.processChallenge(authChallenge, null);
415 
416         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
417         Assertions.assertThrows(AuthenticationException.class, () ->
418                 authscheme.generateAuthResponse(host, request, null));
419     }
420 
421     /**
422      * Test digest authentication with unknown qop value
423      */
424     @Test
425     public void testDigestAuthenticationUnknownAlgo() throws Exception {
426         // Example using Digest auth with MD5-sess
427 
428         final String realm="realm";
429         final String username="username";
430         final String password="password";
431         final String nonce="e273f1776275974f1a120d8b92c5b3cb";
432 
433         final HttpRequest request = new BasicHttpRequest("Simple", "/");
434         final HttpHost host = new HttpHost("somehost", 80);
435         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
436                 .add(new AuthScope(host, realm, null), username, password.toCharArray())
437                 .build();
438 
439         final String challenge=StandardAuthScheme.DIGEST + " realm=\"" + realm + "\", "
440             + "nonce=\"" + nonce + "\", "
441             + "opaque=\"SomeString\", "
442             + "stale=false, "
443             + "algorithm=stuff, "
444             + "qop=\"auth\"";
445 
446         final AuthChallenge authChallenge = parse(challenge);
447 
448         final DigestScheme authscheme = new DigestScheme();
449         authscheme.processChallenge(authChallenge, null);
450 
451         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
452         Assertions.assertThrows(AuthenticationException.class, () ->
453                 authscheme.generateAuthResponse(host, request, null));
454     }
455 
456     @Test
457     public void testDigestAuthenticationWithStaleNonce() throws Exception {
458         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", " +
459                 "nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", stale=\"true\"";
460         final AuthChallenge authChallenge = parse(challenge);
461         final AuthScheme authscheme = new DigestScheme();
462         authscheme.processChallenge(authChallenge, null);
463 
464         Assertions.assertFalse(authscheme.isChallengeComplete());
465     }
466 
467     private static Map<String, String> parseAuthResponse(final String authResponse) {
468         if (!authResponse.startsWith(StandardAuthScheme.DIGEST + " ")) {
469             return null;
470         }
471         final String s = authResponse.substring(7);
472         final ParserCursor cursor = new ParserCursor(0, s.length());
473         final HeaderElement[] elements = BasicHeaderValueParser.INSTANCE.parseElements(s, cursor);
474         final Map<String, String> map = new HashMap<>(elements.length);
475         for (final HeaderElement element : elements) {
476             map.put(element.getName(), element.getValue());
477         }
478         return map;
479     }
480 
481     @Test
482     public void testDigestNouceCount() throws Exception {
483         final HttpRequest request = new BasicHttpRequest("GET", "/");
484         final HttpHost host = new HttpHost("somehost", 80);
485         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
486                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
487                 .build();
488 
489         final String challenge1 = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", qop=auth";
490         final AuthChallenge authChallenge1 = parse(challenge1);
491 
492         final DigestScheme authscheme = new DigestScheme();
493         authscheme.processChallenge(authChallenge1, null);
494         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
495         final String authResponse1 = authscheme.generateAuthResponse(host, request, null);
496 
497         final Map<String, String> table1 = parseAuthResponse(authResponse1);
498         Assertions.assertEquals("00000001", table1.get("nc"));
499 
500         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
501         final String authResponse2 = authscheme.generateAuthResponse(host, request, null);
502 
503         final Map<String, String> table2 = parseAuthResponse(authResponse2);
504         Assertions.assertEquals("00000002", table2.get("nc"));
505         final String challenge2 = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", qop=auth";
506         final AuthChallenge authChallenge2 = parse(challenge2);
507         authscheme.processChallenge(authChallenge2, null);
508 
509         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
510         final String authResponse3 = authscheme.generateAuthResponse(host, request, null);
511 
512         final Map<String, String> table3 = parseAuthResponse(authResponse3);
513         Assertions.assertEquals("00000003", table3.get("nc"));
514         final String challenge3 = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"e273f1776275974f1a120d8b92c5b3cb\", qop=auth";
515         final AuthChallenge authChallenge3 = parse(challenge3);
516         authscheme.processChallenge(authChallenge3, null);
517 
518         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
519         final String authResponse4 = authscheme.generateAuthResponse(host, request, null);
520 
521         final Map<String, String> table4 = parseAuthResponse(authResponse4);
522         Assertions.assertEquals("00000001", table4.get("nc"));
523     }
524 
525     @Test
526     public void testDigestMD5SessA1AndCnonceConsistency() throws Exception {
527         final HttpHost host = new HttpHost("somehost", 80);
528         final HttpRequest request = new BasicHttpRequest("GET", "/");
529         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
530                 .add(new AuthScope(host, "subnet.domain.com", null), "username", "password".toCharArray())
531                 .build();
532 
533         final String challenge1 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=MD5-sess, nonce=\"1234567890abcdef\", " +
534                 "charset=utf-8, realm=\"subnet.domain.com\"";
535         final AuthChallenge authChallenge1 = parse(challenge1);
536         final DigestScheme authscheme = new DigestScheme();
537         authscheme.processChallenge(authChallenge1, null);
538         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
539         final String authResponse1 = authscheme.generateAuthResponse(host, request, null);
540 
541         final Map<String, String> table1 = parseAuthResponse(authResponse1);
542         Assertions.assertEquals("00000001", table1.get("nc"));
543         final String cnonce1 = authscheme.getCnonce();
544         final String sessionKey1 = authscheme.getA1();
545 
546         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
547         final String authResponse2 = authscheme.generateAuthResponse(host, request, null);
548         final Map<String, String> table2 = parseAuthResponse(authResponse2);
549         Assertions.assertEquals("00000002", table2.get("nc"));
550         final String cnonce2 = authscheme.getCnonce();
551         final String sessionKey2 = authscheme.getA1();
552 
553         Assertions.assertEquals(cnonce1, cnonce2);
554         Assertions.assertEquals(sessionKey1, sessionKey2);
555 
556         final String challenge2 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=MD5-sess, nonce=\"1234567890abcdef\", " +
557             "charset=utf-8, realm=\"subnet.domain.com\"";
558         final AuthChallenge authChallenge2 = parse(challenge2);
559         authscheme.processChallenge(authChallenge2, null);
560         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
561         final String authResponse3 = authscheme.generateAuthResponse(host, request, null);
562         final Map<String, String> table3 = parseAuthResponse(authResponse3);
563         Assertions.assertEquals("00000003", table3.get("nc"));
564 
565         final String cnonce3 = authscheme.getCnonce();
566         final String sessionKey3 = authscheme.getA1();
567 
568         Assertions.assertEquals(cnonce1, cnonce3);
569         Assertions.assertEquals(sessionKey1, sessionKey3);
570 
571         final String challenge3 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=MD5-sess, nonce=\"fedcba0987654321\", " +
572             "charset=utf-8, realm=\"subnet.domain.com\"";
573         final AuthChallenge authChallenge3 = parse(challenge3);
574         authscheme.processChallenge(authChallenge3, null);
575         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
576         final String authResponse4 = authscheme.generateAuthResponse(host, request, null);
577         final Map<String, String> table4 = parseAuthResponse(authResponse4);
578         Assertions.assertEquals("00000001", table4.get("nc"));
579 
580         final String cnonce4 = authscheme.getCnonce();
581         final String sessionKey4 = authscheme.getA1();
582 
583         Assertions.assertNotEquals(cnonce1, cnonce4);
584         Assertions.assertNotEquals(sessionKey1, sessionKey4);
585     }
586 
587     @Test
588     public void testHttpEntityDigest() throws Exception {
589         final HttpEntityDigester digester = new HttpEntityDigester(MessageDigest.getInstance("MD5"));
590         Assertions.assertNull(digester.getDigest());
591         digester.write('a');
592         digester.write('b');
593         digester.write('c');
594         digester.write(0xe4);
595         digester.write(0xf6);
596         digester.write(0xfc);
597         digester.write(new byte[] { 'a', 'b', 'c'});
598         Assertions.assertNull(digester.getDigest());
599         digester.close();
600         Assertions.assertEquals("acd2b59cd01c7737d8069015584c6cac", DigestScheme.formatHex(digester.getDigest()));
601         Assertions.assertThrows(IOException.class, () -> digester.write('a'));
602         Assertions.assertThrows(IOException.class, () -> digester.write(new byte[] { 'a', 'b', 'c'}));
603     }
604 
605     @Test
606     public void testDigestAuthenticationQopAuthInt() throws Exception {
607         final ClassicHttpRequest request = new BasicClassicHttpRequest("Post", "/");
608         request.setEntity(new StringEntity("abc\u00e4\u00f6\u00fcabc", StandardCharsets.ISO_8859_1));
609         final HttpHost host = new HttpHost("somehost", 80);
610         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
611                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
612                 .build();
613 
614         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
615                 "qop=\"auth,auth-int\"";
616         final AuthChallenge authChallenge = parse(challenge);
617         final DigestScheme authscheme = new DigestScheme();
618         authscheme.processChallenge(authChallenge, null);
619         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
620         final String authResponse = authscheme.generateAuthResponse(host, request, null);
621 
622         Assertions.assertEquals("Post:/:acd2b59cd01c7737d8069015584c6cac", authscheme.getA2());
623 
624         final Map<String, String> table = parseAuthResponse(authResponse);
625         Assertions.assertEquals("username", table.get("username"));
626         Assertions.assertEquals("realm1", table.get("realm"));
627         Assertions.assertEquals("/", table.get("uri"));
628         Assertions.assertEquals("auth-int", table.get("qop"));
629         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
630     }
631 
632     @Test
633     public void testDigestAuthenticationQopAuthIntNullEntity() throws Exception {
634         final HttpRequest request = new BasicHttpRequest("Post", "/");
635         final HttpHost host = new HttpHost("somehost", 80);
636         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
637                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
638                 .build();
639 
640         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
641                 "qop=\"auth-int\"";
642         final AuthChallenge authChallenge = parse(challenge);
643         final DigestScheme authscheme = new DigestScheme();
644         authscheme.processChallenge(authChallenge, null);
645         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
646         final String authResponse = authscheme.generateAuthResponse(host, request, null);
647 
648         Assertions.assertEquals("Post:/:d41d8cd98f00b204e9800998ecf8427e", authscheme.getA2());
649 
650         final Map<String, String> table = parseAuthResponse(authResponse);
651         Assertions.assertEquals("username", table.get("username"));
652         Assertions.assertEquals("realm1", table.get("realm"));
653         Assertions.assertEquals("/", table.get("uri"));
654         Assertions.assertEquals("auth-int", table.get("qop"));
655         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
656     }
657 
658     @Test
659     public void testDigestAuthenticationQopAuthOrAuthIntNonRepeatableEntity() throws Exception {
660         final ClassicHttpRequest request = new BasicClassicHttpRequest("Post", "/");
661         request.setEntity(new InputStreamEntity(new ByteArrayInputStream(new byte[] {'a'}), -1, ContentType.DEFAULT_TEXT));
662         final HttpHost host = new HttpHost("somehost", 80);
663         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
664                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
665                 .build();
666 
667         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
668                 "qop=\"auth,auth-int\"";
669         final AuthChallenge authChallenge = parse(challenge);
670         final DigestScheme authscheme = new DigestScheme();
671         authscheme.processChallenge(authChallenge, null);
672         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
673         final String authResponse = authscheme.generateAuthResponse(host, request, null);
674 
675         Assertions.assertEquals("Post:/", authscheme.getA2());
676 
677         final Map<String, String> table = parseAuthResponse(authResponse);
678         Assertions.assertEquals("username", table.get("username"));
679         Assertions.assertEquals("realm1", table.get("realm"));
680         Assertions.assertEquals("/", table.get("uri"));
681         Assertions.assertEquals("auth", table.get("qop"));
682         Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
683     }
684 
685     @Test
686     public void testParameterCaseSensitivity() throws Exception {
687         final HttpRequest request = new BasicHttpRequest("GET", "/");
688         final HttpHost host = new HttpHost("somehost", 80);
689         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
690                 .add(new AuthScope(host, "-", null), "username", "password".toCharArray())
691                 .build();
692 
693         final String challenge = StandardAuthScheme.DIGEST + " Realm=\"-\", " +
694                 "nonce=\"YjYuNGYyYmJhMzUuY2I5ZDhlZDE5M2ZlZDM 1Mjk3NGJkNTIyYjgyNTcwMjQ=\", " +
695                 "opaque=\"98700A3D9CE17065E2246B41035C6609\", qop=\"auth\"";
696         final AuthChallenge authChallenge = parse(challenge);
697         final DigestScheme authscheme = new DigestScheme();
698         authscheme.processChallenge(authChallenge, null);
699         Assertions.assertEquals("-", authscheme.getRealm());
700 
701         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
702         authscheme.generateAuthResponse(host, request, null);
703     }
704 
705     @Test
706     public void testDigestAuthenticationQopIntOnlyNonRepeatableEntity() throws Exception {
707         final ClassicHttpRequest request = new BasicClassicHttpRequest("Post", "/");
708         request.setEntity(new InputStreamEntity(new ByteArrayInputStream(new byte[] {'a'}), -1, ContentType.DEFAULT_TEXT));
709         final HttpHost host = new HttpHost("somehost", 80);
710         final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
711                 .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
712                 .build();
713 
714         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
715                 "qop=\"auth-int\"";
716         final AuthChallenge authChallenge = parse(challenge);
717         final DigestScheme authscheme = new DigestScheme();
718         authscheme.processChallenge(authChallenge, null);
719 
720         Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
721         Assertions.assertThrows(AuthenticationException.class, () ->
722                 authscheme.generateAuthResponse(host, request, null));
723     }
724 
725     @Test
726     public void testSerialization() throws Exception {
727         final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", " +
728                 "qop=\"auth,auth-int\"";
729         final AuthChallenge authChallenge = parse(challenge);
730         final DigestScheme digestScheme = new DigestScheme();
731         digestScheme.processChallenge(authChallenge, null);
732 
733         final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
734         final ObjectOutputStream out = new ObjectOutputStream(buffer);
735         out.writeObject(digestScheme);
736         out.flush();
737         final byte[] raw = buffer.toByteArray();
738         final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(raw));
739         final DigestScheme authScheme = (DigestScheme) in.readObject();
740 
741         Assertions.assertEquals(digestScheme.getName(), authScheme.getName());
742         Assertions.assertEquals(digestScheme.getRealm(), authScheme.getRealm());
743         Assertions.assertEquals(digestScheme.isChallengeComplete(), authScheme.isChallengeComplete());
744         Assertions.assertEquals(digestScheme.getA1(), authScheme.getA1());
745         Assertions.assertEquals(digestScheme.getA2(), authScheme.getA2());
746         Assertions.assertEquals(digestScheme.getCnonce(), authScheme.getCnonce());
747     }
748 
749 }