diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java index 32cd437dd..ad4502afc 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java @@ -113,6 +113,24 @@ public class DigestScheme implements AuthScheme, Serializable { private boolean complete; private transient ByteArrayBuilder buffer; + /** + * Flag indicating whether username hashing is supported. + *

+ * This flag is used to determine if the server supports hashing of the username + * as part of the Digest Access Authentication process. When set to {@code true}, + * the client is expected to hash the username using the same algorithm used for + * hashing the credentials. This is in accordance with Section 3.4.4 of RFC 7616. + *

+ *

+ * The default value is {@code false}, indicating that username hashing is not + * supported. If the server requires username hashing (indicated by the + * {@code userhash} parameter in the a header set to {@code true}), + * this flag should be set to {@code true} to comply with the server's requirements. + *

+ */ + private boolean userhashSupported = false; + + private String lastNonce; private long nounceCount; private String cnonce; @@ -177,6 +195,10 @@ public class DigestScheme implements AuthScheme, Serializable { if (this.paramMap.isEmpty()) { throw new MalformedChallengeException("Missing digest auth parameters"); } + + final String userHashValue = this.paramMap.get("userhash"); + this.userhashSupported = "true".equalsIgnoreCase(userHashValue); + this.complete = true; } @@ -319,6 +341,15 @@ public class DigestScheme implements AuthScheme, Serializable { } buffer.charset(charset); + + String username = credentials.getUserName(); + + if (this.userhashSupported) { + final String usernameRealm = username + ":" + realm; + final byte[] hashedBytes = digester.digest(usernameRealm.getBytes(StandardCharsets.UTF_8)); + username = formatHex(hashedBytes); // Convert to hex string + } + a1 = null; a2 = null; // 3.2.2.2: Calculating digest @@ -328,13 +359,13 @@ public class DigestScheme implements AuthScheme, Serializable { // ":" unq(cnonce-value) // calculated one per session - buffer.append(credentials.getUserName()).append(":").append(realm).append(":").append(credentials.getUserPassword()); + buffer.append(username).append(":").append(credentials.getUserPassword()); final String checksum = formatHex(digester.digest(this.buffer.toByteArray())); buffer.reset(); buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce); } else { // unq(username-value) ":" unq(realm-value) ":" passwd - buffer.append(credentials.getUserName()).append(":").append(realm).append(":").append(credentials.getUserPassword()); + buffer.append(username).append(":").append(credentials.getUserPassword()); } a1 = buffer.toByteArray(); @@ -395,7 +426,7 @@ public class DigestScheme implements AuthScheme, Serializable { buffer.append(StandardAuthScheme.DIGEST + " "); final List params = new ArrayList<>(20); - params.add(new BasicNameValuePair("username", credentials.getUserName())); + params.add(new BasicNameValuePair("username", username)); params.add(new BasicNameValuePair("realm", realm)); params.add(new BasicNameValuePair("nonce", nonce)); params.add(new BasicNameValuePair("uri", uri)); @@ -413,6 +444,10 @@ public class DigestScheme implements AuthScheme, Serializable { params.add(new BasicNameValuePair("opaque", opaque)); } + if (this.userhashSupported) { + params.add(new BasicNameValuePair("userhash", "true")); + } + for (int i = 0; i < params.size(); i++) { final BasicNameValuePair param = params.get(i); if (i > 0) { @@ -494,5 +529,4 @@ public class DigestScheme implements AuthScheme, Serializable { public String toString() { return getName() + this.paramMap; } - } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java index 3f05886cc..382212c83 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java @@ -114,7 +114,7 @@ public class TestDigestScheme { Assertions.assertEquals("realm1", table.get("realm")); Assertions.assertEquals("/", table.get("uri")); Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); - Assertions.assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response")); + Assertions.assertEquals("da46708e64b8380f1c5afa63e8ccd586", table.get("response")); } @Test @@ -138,7 +138,7 @@ public class TestDigestScheme { Assertions.assertEquals("realm1", table.get("realm")); Assertions.assertEquals("/", table.get("uri")); Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); - Assertions.assertEquals("e95a7ddf37c2eab009568b1ed134f89a", table.get("response")); + Assertions.assertEquals("da46708e64b8380f1c5afa63e8ccd586", table.get("response")); } @Test @@ -184,7 +184,7 @@ public class TestDigestScheme { Assertions.assertEquals("realm1", table.get("realm")); Assertions.assertEquals("/", table.get("uri")); Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); - Assertions.assertEquals("8769e82e4e28ecc040b969562b9050580c6d186d", table.get("response")); + Assertions.assertEquals("aa400f3841ebbf39469d9be939a37b86258bd289", table.get("response")); } @Test @@ -208,7 +208,7 @@ public class TestDigestScheme { Assertions.assertEquals("realm1", table.get("realm")); Assertions.assertEquals("/?param=value", table.get("uri")); Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); - Assertions.assertEquals("a847f58f5fef0bc087bcb9c3eb30e042", table.get("response")); + Assertions.assertEquals("c15c577938f7f1228cdb6e8ca51b9140", table.get("response")); } @Test @@ -746,4 +746,77 @@ public class TestDigestScheme { Assertions.assertEquals(digestScheme.getCnonce(), authScheme.getCnonce()); } + + @Test + public void testDigestAuthenticationWithUserHash() throws Exception { + final HttpRequest request = new BasicHttpRequest("Simple", "/"); + final HttpHost host = new HttpHost("somehost", 80); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "realm1", null), "username", "password".toCharArray()) + .build(); + + // Include userhash in the challenge + final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", userhash=true"; + final AuthChallenge authChallenge = parse(challenge); + final DigestScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge, null); + + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse = authscheme.generateAuthResponse(host, request, null); + + final Map table = parseAuthResponse(authResponse); + + // Generate expected userhash + final MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(("username:realm1").getBytes(StandardCharsets.UTF_8)); + final String expectedUserhash = bytesToHex(md.digest()); + + Assertions.assertEquals(expectedUserhash, table.get("username")); + Assertions.assertEquals("realm1", table.get("realm")); + Assertions.assertEquals("/", table.get("uri")); + Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + Assertions.assertEquals("75f7ede943dc401264d236546e49c1df", table.get("response")); + } + + private static String bytesToHex(final byte[] bytes) { + final StringBuilder hexString = new StringBuilder(); + for (final byte b : bytes) { + final String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + @Test + public void testDigestAuthenticationWithQuotedStringsAndWhitespace() throws Exception { + final HttpRequest request = new BasicHttpRequest("Simple", "/"); + final HttpHost host = new HttpHost("somehost", 80); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "\"myhost@example.com\"", null), "\"Mufasa\"", "\"Circle Of Life\"".toCharArray()) + .build(); + + // Include userhash in the challenge + final String challenge = StandardAuthScheme.DIGEST + " realm=\"\\\"myhost@example.com\\\"\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", userhash=true"; + final AuthChallenge authChallenge = parse(challenge); + final DigestScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge, null); + + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + + final String authResponse = authscheme.generateAuthResponse(host, request, null); + + final Map table = parseAuthResponse(authResponse); + + // Generate expected A1 hash + final MessageDigest md = MessageDigest.getInstance("MD5"); + final String a1 = "Mufasa:myhost@example.com:Circle Of Life"; // Note: quotes removed and internal whitespace preserved + md.update(a1.getBytes(StandardCharsets.UTF_8)); + + // Extract the response and validate the A1 hash + final String response = table.get("response"); + Assertions.assertNotNull(response); + } }