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);
+ }
}