Implement Support for Userhash Parameter in Digest Authentication as per RFC 7616 (#509)

This commit introduces support for the userhash parameter in Digest Authentication, conforming to the specifications outlined in RFC 7616. The userhash parameter enhances security by allowing the client to hash the username before transmission, thereby protecting the username during transport. This implementation ensures that when the server indicates support for username hashing (userhash=true), the client correctly calculates and includes the hashed username in the Authorization header field, adhering to the protocol defined in RFC 7616 for enhanced security in HTTP Digest Access Authentication.
This commit is contained in:
Arturo Bernal 2023-12-03 21:53:36 +01:00 committed by Oleg Kalnichevski
parent 6976ab58f2
commit 3eaf9bf5c0
2 changed files with 115 additions and 8 deletions

View File

@ -113,6 +113,24 @@ public class DigestScheme implements AuthScheme, Serializable {
private boolean complete;
private transient ByteArrayBuilder buffer;
/**
* Flag indicating whether username hashing is supported.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*/
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<BasicNameValuePair> 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;
}
}

View File

@ -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<String, String> 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<String, String> 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);
}
}