AuthenticatorUtilsTest.java
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.asynchttpclient.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.asynchttpclient.Realm;
import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.request.body.generator.ByteArrayBodyGenerator;
import org.asynchttpclient.request.body.generator.FileBodyGenerator;
import org.asynchttpclient.request.body.generator.InputStreamBodyGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.ByteArrayInputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AuthenticatorUtilsTest {
@Test
void computeBodyHashEmptyBody() throws Exception {
Request request = new RequestBuilder("GET")
.setUrl("http://example.com/api/users")
.build();
Realm realm = new Realm.Builder("user", "pass")
.setAlgorithm("MD5")
.setScheme(Realm.AuthScheme.DIGEST)
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expectedHash = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest());
assertEquals(expectedHash, bodyHash);
}
@Test
void computeBodyHashStringBody_DefaultCharset() throws Exception {
String Body = "Hello World";
Request request = new RequestBuilder("POST")
.setUrl("http://example.com/api/users")
.setBody(Body)
.build();
Realm realm = new Realm.Builder("user", "pass")
.setAlgorithm("MD5")
.setScheme(Realm.AuthScheme.DIGEST)
.build();
String BodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expectedHash = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(Body.getBytes(StandardCharsets.ISO_8859_1)));
assertEquals(expectedHash, BodyHash);
}
@Test
void computeBodyHashStringBody_UTF8() throws Exception {
String Body = "Hello ������"; //chinese
Request request = new RequestBuilder("POST")
.setUrl("http://example.com/api/users")
.setBody(Body)
.setCharset(StandardCharsets.UTF_8)
.build();
Realm realm = new Realm.Builder("user", "pass")
.setAlgorithm("MD5")
.setScheme(Realm.AuthScheme.DIGEST)
.build();
String BodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expectedHash = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(Body.getBytes(StandardCharsets.UTF_8)));
assertEquals(expectedHash, BodyHash);
}
@Test
void computeBodyHashByteArrayBodyGenerator1() throws Exception {
byte[] body = { 0x01, 0x02, 0x03, 0x04, 0x05 };
Request request = new RequestBuilder("POST")
.setUrl("http://example.com/api")
.setBody(body) // builder will wrap this in a ByteArrayBodyGenerator
.build();
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(body)
);
assertEquals(expected, bodyHash);
}
@Test
void computeBodyHashByteArrayBodyGenerator() throws Exception {
byte[] body = { 0x01, 0x02, 0x03, 0x04, 0x05 };
ByteArrayBodyGenerator generator = new ByteArrayBodyGenerator(body);
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
// all other getters return null
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(body)
);
assertEquals(expected, bodyHash);
}
@Test
void computeBodyHashByteBuf() throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("ByteBuf Test", StandardCharsets.UTF_8);
buf.readerIndex(4); // advance reader ��� we should hash only "Buf Test"
// Mock a Request whose body lives in that ByteBuf
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getByteBufData()).thenReturn(buf);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getBodyGenerator()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
try {
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5")
.digest("Buf Test".getBytes(StandardCharsets.UTF_8))
);
assertEquals(expected, bodyHash, "ByteBuf branch produced wrong digest");
assertEquals(4, buf.readerIndex(), "Reader index must stay unchanged");
} finally {
buf.release();
}
}
@Test
void computeBodyHashByteBuffer() throws Exception {
// Create ByteBuffer payload
ByteBuffer bb = ByteBuffer.wrap("ByteBuffer Test".getBytes(StandardCharsets.UTF_8));
bb.position(5); // advance position ��� helper must hash full content
// Mock a Request whose body lives in that ByteBuffer
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getByteBufferData()).thenReturn(bb);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getBodyGenerator()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
// Expected digest of "ByteBuffer Test" (full content)
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5")
.digest("ByteBuffer Test".getBytes(StandardCharsets.UTF_8))
);
assertEquals(expected, bodyHash, "ByteBuffer branch produced wrong digest");
assertEquals(5, bb.position(), "ByteBuffer position must stay unchanged");
}
@TempDir Path tempDir; // JUnit-5-managed temporary folder
@Test
void computeBodyHashFileBodyGenerator() throws Exception {
String content = "File content for testing";
Path file = tempDir.resolve("test.dat");
Files.writeString(file, content, StandardCharsets.UTF_8);
FileBodyGenerator generator = new FileBodyGenerator(file.toFile());
// Stub Request: only BodyGenerator path is populated
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
// Reference digest
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5")
.digest(content.getBytes(StandardCharsets.UTF_8))
);
assertEquals(expected, bodyHash);
}
@Test
void computeBodyHashMultiChunkByteArray() throws Exception {
// forces three chunks (8 K + 8 K + 4 K)
byte[] data = new byte[20 * 1024];
for (int i = 0; i < data.length; i++) {
data[i] = (byte) (i & 0xFF);
}
ByteArrayBodyGenerator generator = new ByteArrayBodyGenerator(data);
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
String hash1 = AuthenticatorUtils.computeBodyHash(request, realm);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest(data)
);
assertEquals(expected, hash1, "Multi-chunk digest mismatch");
String hash2 = AuthenticatorUtils.computeBodyHash(request, realm);
assertEquals(hash1, hash2, "Digest should be reproducible");
assertEquals(32, hash1.length(), "MD5 hex length");
}
@Test
void byteArrayGeneratorTooLargeThrows() {
byte[] oversized = new byte[11 * 1024 * 1024]; // 11 MB
ByteArrayBodyGenerator generator = new ByteArrayBodyGenerator(oversized);
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
assertThrows(UnsupportedOperationException.class, () -> AuthenticatorUtils.computeBodyHash(request, realm));
}
@Test
void fileBodyGeneratorTooLargeThrows() throws Exception {
// create an 11 MB temp file
Path bigFile = tempDir.resolve("big.bin");
try (OutputStream os = Files.newOutputStream(bigFile)) {
byte[] chunk = new byte[1024 * 1024]; // 1 MB zero-block
for (int i = 0; i < 11; i++) {
os.write(chunk);
}
}
FileBodyGenerator generator = new FileBodyGenerator(bigFile.toFile());
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
assertThrows(UnsupportedOperationException.class, () -> AuthenticatorUtils.computeBodyHash(request, realm));
}
@Test
void unsupportedBodyGeneratorThrows() {
InputStreamBodyGenerator generator = new InputStreamBodyGenerator(new ByteArrayInputStream(new byte[10]));
Request request = mock(Request.class);
when(request.getMethod()).thenReturn("POST");
when(request.getBodyGenerator()).thenReturn(generator);
when(request.getStringData()).thenReturn(null);
when(request.getByteData()).thenReturn(null);
when(request.getByteBufData()).thenReturn(null);
when(request.getByteBufferData()).thenReturn(null);
when(request.getCharset()).thenReturn(null);
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("MD5")
.build();
assertThrows(UnsupportedOperationException.class,
() -> AuthenticatorUtils.computeBodyHash(request, realm));
}
@Test
void computeBodyHashSHA256() throws Exception {
String body = "Test SHA-256";
Request request = new RequestBuilder("POST")
.setUrl("http://example.com/api")
.setBody(body)
.build();
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setAlgorithm("SHA-256")
.build();
String bodyHash = AuthenticatorUtils.computeBodyHash(request, realm);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("SHA-256")
.digest(body.getBytes(StandardCharsets.ISO_8859_1)));
assertEquals(expected, bodyHash);
assertEquals(64, bodyHash.length());
}
@Test
void bytesToHexWorks() {
byte[] input = {0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF};
String hex = MessageDigestUtils.bytesToHex(input);
assertEquals("0123456789abcdef", hex);
}
@Test
void bytesToHexNullThrows() {
assertThrows(IllegalArgumentException.class, () -> MessageDigestUtils.bytesToHex(null));
}
// Phase 5: selectBestDigestChallenge tests
@Test
void selectBestDigestChallenge_selectsFirstSupported() {
List<String> headers = Arrays.asList(
"Digest realm=\"test\", algorithm=SHA-512-256, nonce=\"a\"",
"Digest realm=\"test\", algorithm=SHA-256, nonce=\"b\"",
"Digest realm=\"test\", nonce=\"c\""
);
String best = AuthenticatorUtils.selectBestDigestChallenge(headers);
assertNotNull(best);
assertTrue(best.contains("SHA-512-256"));
}
@Test
void selectBestDigestChallenge_skipsUnsupported() {
List<String> headers = Arrays.asList(
"Digest realm=\"test\", algorithm=SCRAM-SHA-256, nonce=\"a\"",
"Digest realm=\"test\", algorithm=SHA-256, nonce=\"b\""
);
String best = AuthenticatorUtils.selectBestDigestChallenge(headers);
assertNotNull(best);
assertTrue(best.contains("SHA-256"));
}
@Test
void selectBestDigestChallenge_defaultsMD5() {
List<String> headers = List.of("Digest realm=\"test\", nonce=\"a\"");
String best = AuthenticatorUtils.selectBestDigestChallenge(headers);
assertNotNull(best);
// No algorithm specified ��� defaults to MD5, which is supported
assertTrue(best.contains("realm=\"test\""));
}
@Test
void selectBestDigestChallenge_skipsNonDigest() {
List<String> headers = Arrays.asList("Basic realm=\"test\"", "Digest realm=\"test\", nonce=\"a\"");
String best = AuthenticatorUtils.selectBestDigestChallenge(headers);
assertNotNull(best);
assertTrue(best.startsWith("Digest"));
}
@Test
void selectBestDigestChallenge_returnsNull_noDigest() {
List<String> headers = List.of("Basic realm=\"test\"");
assertNull(AuthenticatorUtils.selectBestDigestChallenge(headers));
}
@Test
void selectBestDigestChallenge_returnsNull_allUnsupported() {
List<String> headers = List.of("Digest realm=\"test\", algorithm=UNKNOWN-ALG, nonce=\"a\"");
assertNull(AuthenticatorUtils.selectBestDigestChallenge(headers));
}
// Phase 6: userhash computation
@Test
void computeUserhash_md5() throws Exception {
String result = AuthenticatorUtils.computeUserhash("user", "realm", "MD5", StandardCharsets.ISO_8859_1);
// H("user:realm") using MD5
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("MD5").digest("user:realm".getBytes(StandardCharsets.ISO_8859_1)));
assertEquals(expected, result);
}
@Test
void computeUserhash_sha256() throws Exception {
String result = AuthenticatorUtils.computeUserhash("user", "realm", "SHA-256", StandardCharsets.ISO_8859_1);
String expected = MessageDigestUtils.bytesToHex(
MessageDigest.getInstance("SHA-256").digest("user:realm".getBytes(StandardCharsets.ISO_8859_1)));
assertEquals(expected, result);
assertEquals(64, result.length()); // SHA-256 hex is 64 chars
}
// Phase 7: rspauth computation
@Test
void computeRspAuth_basic() throws Exception {
Realm realm = new Realm.Builder("user", "pass")
.setScheme(Realm.AuthScheme.DIGEST)
.setRealmName("testrealm")
.setNonce("testnonce")
.setAlgorithm("MD5")
.setQop("auth")
.setUri(org.asynchttpclient.uri.Uri.create("http://example.com/path"))
.build();
String rspauth = AuthenticatorUtils.computeRspAuth(realm);
assertNotNull(rspauth);
assertEquals(32, rspauth.length()); // MD5 hex is 32 chars
}
}