Skip to content

Commit 4168dd1

Browse files
hyperxpropratt4
andauthored
Implement full RFC 7616 HTTP Digest Authentication compliance (#2148)
Motivation: Achieve full RFC 7616 compliance for HTTP Digest Authentication — supporting stale nonce recovery, nonce count tracking, userhash, Authentication-Info processing, multiple challenge negotiation, algorithm-aware auth-int, and Proxy-Authenticate parity. Modification: - Add stale, userhash, entityBodyHash fields to Realm/Realm.Builder; replace quoted-only match() with matchParam() supporting both quoted and unquoted header values; fix parseProxyAuthenticateHeader to match parseWWWAuthenticateHeader parity for charset, qop, stale, and userhash. - Add NonceCounter for thread-safe nc tracking, selectBestDigestChallenge() for multi-challenge negotiation, computeUserhash()/computeRspAuth() helpers; wire stale-nonce retry, nc increment, auth-int body hashing, and Authentication-Info nextnonce/rspauth processing into both 401/407 interceptors via shared NonceCounter. Result: Fixes #2068 --------- Co-authored-by: Pratik Katti <90851204+pratt4@users.noreply.github.com>
1 parent d4a7e7d commit 4168dd1

16 files changed

+2208
-147
lines changed

client/src/main/java/org/asynchttpclient/Dsl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ public static Realm.Builder realm(Realm prototype) {
112112
.setServicePrincipalName(prototype.getServicePrincipalName())
113113
.setUseCanonicalHostname(prototype.isUseCanonicalHostname())
114114
.setCustomLoginConfig(prototype.getCustomLoginConfig())
115-
.setLoginContextName(prototype.getLoginContextName());
115+
.setLoginContextName(prototype.getLoginContextName())
116+
.setUserhash(prototype.isUserhash());
117+
// Note: stale is NOT copied — it's challenge-specific, always starts false
116118
}
117119

118120
public static Realm.Builder realm(AuthScheme scheme, String principal, String password) {

client/src/main/java/org/asynchttpclient/Realm.java

Lines changed: 183 additions & 56 deletions
Large diffs are not rendered by default.

client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,22 @@
3232
import org.asynchttpclient.netty.channel.ChannelManager;
3333
import org.asynchttpclient.netty.request.NettyRequestSender;
3434
import org.asynchttpclient.proxy.ProxyServer;
35+
import org.asynchttpclient.util.AuthenticatorUtils;
36+
import org.asynchttpclient.util.NonceCounter;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
3539

3640
import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
41+
import static org.asynchttpclient.Dsl.realm;
3742
import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.CONTINUE_100;
3843
import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.OK_200;
3944
import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.PROXY_AUTHENTICATION_REQUIRED_407;
4045
import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.UNAUTHORIZED_401;
4146

4247
public class Interceptors {
4348

49+
private static final Logger LOGGER = LoggerFactory.getLogger(Interceptors.class);
50+
4451
private final AsyncHttpClientConfig config;
4552
private final Unauthorized401Interceptor unauthorized401Interceptor;
4653
private final ProxyUnauthorized407Interceptor proxyUnauthorized407Interceptor;
@@ -50,13 +57,15 @@ public class Interceptors {
5057
private final ResponseFiltersInterceptor responseFiltersInterceptor;
5158
private final boolean hasResponseFilters;
5259
private final ClientCookieDecoder cookieDecoder;
60+
private final NonceCounter nonceCounter;
5361

5462
public Interceptors(AsyncHttpClientConfig config,
5563
ChannelManager channelManager,
5664
NettyRequestSender requestSender) {
5765
this.config = config;
58-
unauthorized401Interceptor = new Unauthorized401Interceptor(channelManager, requestSender);
59-
proxyUnauthorized407Interceptor = new ProxyUnauthorized407Interceptor(channelManager, requestSender);
66+
nonceCounter = new NonceCounter();
67+
unauthorized401Interceptor = new Unauthorized401Interceptor(channelManager, requestSender, nonceCounter);
68+
proxyUnauthorized407Interceptor = new ProxyUnauthorized407Interceptor(channelManager, requestSender, nonceCounter);
6069
continue100Interceptor = new Continue100Interceptor(requestSender);
6170
redirect30xInterceptor = new Redirect30xInterceptor(channelManager, config, requestSender);
6271
connectSuccessInterceptor = new ConnectSuccessInterceptor(channelManager, requestSender);
@@ -109,6 +118,52 @@ public boolean exitAfterIntercept(Channel channel, NettyResponseFuture<?> future
109118
if (httpRequest.method() == HttpMethod.CONNECT && statusCode == OK_200) {
110119
return connectSuccessInterceptor.exitAfterHandlingConnect(channel, future, request, proxyServer);
111120
}
121+
122+
// Process Authentication-Info / Proxy-Authentication-Info headers (RFC 7616 Section 3.5)
123+
if (realm != null && realm.getScheme() == Realm.AuthScheme.DIGEST) {
124+
processAuthenticationInfo(future, responseHeaders, realm, false);
125+
}
126+
Realm proxyRealm = future.getProxyRealm();
127+
if (proxyRealm != null && proxyRealm.getScheme() == Realm.AuthScheme.DIGEST) {
128+
processAuthenticationInfo(future, responseHeaders, proxyRealm, true);
129+
}
130+
112131
return false;
113132
}
133+
134+
private void processAuthenticationInfo(NettyResponseFuture<?> future, HttpHeaders responseHeaders,
135+
Realm currentRealm, boolean proxy) {
136+
String headerName = proxy ? "Proxy-Authentication-Info" : "Authentication-Info";
137+
String authInfoHeader = responseHeaders.get(headerName);
138+
if (authInfoHeader == null) {
139+
return;
140+
}
141+
142+
String nextnonce = Realm.Builder.matchParam(authInfoHeader, "nextnonce");
143+
if (nextnonce != null) {
144+
// Rotate to the new nonce
145+
String oldNonce = currentRealm.getNonce();
146+
if (oldNonce != null) {
147+
nonceCounter.reset(oldNonce);
148+
}
149+
Realm newRealm = realm(currentRealm)
150+
.setNonce(nextnonce)
151+
.setNc("00000001")
152+
.build();
153+
if (proxy) {
154+
future.setProxyRealm(newRealm);
155+
} else {
156+
future.setRealm(newRealm);
157+
}
158+
LOGGER.debug("Rotated to nextnonce from {} header", headerName);
159+
}
160+
161+
String rspauth = Realm.Builder.matchParam(authInfoHeader, "rspauth");
162+
if (rspauth != null) {
163+
String expectedRspauth = AuthenticatorUtils.computeRspAuth(currentRealm);
164+
if (!rspauth.equalsIgnoreCase(expectedRspauth)) {
165+
LOGGER.warn("Server rspauth mismatch: expected={}, got={}", expectedRspauth, rspauth);
166+
}
167+
}
168+
}
114169
}

client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.asynchttpclient.proxy.ProxyServer;
3535
import org.asynchttpclient.spnego.SpnegoEngine;
3636
import org.asynchttpclient.spnego.SpnegoEngineException;
37+
import org.asynchttpclient.util.AuthenticatorUtils;
38+
import org.asynchttpclient.util.NonceCounter;
3739
import org.slf4j.Logger;
3840
import org.slf4j.LoggerFactory;
3941

@@ -44,6 +46,7 @@
4446
import static org.asynchttpclient.Dsl.realm;
4547
import static org.asynchttpclient.util.AuthenticatorUtils.NEGOTIATE;
4648
import static org.asynchttpclient.util.AuthenticatorUtils.getHeaderWithPrefix;
49+
import static org.asynchttpclient.util.AuthenticatorUtils.selectBestDigestChallenge;
4750
import static org.asynchttpclient.util.HttpConstants.Methods.CONNECT;
4851

4952
public class ProxyUnauthorized407Interceptor {
@@ -52,20 +55,17 @@ public class ProxyUnauthorized407Interceptor {
5255

5356
private final ChannelManager channelManager;
5457
private final NettyRequestSender requestSender;
58+
private final NonceCounter nonceCounter;
5559

56-
ProxyUnauthorized407Interceptor(ChannelManager channelManager, NettyRequestSender requestSender) {
60+
ProxyUnauthorized407Interceptor(ChannelManager channelManager, NettyRequestSender requestSender, NonceCounter nonceCounter) {
5761
this.channelManager = channelManager;
5862
this.requestSender = requestSender;
63+
this.nonceCounter = nonceCounter;
5964
}
6065

6166
public boolean exitAfterHandling407(Channel channel, NettyResponseFuture<?> future, HttpResponse response, Request request,
6267
ProxyServer proxyServer, HttpRequest httpRequest) {
6368

64-
if (future.isAndSetInProxyAuth(true)) {
65-
LOGGER.info("Can't handle 407 as auth was already performed");
66-
return false;
67-
}
68-
6969
Realm proxyRealm = future.getProxyRealm();
7070

7171
if (proxyRealm == null) {
@@ -80,6 +80,81 @@ public boolean exitAfterHandling407(Channel channel, NettyResponseFuture<?> futu
8080
return false;
8181
}
8282

83+
// For DIGEST, check stale before blocking on isAndSetInProxyAuth
84+
if (proxyRealm.getScheme() == AuthScheme.DIGEST) {
85+
String digestHeader = selectBestDigestChallenge(proxyAuthHeaders);
86+
if (digestHeader == null) {
87+
LOGGER.info("Can't handle 407 with Digest realm as Proxy-Authenticate headers don't match");
88+
return false;
89+
}
90+
Realm.Builder realmBuilder = realm(proxyRealm)
91+
.setUri(request.getUri())
92+
.setMethodName(request.getMethod())
93+
.setUsePreemptiveAuth(true)
94+
.parseProxyAuthenticateHeader(digestHeader);
95+
96+
boolean isStale = realmBuilder.isStale();
97+
Realm previousRealm = future.getProxyRealm();
98+
boolean alreadyRetriedStale = previousRealm != null && previousRealm.isStale();
99+
100+
if (isStale && !alreadyRetriedStale) {
101+
// First stale response: allow retry by resetting inProxyAuth
102+
LOGGER.debug("Proxy indicated stale nonce, retrying with new nonce");
103+
future.setInProxyAuth(false);
104+
if (proxyRealm.getNonce() != null) {
105+
nonceCounter.reset(proxyRealm.getNonce());
106+
}
107+
} else if (future.isAndSetInProxyAuth(true)) {
108+
LOGGER.info("Can't handle 407 as auth was already performed");
109+
return false;
110+
}
111+
112+
// Set nc from counter
113+
String nonce = realmBuilder.getNonceValue();
114+
if (nonce != null) {
115+
realmBuilder.setNc(nonceCounter.nextNc(nonce));
116+
}
117+
118+
// Handle auth-int
119+
if ("auth-int".equals(realmBuilder.getQopValue())) {
120+
String bodyHash = AuthenticatorUtils.computeBodyHash(request, proxyRealm);
121+
realmBuilder.setEntityBodyHash(bodyHash);
122+
}
123+
124+
Realm newDigestRealm = realmBuilder.build();
125+
future.setProxyRealm(newDigestRealm);
126+
127+
future.setChannelState(ChannelState.NEW);
128+
HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders());
129+
130+
RequestBuilder nextRequestBuilder = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders);
131+
if (future.getCurrentRequest().getUri().isSecured()) {
132+
nextRequestBuilder.setMethod(CONNECT);
133+
}
134+
final Request nextRequest = nextRequestBuilder.build();
135+
136+
LOGGER.debug("Sending proxy authentication to {}", request.getUri());
137+
if (channel instanceof Http2StreamChannel) {
138+
channel.close();
139+
requestSender.sendNextRequest(nextRequest, future);
140+
} else if (future.isKeepAlive()
141+
&& !HttpUtil.isTransferEncodingChunked(httpRequest)
142+
&& !HttpUtil.isTransferEncodingChunked(response)) {
143+
future.setConnectAllowed(true);
144+
future.setReuseChannel(true);
145+
requestSender.drainChannelAndExecuteNextRequest(channel, future, nextRequest);
146+
} else {
147+
channelManager.closeChannel(channel);
148+
requestSender.sendNextRequest(nextRequest, future);
149+
}
150+
return true;
151+
}
152+
153+
if (future.isAndSetInProxyAuth(true)) {
154+
LOGGER.info("Can't handle 407 as auth was already performed");
155+
return false;
156+
}
157+
83158
// FIXME what's this???
84159
future.setChannelState(ChannelState.NEW);
85160
HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders());
@@ -92,37 +167,16 @@ public boolean exitAfterHandling407(Channel channel, NettyResponseFuture<?> futu
92167
}
93168

94169
if (proxyRealm.isUsePreemptiveAuth()) {
95-
// FIXME do we need this, as future.getAndSetAuth
96-
// was tested above?
97-
// auth was already performed, most likely auth
98-
// failed
99170
LOGGER.info("Can't handle 407 with Basic realm as auth was preemptive and already performed");
100171
return false;
101172
}
102173

103-
// FIXME do we want to update the realm, or directly
104-
// set the header?
105174
Realm newBasicRealm = realm(proxyRealm)
106175
.setUsePreemptiveAuth(true)
107176
.build();
108177
future.setProxyRealm(newBasicRealm);
109178
break;
110179

111-
case DIGEST:
112-
String digestHeader = getHeaderWithPrefix(proxyAuthHeaders, "Digest");
113-
if (digestHeader == null) {
114-
LOGGER.info("Can't handle 407 with Digest realm as Proxy-Authenticate headers don't match");
115-
return false;
116-
}
117-
Realm newDigestRealm = realm(proxyRealm)
118-
.setUri(request.getUri())
119-
.setMethodName(request.getMethod())
120-
.setUsePreemptiveAuth(true)
121-
.parseProxyAuthenticateHeader(digestHeader)
122-
.build();
123-
future.setProxyRealm(newDigestRealm);
124-
break;
125-
126180
case NTLM:
127181
String ntlmHeader = getHeaderWithPrefix(proxyAuthHeaders, "NTLM");
128182
if (ntlmHeader == null) {

0 commit comments

Comments
 (0)