diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 66d2b73b3..c2cf701b8 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -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. @@ -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 && @@ -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(); @@ -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(); diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java index c98f19483..971025275 100644 --- a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -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. @@ -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))); + } }