Skip to content

Commit

Permalink
Implement Support for Userhash Parameter in Digest Authentication as …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
arturobernalg authored Dec 3, 2023
1 parent e16c1bf commit da6d60f
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,24 @@ private enum QualityOfProtection {
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;
Expand Down Expand Up @@ -177,6 +195,10 @@ public void processChallenge(
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;
}

Expand Down Expand Up @@ -319,6 +341,15 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
}
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
Expand All @@ -328,13 +359,13 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
// ":" 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();

Expand Down Expand Up @@ -395,7 +426,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
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));
Expand All @@ -413,6 +444,10 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
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) {
Expand Down Expand Up @@ -494,5 +529,4 @@ private void readObject(final ObjectInputStream in) throws IOException, ClassNot
public String toString() {
return getName() + this.paramMap;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void testDigestAuthenticationWithDefaultCreds() throws Exception {
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
Expand All @@ -138,7 +138,7 @@ public void testDigestAuthentication() throws Exception {
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
Expand Down Expand Up @@ -184,7 +184,7 @@ public void testDigestAuthenticationWithSHA() throws Exception {
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
Expand All @@ -208,7 +208,7 @@ public void testDigestAuthenticationWithQueryStringInDigestURI() throws Exceptio
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
Expand Down Expand Up @@ -746,4 +746,77 @@ public void testSerialization() throws Exception {
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, "\"[email protected]\"", null), "\"Mufasa\"", "\"Circle Of Life\"".toCharArray())
.build();

// Include userhash in the challenge
final String challenge = StandardAuthScheme.DIGEST + " realm=\"\\\"[email protected]\\\"\", 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:[email protected]: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);
}
}

0 comments on commit da6d60f

Please sign in to comment.