Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion client/src/main/java/org/asynchttpclient/Dsl.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ public static Realm.Builder realm(Realm prototype) {
.setUseCanonicalHostname(prototype.isUseCanonicalHostname())
.setCustomLoginConfig(prototype.getCustomLoginConfig())
.setLoginContextName(prototype.getLoginContextName())
.setUserhash(prototype.isUserhash());
.setUserhash(prototype.isUserhash())
.setSid(prototype.getSid())
.setMaxIterationCount(prototype.getMaxIterationCount());
// Note: stale is NOT copied — it's challenge-specific, always starts false
}

Expand All @@ -132,4 +134,9 @@ public static Realm.Builder digestAuthRealm(String principal, String password) {
public static Realm.Builder ntlmAuthRealm(String principal, String password) {
return realm(AuthScheme.NTLM, principal, password);
}

public static Realm.Builder scramSha256AuthRealm(String principal, String password) {
return realm(AuthScheme.SCRAM_SHA_256, principal, password);
}

}
34 changes: 31 additions & 3 deletions client/src/main/java/org/asynchttpclient/Realm.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public class Realm {
private final @Nullable String loginContextName;
private final boolean stale;
private final boolean userhash;
private final @Nullable String sid;
private final int maxIterationCount;

private Realm(@Nullable AuthScheme scheme,
@Nullable String principal,
Expand All @@ -93,7 +95,9 @@ private Realm(@Nullable AuthScheme scheme,
@Nullable Map<String, String> customLoginConfig,
@Nullable String loginContextName,
boolean stale,
boolean userhash) {
boolean userhash,
@Nullable String sid,
int maxIterationCount) {

this.scheme = requireNonNull(scheme, "scheme");
this.principal = principal;
Expand All @@ -119,6 +123,8 @@ private Realm(@Nullable AuthScheme scheme,
this.loginContextName = loginContextName;
this.stale = stale;
this.userhash = userhash;
this.sid = sid;
this.maxIterationCount = maxIterationCount;
}

public @Nullable String getPrincipal() {
Expand Down Expand Up @@ -232,6 +238,14 @@ public boolean isUserhash() {
return userhash;
}

public @Nullable String getSid() {
return sid;
}

public int getMaxIterationCount() {
return maxIterationCount;
}

@Override
public String toString() {
return "Realm{" +
Expand Down Expand Up @@ -261,7 +275,7 @@ public String toString() {
}

public enum AuthScheme {
BASIC, DIGEST, NTLM, SPNEGO, KERBEROS
BASIC, DIGEST, NTLM, SPNEGO, KERBEROS, SCRAM_SHA_256
}

/**
Expand Down Expand Up @@ -300,6 +314,8 @@ public static class Builder {
private boolean stale;
private boolean userhash;
private @Nullable String entityBodyHash;
private @Nullable String sid;
private int maxIterationCount = 16_384;

public Builder() {
principal = null;
Expand Down Expand Up @@ -432,6 +448,16 @@ public Builder setEntityBodyHash(@Nullable String entityBodyHash) {
return this;
}

public Builder setSid(@Nullable String sid) {
this.sid = sid;
return this;
}

public Builder setMaxIterationCount(int maxIterationCount) {
this.maxIterationCount = maxIterationCount;
return this;
}

public @Nullable String getQopValue() {
return qop;
}
Expand Down Expand Up @@ -720,7 +746,9 @@ public Realm build() {
customLoginConfig,
loginContextName,
stale,
userhash);
userhash,
sid,
maxIterationCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface TransferListener {
/**
* Invoked every time response's chunk are received.
*
* @param bytes a {@link byte} array
* @param bytes a byte array
*/
void onBytesReceived(byte[] bytes);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.asynchttpclient.netty.request.NettyRequest;
import org.asynchttpclient.netty.timeout.TimeoutsHolder;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.scram.ScramContext;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -126,6 +127,7 @@ public final class NettyResponseFuture<V> implements ListenableFuture<V> {
private boolean allowConnect;
private Realm realm;
private Realm proxyRealm;
private volatile ScramContext scramContext;

public NettyResponseFuture(Request originalRequest,
AsyncHandler<V> asyncHandler,
Expand Down Expand Up @@ -540,6 +542,14 @@ public void setProxyRealm(Realm proxyRealm) {
this.proxyRealm = proxyRealm;
}

public ScramContext getScramContext() {
return scramContext;
}

public void setScramContext(ScramContext scramContext) {
this.scramContext = scramContext;
}

@Override
public String toString() {
return "NettyResponseFuture{" + //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@
import org.asynchttpclient.netty.channel.ChannelManager;
import org.asynchttpclient.netty.request.NettyRequestSender;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.scram.ScramContext;
import org.asynchttpclient.scram.ScramMessageParser;
import org.asynchttpclient.scram.ScramState;
import org.asynchttpclient.util.AuthenticatorUtils;
import org.asynchttpclient.util.NonceCounter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static io.netty.handler.codec.http.HttpHeaderNames.SET_COOKIE;
import static org.asynchttpclient.Dsl.realm;
import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.CONTINUE_100;
Expand Down Expand Up @@ -128,6 +134,14 @@ public boolean exitAfterIntercept(Channel channel, NettyResponseFuture<?> future
processAuthenticationInfo(future, responseHeaders, proxyRealm, true);
}

// Process SCRAM Authentication-Info (RFC 7804 §5)
if (realm != null && realm.getScheme() == Realm.AuthScheme.SCRAM_SHA_256) {
processScramAuthenticationInfo(future, responseHeaders, "Authentication-Info");
}
if (proxyRealm != null && proxyRealm.getScheme() == Realm.AuthScheme.SCRAM_SHA_256) {
processScramAuthenticationInfo(future, responseHeaders, "Proxy-Authentication-Info");
}

return false;
}

Expand Down Expand Up @@ -166,4 +180,36 @@ private void processAuthenticationInfo(NettyResponseFuture<?> future, HttpHeader
}
}
}

private void processScramAuthenticationInfo(NettyResponseFuture<?> future, HttpHeaders responseHeaders,
String headerName) {
ScramContext ctx = future.getScramContext();
if (ctx == null || ctx.getState() != ScramState.CLIENT_FINAL_SENT) {
return;
}

String authInfo = responseHeaders.get(headerName);
if (authInfo == null) {
// RFC 7804 §6: may be in chunked trailers (not supported by AHC)
LOGGER.warn("SCRAM: response without {} header; "
+ "ServerSignature cannot be verified (may be in chunked trailers)", headerName);
return;
}

String data = Realm.Builder.matchParam(authInfo, "data");
if (data == null) {
LOGGER.warn("SCRAM: Authentication-Info header missing data attribute");
return;
}

String serverFinalMsg = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8);
if (ctx.verifyServerFinal(serverFinalMsg)) {
ctx.setState(ScramState.AUTHENTICATED);
LOGGER.debug("SCRAM ServerSignature verified successfully");
} else {
ctx.setState(ScramState.FAILED);
LOGGER.warn("SCRAM ServerSignature verification failed — authentication unsuccessful "
+ "(RFC 7804 §5: MUST consider unsuccessful)");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,20 @@
import io.netty.handler.codec.http2.Http2StreamChannel;
import org.asynchttpclient.ntlm.NtlmEngine;
import org.asynchttpclient.proxy.ProxyServer;
import org.asynchttpclient.scram.ScramContext;
import org.asynchttpclient.scram.ScramException;
import org.asynchttpclient.scram.ScramMessageFormatter;
import org.asynchttpclient.scram.ScramMessageParser;
import org.asynchttpclient.scram.ScramState;
import org.asynchttpclient.spnego.SpnegoEngine;
import org.asynchttpclient.spnego.SpnegoEngineException;
import org.asynchttpclient.util.AuthenticatorUtils;
import org.asynchttpclient.util.NonceCounter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;

import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHENTICATE;
Expand Down Expand Up @@ -214,6 +221,65 @@ public boolean exitAfterHandling407(Channel channel, NettyResponseFuture<?> futu
}
}
break;
case SCRAM_SHA_256:
String scramPrefix = "SCRAM-SHA-256";
String scramHeader = getHeaderWithPrefix(proxyAuthHeaders, scramPrefix);
if (scramHeader == null) {
LOGGER.info("Can't handle 407 with SCRAM realm as Proxy-Authenticate headers don't match");
return false;
}

try {
ScramMessageParser.ScramChallengeParams params = ScramMessageParser.parseWwwAuthenticateScram(scramHeader);
ScramContext ctx = future.getScramContext();

if (ctx == null) {
ctx = new ScramContext(proxyRealm.getPrincipal(), proxyRealm.getPassword(),
params.realm != null ? params.realm : proxyRealm.getRealmName(),
scramPrefix);
ctx.setInitialChallengeParams(params);

String base64Data = Base64.getEncoder().encodeToString(
ctx.getClientFirstMessage().getBytes(StandardCharsets.UTF_8));
String authHeader = ScramMessageFormatter.formatAuthorizationHeader(
ctx.getMechanism(), ctx.getRealmName(), null, base64Data);

requestHeaders.set(PROXY_AUTHORIZATION, authHeader);
future.setScramContext(ctx);
future.setInProxyAuth(false);

} else if (ctx.getState() == ScramState.CLIENT_FIRST_SENT) {
if (params.sid == null) {
LOGGER.warn("SCRAM: missing sid in proxy server-first response");
return false;
}
if (params.data == null) {
LOGGER.warn("SCRAM: missing data in proxy server-first response");
return false;
}

String serverFirstMsg = new String(Base64.getDecoder().decode(params.data), StandardCharsets.UTF_8);
ctx.processServerFirst(serverFirstMsg, proxyRealm.getMaxIterationCount());
ctx.setSid(params.sid);

String clientFinalMsg = ctx.computeClientFinal();
String base64Data = Base64.getEncoder().encodeToString(
clientFinalMsg.getBytes(StandardCharsets.UTF_8));
String authHeader = ScramMessageFormatter.formatAuthorizationHeader(
ctx.getMechanism(), null, params.sid, base64Data);

requestHeaders.set(PROXY_AUTHORIZATION, authHeader);

} else {
LOGGER.warn("SCRAM proxy authentication failed: unexpected 407 in state {}", ctx.getState());
return false;
}
} catch (ScramException e) {
LOGGER.warn("SCRAM proxy authentication failed: {}", e.getMessage());
return false;
}
break;

default:
throw new IllegalStateException("Invalid Authentication scheme " + proxyRealm.getScheme());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture<?>
// We must allow auth handling again.
future.setInAuth(false);
future.setInProxyAuth(false);
future.setScramContext(null);

String originalMethod = request.getMethod();
boolean switchToGet = !originalMethod.equals(GET) &&
Expand Down Expand Up @@ -196,7 +197,8 @@ private static HttpHeaders propagatedHeaders(Request request, Realm realm, boole
headers.remove(CONTENT_TYPE);
}

if (stripAuthorization || (realm != null && realm.getScheme() == AuthScheme.NTLM)) {
if (stripAuthorization || (realm != null && (realm.getScheme() == AuthScheme.NTLM
|| realm.getScheme() == AuthScheme.SCRAM_SHA_256))) {
headers.remove(AUTHORIZATION)
.remove(PROXY_AUTHORIZATION);
}
Expand Down
Loading
Loading