From a4a39305c37912ef3df0b28d6778436a79b295f4 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Fri, 1 Nov 2024 22:28:06 +0100 Subject: [PATCH 1/3] Support RFC 7616 compliance in DigestScheme with extended hash algorithm support and charset Enhanced DigestScheme to support SHA-256, SHA-512/256, algorithms in compliance with RFC 7616. Adjusted cnonce generation for adequate entropy in SHA-256 and SHA-512/256 contexts. --- .../client5/http/impl/auth/DigestScheme.java | 79 ++++++-- .../http/impl/auth/TestDigestScheme.java | 178 ++++++++++++++++++ 2 files changed, 243 insertions(+), 14 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java index 85ed712d2d..a9b5d6a6e3 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java @@ -315,11 +315,9 @@ private String createDigestResponse(final HttpRequest request) throws Authentica } final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset); - String digAlg = algorithm; + // If an algorithm is not specified, default to MD5. - if (digAlg == null || digAlg.equalsIgnoreCase("MD5-sess")) { - digAlg = "MD5"; - } + final String digAlg = resolveAlgorithm(algorithm); final MessageDigest digester; try { @@ -343,7 +341,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica final String nc = sb.toString(); if (cnonce == null) { - cnonce = formatHex(createCnonce()); + cnonce = formatHex(createCnonce(digAlg)); } if (buffer == null) { @@ -378,7 +376,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica } // 3.2.2.2: Calculating digest - if ("MD5-sess".equalsIgnoreCase(algorithm)) { + if ("MD5-sess".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { // H( unq(username-value) ":" unq(realm-value) ":" passwd ) // ":" unq(nonce-value) // ":" unq(cnonce-value) @@ -517,10 +515,15 @@ String getA2() { } /** - * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long string. + * Encodes a byte array digest into a hexadecimal string. + *

+ * This method supports digests of various lengths, such as 16 bytes (128-bit) for MD5, + * 32 bytes (256-bit) for SHA-256, and SHA-512/256. Each byte is converted to two + * hexadecimal characters, so the resulting string length is twice the byte array length. + *

* - * @param binaryData array containing the digest - * @return encoded MD5, or {@code null} if encoding failed + * @param binaryData the array containing the digest bytes + * @return encoded hexadecimal string, or {@code null} if encoding failed */ static String formatHex(final byte[] binaryData) { final int n = binaryData.length; @@ -531,22 +534,47 @@ static String formatHex(final byte[] binaryData) { buffer[i * 2] = HEXADECIMAL[high]; buffer[(i * 2) + 1] = HEXADECIMAL[low]; } - return new String(buffer); } + /** - * Creates a random cnonce value based on the current time. + * Creates a random cnonce value based on the specified algorithm's expected entropy. + * Adjusts the length of the byte array based on the algorithm to ensure sufficient entropy. * - * @return The cnonce value as String. + * @param algorithm the algorithm for which the cnonce is being generated (e.g., "MD5", "SHA-256", "SHA-512-256"). + * @return The cnonce value as a byte array. + * @since 5.5 */ - static byte[] createCnonce() { + static byte[] createCnonce(final String algorithm) { final SecureRandom rnd = new SecureRandom(); - final byte[] tmp = new byte[8]; + final int length; + switch (algorithm.toUpperCase()) { + case "SHA-256": + case "SHA-512/256": + length = 32; + break; + case "MD5": + default: + length = 8; + break; + } + final byte[] tmp = new byte[length]; rnd.nextBytes(tmp); return tmp; } + /** + * Creates a random cnonce value based on the current time. + * + * @return The cnonce value as String. + * @deprecated Use {@link DigestScheme#createCnonce(String)} instead. + */ + @Deprecated + static byte[] createCnonce() { + return createCnonce("MD5"); // Default to MD5 to maintain compatibility + } + private void writeObject(final ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeUTF(defaultCharset.name()); @@ -601,4 +629,27 @@ private boolean containsInvalidABNFChars(final String value) { } return false; } + + /** + * Resolves the specified algorithm name to a standard form based on recognized algorithm suffixes. + *

+ * This method translates session-based algorithms (e.g., "-sess" suffix) to their base forms + * for correct MessageDigest usage. If no algorithm is specified or "MD5-sess" is provided, + * it defaults to "MD5". The method also maps "SHA-512-256" to "SHA-512/256" to align with + * Java's naming for SHA-512/256. + *

+ * + * @param algorithm the algorithm name to resolve, such as "MD5-sess", "SHA-256-sess", or "SHA-512-256-sess" + * @return the resolved base algorithm name, or the original algorithm name if no mapping applies + */ + private String resolveAlgorithm(final String algorithm) { + if (algorithm == null || algorithm.equalsIgnoreCase("MD5-sess")) { + return "MD5"; + } else if (algorithm.equalsIgnoreCase("SHA-256-sess")) { + return "SHA-256"; + } else if (algorithm.equalsIgnoreCase("SHA-512-256-sess") || algorithm.equalsIgnoreCase("SHA-512-256")) { + return "SHA-512/256"; + } + return algorithm; // If it's already a supported algorithm or doesn't need translating + } } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java index af8204b85d..f5f010c2d6 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java @@ -987,4 +987,182 @@ void testNoNextNonceUsageFromContext() throws Exception { } + @Test + void testDigestAuthenticationWithSHA256() 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(); + + final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", algorithm=SHA-256"; + 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 table = parseAuthResponse(authResponse); + Assertions.assertEquals("username", table.get("username")); + Assertions.assertEquals("realm1", table.get("realm")); + Assertions.assertEquals("/", table.get("uri")); + Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + Assertions.assertEquals("SHA-256", table.get("algorithm")); + Assertions.assertNotNull(table.get("response")); + + } + + @Test + void testDigestAuthenticationWithSHA512_256() 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(); + + final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", algorithm=SHA-512-256"; + 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 table = parseAuthResponse(authResponse); + Assertions.assertEquals("username", table.get("username")); + Assertions.assertEquals("realm1", table.get("realm")); + Assertions.assertEquals("/", table.get("uri")); + Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); + Assertions.assertEquals("SHA-512-256", table.get("algorithm")); + Assertions.assertNotNull(table.get("response")); + } + + @Test + void testDigestSHA256SessA1AndCnonceConsistency() throws Exception { + final HttpHost host = new HttpHost("somehost", 80); + final HttpRequest request = new BasicHttpRequest("GET", "/"); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "subnet.domain.com", null), "username", "password".toCharArray()) + .build(); + + final String challenge1 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"1234567890abcdef\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge1 = parse(challenge1); + final DigestScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge1, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse1 = authscheme.generateAuthResponse(host, request, null); + + final Map table1 = parseAuthResponse(authResponse1); + Assertions.assertEquals("00000001", table1.get("nc")); + final String cnonce1 = authscheme.getCnonce(); + final String sessionKey1 = authscheme.getA1(); + + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse2 = authscheme.generateAuthResponse(host, request, null); + final Map table2 = parseAuthResponse(authResponse2); + Assertions.assertEquals("00000002", table2.get("nc")); + final String cnonce2 = authscheme.getCnonce(); + final String sessionKey2 = authscheme.getA1(); + + Assertions.assertEquals(cnonce1, cnonce2); + Assertions.assertEquals(sessionKey1, sessionKey2); + + final String challenge2 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"1234567890abcdef\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge2 = parse(challenge2); + authscheme.processChallenge(authChallenge2, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse3 = authscheme.generateAuthResponse(host, request, null); + final Map table3 = parseAuthResponse(authResponse3); + Assertions.assertEquals("00000003", table3.get("nc")); + + final String cnonce3 = authscheme.getCnonce(); + final String sessionKey3 = authscheme.getA1(); + + Assertions.assertEquals(cnonce1, cnonce3); + Assertions.assertEquals(sessionKey1, sessionKey3); + + final String challenge3 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"fedcba0987654321\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge3 = parse(challenge3); + authscheme.processChallenge(authChallenge3, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse4 = authscheme.generateAuthResponse(host, request, null); + final Map table4 = parseAuthResponse(authResponse4); + Assertions.assertEquals("00000001", table4.get("nc")); + + final String cnonce4 = authscheme.getCnonce(); + final String sessionKey4 = authscheme.getA1(); + + Assertions.assertNotEquals(cnonce1, cnonce4); + Assertions.assertNotEquals(sessionKey1, sessionKey4); + } + + + @Test + void testDigestSHA512256SessA1AndCnonceConsistency() throws Exception { + final HttpHost host = new HttpHost("somehost", 80); + final HttpRequest request = new BasicHttpRequest("GET", "/"); + final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(host, "subnet.domain.com", null), "username", "password".toCharArray()) + .build(); + + final String challenge1 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"1234567890abcdef\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge1 = parse(challenge1); + final DigestScheme authscheme = new DigestScheme(); + authscheme.processChallenge(authChallenge1, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse1 = authscheme.generateAuthResponse(host, request, null); + + final Map table1 = parseAuthResponse(authResponse1); + Assertions.assertEquals("00000001", table1.get("nc")); + final String cnonce1 = authscheme.getCnonce(); + final String sessionKey1 = authscheme.getA1(); + + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse2 = authscheme.generateAuthResponse(host, request, null); + final Map table2 = parseAuthResponse(authResponse2); + Assertions.assertEquals("00000002", table2.get("nc")); + final String cnonce2 = authscheme.getCnonce(); + final String sessionKey2 = authscheme.getA1(); + + Assertions.assertEquals(cnonce1, cnonce2); + Assertions.assertEquals(sessionKey1, sessionKey2); + + final String challenge2 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"1234567890abcdef\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge2 = parse(challenge2); + authscheme.processChallenge(authChallenge2, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse3 = authscheme.generateAuthResponse(host, request, null); + final Map table3 = parseAuthResponse(authResponse3); + Assertions.assertEquals("00000003", table3.get("nc")); + + final String cnonce3 = authscheme.getCnonce(); + final String sessionKey3 = authscheme.getA1(); + + Assertions.assertEquals(cnonce1, cnonce3); + Assertions.assertEquals(sessionKey1, sessionKey3); + + final String challenge3 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"fedcba0987654321\", " + + "charset=utf-8, realm=\"subnet.domain.com\""; + final AuthChallenge authChallenge3 = parse(challenge3); + authscheme.processChallenge(authChallenge3, null); + Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); + final String authResponse4 = authscheme.generateAuthResponse(host, request, null); + final Map table4 = parseAuthResponse(authResponse4); + Assertions.assertEquals("00000001", table4.get("nc")); + + final String cnonce4 = authscheme.getCnonce(); + final String sessionKey4 = authscheme.getA1(); + + Assertions.assertNotEquals(cnonce1, cnonce4); + Assertions.assertNotEquals(sessionKey1, sessionKey4); + } + + + } From 4da0d60e92af20bda681881810088f5a3977552b Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Fri, 1 Nov 2024 22:33:34 +0100 Subject: [PATCH 2/3] Increase MD5 cnonce length to 16 bytes for full 128-bit entropy --- .../java/org/apache/hc/client5/http/impl/auth/DigestScheme.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java index a9b5d6a6e3..7e8a153a9f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java @@ -556,7 +556,7 @@ static byte[] createCnonce(final String algorithm) { break; case "MD5": default: - length = 8; + length = 16; break; } final byte[] tmp = new byte[length]; From d2c0e086608c978fffc1efb55bbbb14363eea049 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Sun, 3 Nov 2024 15:59:35 +0100 Subject: [PATCH 3/3] Use represent supported algorithms. --- .../client5/http/impl/auth/DigestScheme.java | 137 +++++++++++++----- .../http/impl/auth/TestDigestScheme.java | 9 +- 2 files changed, 104 insertions(+), 42 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java index 7e8a153a9f..0771f9bd3b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java @@ -242,7 +242,7 @@ public Principal getPrincipal() { } @Override - public String generateAuthResponse( + public String generateAuthResponse( final HttpHost host, final HttpRequest request, final HttpContext context) throws AuthenticationException { @@ -317,11 +317,13 @@ private String createDigestResponse(final HttpRequest request) throws Authentica final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset); // If an algorithm is not specified, default to MD5. - final String digAlg = resolveAlgorithm(algorithm); + + DigestAlgorithm digAlg = null; final MessageDigest digester; try { - digester = createMessageDigest(digAlg); + digAlg = DigestAlgorithm.fromString(algorithm == null ? "MD5" : algorithm); + digester = createMessageDigest(digAlg.getBaseAlgorithm()); } catch (final UnsupportedDigestAlgorithmException ex) { throw new AuthenticationException("Unsupported digest algorithm: " + digAlg); } @@ -376,7 +378,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica } // 3.2.2.2: Calculating digest - if ("MD5-sess".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { + if (digAlg.isSessionBased()) { // H( unq(username-value) ":" unq(realm-value) ":" passwd ) // ":" unq(nonce-value) // ":" unq(cnonce-value) @@ -546,10 +548,10 @@ static String formatHex(final byte[] binaryData) { * @return The cnonce value as a byte array. * @since 5.5 */ - static byte[] createCnonce(final String algorithm) { + static byte[] createCnonce(final DigestAlgorithm algorithm) { final SecureRandom rnd = new SecureRandom(); final int length; - switch (algorithm.toUpperCase()) { + switch (algorithm.name().toUpperCase()) { case "SHA-256": case "SHA-512/256": length = 32; @@ -564,16 +566,6 @@ static byte[] createCnonce(final String algorithm) { return tmp; } - /** - * Creates a random cnonce value based on the current time. - * - * @return The cnonce value as String. - * @deprecated Use {@link DigestScheme#createCnonce(String)} instead. - */ - @Deprecated - static byte[] createCnonce() { - return createCnonce("MD5"); // Default to MD5 to maintain compatibility - } private void writeObject(final ObjectOutputStream out) throws IOException { out.defaultWriteObject(); @@ -631,25 +623,100 @@ private boolean containsInvalidABNFChars(final String value) { } /** - * Resolves the specified algorithm name to a standard form based on recognized algorithm suffixes. - *

- * This method translates session-based algorithms (e.g., "-sess" suffix) to their base forms - * for correct MessageDigest usage. If no algorithm is specified or "MD5-sess" is provided, - * it defaults to "MD5". The method also maps "SHA-512-256" to "SHA-512/256" to align with - * Java's naming for SHA-512/256. - *

- * - * @param algorithm the algorithm name to resolve, such as "MD5-sess", "SHA-256-sess", or "SHA-512-256-sess" - * @return the resolved base algorithm name, or the original algorithm name if no mapping applies + * Enum representing supported digest algorithms for HTTP Digest Authentication, + * including session-based variants. */ - private String resolveAlgorithm(final String algorithm) { - if (algorithm == null || algorithm.equalsIgnoreCase("MD5-sess")) { - return "MD5"; - } else if (algorithm.equalsIgnoreCase("SHA-256-sess")) { - return "SHA-256"; - } else if (algorithm.equalsIgnoreCase("SHA-512-256-sess") || algorithm.equalsIgnoreCase("SHA-512-256")) { - return "SHA-512/256"; - } - return algorithm; // If it's already a supported algorithm or doesn't need translating + private enum DigestAlgorithm { + + /** + * MD5 digest algorithm. + */ + MD5("MD5", false), + + /** + * MD5 digest algorithm with session-based variant. + */ + MD5_SESS("MD5", true), + + /** + * SHA-256 digest algorithm. + */ + SHA_256("SHA-256", false), + + /** + * SHA-256 digest algorithm with session-based variant. + */ + SHA_256_SESS("SHA-256", true), + + /** + * SHA-512/256 digest algorithm. + */ + SHA_512_256("SHA-512/256", false), + + /** + * SHA-512/256 digest algorithm with session-based variant. + */ + SHA_512_256_SESS("SHA-512/256", true); + + private final String baseAlgorithm; + private final boolean sessionBased; + + /** + * Constructor for {@code DigestAlgorithm}. + * + * @param baseAlgorithm the base name of the algorithm, e.g., "MD5" or "SHA-256" + * @param sessionBased indicates if the algorithm is session-based (i.e., includes the "-sess" suffix) + */ + DigestAlgorithm(final String baseAlgorithm, final boolean sessionBased) { + this.baseAlgorithm = baseAlgorithm; + this.sessionBased = sessionBased; + } + + /** + * Retrieves the base algorithm name without session suffix. + * + * @return the base algorithm name + */ + private String getBaseAlgorithm() { + return baseAlgorithm; + } + + /** + * Checks if the algorithm is session-based. + * + * @return {@code true} if the algorithm includes the "-sess" suffix, otherwise {@code false} + */ + private boolean isSessionBased() { + return sessionBased; + } + + /** + * Maps a string representation of an algorithm to the corresponding enum constant. + * + * @param algorithm the algorithm name, e.g., "SHA-256" or "SHA-512-256-sess" + * @return the corresponding {@code DigestAlgorithm} constant + * @throws UnsupportedDigestAlgorithmException if the algorithm is unsupported + */ + private static DigestAlgorithm fromString(final String algorithm) { + switch (algorithm.toUpperCase(Locale.ROOT)) { + case "MD5": + return MD5; + case "MD5-SESS": + return MD5_SESS; + case "SHA-256": + return SHA_256; + case "SHA-256-SESS": + return SHA_256_SESS; + case "SHA-512/256": + case "SHA-512-256": + return SHA_512_256; + case "SHA-512-256-SESS": + return SHA_512_256_SESS; + default: + throw new UnsupportedDigestAlgorithmException("Unsupported digest algorithm: " + algorithm); + } + } } + + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java index f5f010c2d6..6b0fc5b59d 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java @@ -178,14 +178,9 @@ void testDigestAuthenticationWithSHA() throws Exception { authscheme.processChallenge(authChallenge, null); Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null)); - final String authResponse = authscheme.generateAuthResponse(host, request, null); - final Map table = parseAuthResponse(authResponse); - Assertions.assertEquals("username", table.get("username")); - Assertions.assertEquals("realm1", table.get("realm")); - Assertions.assertEquals("/", table.get("uri")); - Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce")); - Assertions.assertEquals("8769e82e4e28ecc040b969562b9050580c6d186d", table.get("response")); + Assertions.assertThrows(AuthenticationException.class, () -> + authscheme.generateAuthResponse(host, request, null), "Expected UnsupportedDigestAlgorithmException for unsupported 'SHA' algorithm"); } @Test