Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,66 +207,6 @@ public static String computeRspAuth(Realm realm) {
}
}

/**
* Calculates the digest response value for HTTP Digest Authentication.
* This method computes HA1 and HA2 (including entity-body hash for auth-int).
*
* @param realm The authentication realm containing credentials and challenge info
* @param request The HTTP request (needed for method, uri, and body)
* @return The computed response hex string
* @throws UnsupportedOperationException if qop=auth-int but body cannot be hashed
*/
static String computeDigestResponse(Realm realm, Request request) {
String algorithm = realm.getAlgorithm() != null ? realm.getAlgorithm() : "MD5";
String qop = realm.getQop() != null ? realm.getQop() : "auth";

String hashAlgorithm = algorithm.replace("-sess", "");
Charset wireCharset = realm.getCharset() != null ?
realm.getCharset() : StandardCharsets.ISO_8859_1;

// Calculate HA1
String ha1 = calculateHA1(realm, algorithm);

// Get request URI
Uri uri = request.getUri();
String requestUri = uri.getPath() +
(uri.getQuery() != null ? "?" + uri.getQuery() : "");

// Calculate HA2
String ha2;
if ("auth-int".equals(qop)) {
String bodyHash = computeBodyHash(request, realm);
ha2 = calculateHA2AuthInt(request, requestUri, bodyHash, hashAlgorithm, wireCharset);
} else {
// Regular auth: HA2 = H(method:uri)
String a2Plain = request.getMethod() + ":" + requestUri;
MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm);
try {
md.update(a2Plain.getBytes(wireCharset));
ha2 = MessageDigestUtils.bytesToHex(md.digest());
} finally {
md.reset();
}
}

// Build final response
String nc = realm.getNc() != null ? realm.getNc() : "00000001";
String cnonce = realm.getCnonce();
String nonce = realm.getNonce();

// response = H(HA1:nonce:nc:cnonce:qop:HA2)
String responseInput = ha1 + ":" + nonce + ":" + nc + ":" +
cnonce + ":" + qop + ":" + ha2;

MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm);
try {
md.update(responseInput.getBytes(StandardCharsets.ISO_8859_1));
return MessageDigestUtils.bytesToHex(md.digest());
} finally {
md.reset();
}
}

/**
* Calculates the HA1 value for HTTP Digest Authentication.
* This method handles both regular and session-based HA1 calculations.
Expand Down Expand Up @@ -300,28 +240,6 @@ private static String calculateHA1(Realm realm, String algorithm) {
}
}

/**
* Calculates the HA2 value for HTTP Digest Authentication.
* This method handles both auth and auth-int cases.
*
* @param request The HTTP request (needed for method, uri, and body)
* @param requestUri The request URI
* @param bodyHash The entity-body hash (for auth-int, can be empty for auth)
* @param hashAlgorithm The digest algorithm (e.g., "MD5")
* @param wireCs The charset used for wire encoding
* @return The computed HA2 hex string
*/
private static String calculateHA2AuthInt(Request request, String requestUri, String bodyHash, String hashAlgorithm, Charset wireCs) {
String a2Plain = request.getMethod() + ':' + requestUri + ':' + bodyHash;
MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm);
try {
md.update(a2Plain.getBytes(wireCs));
return MessageDigestUtils.bytesToHex(md.digest());
} finally {
md.reset(); // return clean to pool
}
}

public static String computeBodyHash(Request request, Realm realm) {

if (request.getStringData() == null &&
Expand Down Expand Up @@ -561,8 +479,7 @@ private static void appendQuotedStringContent(StringBuilder builder, @Nullable S
.setMethodName(request.getMethod());

if ("auth-int".equals(proxyRealm.getQop())) {
String response = computeDigestResponse(proxyRealm, request);
realmBuilder.setResponse(response);
realmBuilder.setEntityBodyHash(computeBodyHash(request, proxyRealm));
}

proxyRealm = realmBuilder.build();
Expand Down Expand Up @@ -644,8 +561,7 @@ private static void appendQuotedStringContent(StringBuilder builder, @Nullable S
.setUri(uri)
.setMethodName(request.getMethod());
if ("auth-int".equals(realm.getQop())) {
String response = computeDigestResponse(realmBuilder.build(), request);
realmBuilder.setResponse(response);
realmBuilder.setEntityBodyHash(computeBodyHash(request, realm));
}

realm = realmBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,52 @@ void computeRspAuth_basic() throws Exception {
assertEquals(32, rspauth.length()); // MD5 hex is 32 chars
}

@Test
void preemptiveDigestAuthIntUsesRequestBody() throws Exception {
assertPreemptiveDigestAuthIntUsesRequestBody(false);
}

@Test
void preemptiveProxyDigestAuthIntUsesRequestBody() throws Exception {
assertPreemptiveDigestAuthIntUsesRequestBody(true);
}

private static void assertPreemptiveDigestAuthIntUsesRequestBody(boolean proxy) throws Exception {
String principal = "user";
String password = "pass";
String realmName = "testrealm";
String nonce = "testnonce";
String body = "request body";
Realm realm = new Realm.Builder(principal, password)
.setScheme(Realm.AuthScheme.DIGEST)
.setUsePreemptiveAuth(true)
.setRealmName(realmName)
.setNonce(nonce)
.setAlgorithm("MD5")
.setQop("auth-int")
.build();
Request request = new RequestBuilder("POST")
.setUrl("http://example.com/dir/index.html")
.setBody(body)
.build();

String header = proxy
? AuthenticatorUtils.perRequestProxyAuthorizationHeader(request, realm)
: AuthenticatorUtils.perRequestAuthorizationHeader(request, realm);

assertNotNull(header);
String nc = Realm.Builder.matchParam(header, "nc");
String cnonce = Realm.Builder.matchParam(header, "cnonce");
String response = Realm.Builder.matchParam(header, "response");
assertNotNull(nc);
assertNotNull(cnonce);
assertNotNull(response);
String ha1 = md5(principal + ':' + realmName + ':' + password);
String bodyHash = md5(body);
String ha2 = md5("POST:/dir/index.html:" + bodyHash);
assertEquals(md5(ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ":auth-int:" + ha2), response);
}

// A server-controlled challenge value (nonce/realm/opaque) must be backslash-escaped when echoed
// into the quoted-string params of the outgoing Authorization header, so a bare " cannot close the
// value early and inject extra auth-params.
Expand All @@ -518,4 +564,9 @@ void digestChallengeValuesEscapedInAuthorizationHeader() {
assertTrue(header.contains("opaque=\"op\\\\aque\""), header);
assertFalse(header.contains("nonce=\"abc\"x\""), header);
}

private static String md5(String value) throws Exception {
return MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(value.getBytes(StandardCharsets.ISO_8859_1)));
}
}
Loading