DigestAuthRfc7616Test.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;
import io.github.artsok.RepeatedIfExceptionsTest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.asynchttpclient.test.ExtendedDigestAuthenticator;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.jupiter.api.BeforeEach;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.asynchttpclient.Dsl.asyncHttpClient;
import static org.asynchttpclient.Dsl.digestAuthRealm;
import static org.asynchttpclient.test.TestUtils.ADMIN;
import static org.asynchttpclient.test.TestUtils.USER;
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class DigestAuthRfc7616Test extends AbstractBasicTest {
@Override
@BeforeEach
public void setUpGlobal() throws Exception {
server = new Server();
ServerConnector connector = addHttpConnector(server);
server.setHandler(configureHandler());
server.start();
port1 = connector.getLocalPort();
logger.info("Local HTTP server started successfully");
}
@Override
public AbstractHandler configureHandler() throws Exception {
return new StaleNonceHandler();
}
// Phase 2: Stale nonce handling
@RepeatedIfExceptionsTest(repeats = 5)
public void staleNonceRetry() throws Exception {
server.stop();
server = new Server();
ServerConnector connector = addHttpConnector(server);
server.setHandler(new StaleNonceHandler());
server.start();
port1 = connector.getLocalPort();
try (AsyncHttpClient client = asyncHttpClient()) {
Future<Response> f = client.prepareGet("http://localhost:" + port1 + '/')
.setRealm(digestAuthRealm(USER, ADMIN).setRealmName("MyRealm").build())
.execute();
Response resp = f.get(60, TimeUnit.SECONDS);
assertNotNull(resp);
assertEquals(HttpServletResponse.SC_OK, resp.getStatusCode());
assertNotNull(resp.getHeader("X-Auth"));
}
}
// Phase 5: Multiple challenges - select best algorithm
@RepeatedIfExceptionsTest(repeats = 5)
public void multipleChallengesSelectsBest() throws Exception {
server.stop();
server = new Server();
ServerConnector connector = addHttpConnector(server);
server.setHandler(new MultipleChallengeHandler());
server.start();
port1 = connector.getLocalPort();
try (AsyncHttpClient client = asyncHttpClient()) {
Future<Response> f = client.prepareGet("http://localhost:" + port1 + '/')
.setRealm(digestAuthRealm(USER, ADMIN).setRealmName("MyRealm").build())
.execute();
Response resp = f.get(60, TimeUnit.SECONDS);
assertNotNull(resp);
assertEquals(HttpServletResponse.SC_OK, resp.getStatusCode());
// Verify the client picked SHA-256
String authHeader = resp.getHeader("X-Auth");
assertNotNull(authHeader);
}
}
// Phase 7: Authentication-Info with nextnonce
@RepeatedIfExceptionsTest(repeats = 5)
public void authenticationInfoNextnonce() throws Exception {
server.stop();
server = new Server();
ServerConnector connector = addHttpConnector(server);
server.setHandler(new NextNonceHandler());
server.start();
port1 = connector.getLocalPort();
try (AsyncHttpClient client = asyncHttpClient()) {
Future<Response> f = client.prepareGet("http://localhost:" + port1 + '/')
.setRealm(digestAuthRealm(USER, ADMIN).setRealmName("MyRealm").build())
.execute();
Response resp = f.get(60, TimeUnit.SECONDS);
assertNotNull(resp);
assertEquals(HttpServletResponse.SC_OK, resp.getStatusCode());
}
}
/**
* Handler that sends stale=true on the second 401, forcing a nonce refresh.
*/
private static class StaleNonceHandler extends AbstractHandler {
private final String realm = "MyRealm";
private final ExtendedDigestAuthenticator authenticator = new ExtendedDigestAuthenticator();
private final AtomicInteger requestCount = new AtomicInteger(0);
private volatile String currentNonce = ExtendedDigestAuthenticator.newNonce();
@Override
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
int count = requestCount.incrementAndGet();
String authz = request.getHeader("Authorization");
if (authz == null || !authz.startsWith("Digest ")) {
// First request: no auth ��� challenge
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("WWW-Authenticate",
authenticator.createAuthenticateHeader(realm, currentNonce, false));
response.getOutputStream().close();
return;
}
// Second request has auth - simulate stale nonce on first auth attempt
String credentials = authz.substring("Digest ".length());
Map<String, String> params = ExtendedDigestAuthenticator.parseCredentials(credentials);
if (count == 2) {
// Simulate stale nonce - send new nonce with stale=true
currentNonce = ExtendedDigestAuthenticator.newNonce();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("WWW-Authenticate",
authenticator.createAuthenticateHeader(realm, currentNonce, true));
response.getOutputStream().close();
return;
}
// Third request - validate with new nonce
if (!USER.equals(params.get("username"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
boolean ok = ExtendedDigestAuthenticator.validateDigest(request.getMethod(), credentials, ADMIN);
if (!ok) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
response.addHeader("X-Auth", authz);
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
/**
* Handler that sends multiple Digest challenges with different algorithms.
*/
private static class MultipleChallengeHandler extends AbstractHandler {
private final String realm = "MyRealm";
private final String nonce = ExtendedDigestAuthenticator.newNonce();
private final ExtendedDigestAuthenticator sha256Auth = new ExtendedDigestAuthenticator("SHA-256");
@Override
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String authz = request.getHeader("Authorization");
if (authz == null || !authz.startsWith("Digest ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// Send multiple challenges - SHA-256 first (preferred), then MD5
response.addHeader("WWW-Authenticate",
sha256Auth.createAuthenticateHeader(realm, nonce, false));
response.addHeader("WWW-Authenticate",
"Digest realm=\"" + realm + "\", nonce=\"" + nonce + "\", qop=\"auth\"");
response.getOutputStream().close();
return;
}
String credentials = authz.substring("Digest ".length());
Map<String, String> params = ExtendedDigestAuthenticator.parseCredentials(credentials);
if (!USER.equals(params.get("username"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
boolean ok = ExtendedDigestAuthenticator.validateDigest(request.getMethod(), credentials, ADMIN);
if (!ok) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
response.addHeader("X-Auth", authz);
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
/**
* Handler that sends Authentication-Info header with nextnonce.
*/
private static class NextNonceHandler extends AbstractHandler {
private final String realm = "MyRealm";
private final ExtendedDigestAuthenticator authenticator = new ExtendedDigestAuthenticator();
private final String nonce = ExtendedDigestAuthenticator.newNonce();
@Override
public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String authz = request.getHeader("Authorization");
if (authz == null || !authz.startsWith("Digest ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("WWW-Authenticate",
authenticator.createAuthenticateHeader(realm, nonce, false));
response.getOutputStream().close();
return;
}
String credentials = authz.substring("Digest ".length());
Map<String, String> params = ExtendedDigestAuthenticator.parseCredentials(credentials);
if (!USER.equals(params.get("username"))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
boolean ok = ExtendedDigestAuthenticator.validateDigest(request.getMethod(), credentials, ADMIN);
if (!ok) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().close();
return;
}
// Send Authentication-Info with nextnonce
String nextNonce = ExtendedDigestAuthenticator.newNonce();
response.addHeader("Authentication-Info", "nextnonce=\"" + nextNonce + "\"");
response.addHeader("X-Auth", authz);
response.setStatus(HttpServletResponse.SC_OK);
response.getOutputStream().flush();
response.getOutputStream().close();
}
}
}