Is there a way to authenticate routes with NTLM Authentication in a GatewayFilter?
I took as a reference the ExchangeFilterFunction from this question which works perfectly with WebClient but I couldn’t make it work in Spring Cloud Gateway.
For the GatewayFilter I took as a reference the WebClientHttpRoutingFilter which utilizes a WebClient.
It returns a 200 OK but the client call hangs.
GatewayFilter
@Component
public class NtlmAuthenticationGatewayFilter
extends AbstractGatewayFilterFactory<NtlmAuthenticationGatewayFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
//URI requestUrl = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); //this is null for some reason in GatewayFilters
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
PathContainer pathContainer = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_PREDICATE_PATH_CONTAINER_ATTR);
URI requestUrl = null;
try {
requestUrl = new URI(route.getUri() + pathContainer.value());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
String scheme = requestUrl.getScheme();
if (isAlreadyRouted(exchange) || (!"http".equals(scheme) && !"https".equals(scheme))) {
return chain.filter(exchange);
}
setAlreadyRouted(exchange);
HttpMethod method = request.getMethod();
boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false);
WebClient webClient = WebClient.builder().filter(new NtlmExchangeFilterFunction(config.getDomain(),
config.getUsername(), config.getPassword())).build();
WebClient.RequestBodySpec bodySpec = webClient
.method(method)
.uri(requestUrl)
.headers(httpHeaders -> {
if (!preserveHost) {
httpHeaders.remove(HttpHeaders.HOST);
}
});
WebClient.RequestHeadersSpec<?> headersSpec = bodySpec.body(BodyInserters.fromDataBuffers(request.getBody()));
return headersSpec.exchangeToMono(Mono::just)
.flatMap(res -> {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().putAll(res.headers().asHttpHeaders());
response.setStatusCode(res.statusCode());
// Defer committing the response until all route filters have run
// Put client response as ServerWebExchange attribute and write
// response later NettyWriteResponseFilter
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
return chain.filter(exchange);
});
};
}
@AllArgsConstructor
@Getter
public static class Config {
String domain;
String username;
String password;
}
}
ExchangeFilterFunction
public class NtlmExchangeFilterFunction implements ExchangeFilterFunction {
private final NtlmPasswordAuthentication ntlmPasswordAuthentication;
public NtlmExchangeFilterFunction(String domain, String username, String password) {
this.ntlmPasswordAuthentication = new NtlmPasswordAuthentication(domain, username, password);
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
final NtlmContext ctxt = new NtlmContext(ntlmPasswordAuthentication, true);
final byte[] token1 = ntlm(ctxt, new byte[0]);
final ClientRequest ntlmChallenge = ClientRequest.from(request)
.header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(token1)))
.build();
return next
.exchange(ntlmChallenge)
.publishOn(Schedulers.single())
.flatMap(clientResponse -> {
byte[] token2 = ntlm(ctxt, Base64.decode(extractNtlmHeaderValue(clientResponse)));
return next.exchange(ClientRequest.from(request)
.header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(token2)))
.build());
});
}
private byte[] ntlm(NtlmContext ctxt, byte[] token) {
try {
return ctxt.initSecContext(token, 0, token.length);
} catch (SmbException e) {
throw new RuntimeException(e);
}
}
private String extractNtlmHeaderValue(ClientResponse response) {
return response
.headers()
.header(HttpHeaders.WWW_AUTHENTICATE).get(0).replace("NTLM ", "");
}
}
The second option is to try to adjust ExchangeFilterFuction’s logic to the GatewayFilter but I’m not sure how it can be done. I don’t know how to pass the second token to the request.
public class NtlmAuthenticationGatewayFilter
extends AbstractGatewayFilterFactory<NtlmAuthenticationGatewayFilter.Config> {
@Override
public GatewayFilter apply(NtlmAuthenticationGatewayFilter.Config config) {
return (exchange, chain) -> {
final NtlmContext ctxt = new NtlmContext(new NtlmPasswordAuthentication(config.getDomain(),
config.getUsername(), config.getPassword()), true);
final byte[] token1 = ntlm(ctxt, new byte[0]);
ServerHttpRequest request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders
.add(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(token1)))).build();
return chain.filter(exchange.mutate().request(request).build())
.publishOn(Schedulers.single())
.then(Mono.defer(() -> {
ServerHttpResponse response = exchange.getResponse();
byte[] token2 = ntlm(ctxt, Base64.decode(extractNtlmHeaderValue(response)));
ServerHttpRequest request2 = exchange.getRequest().mutate().header(HttpHeaders.AUTHORIZATION,
"NTLM ".concat(Base64.encode(token2))).build();
return chain.filter(exchange.mutate().request(request2).build());
}));
};
}
private byte[] ntlm(NtlmContext ctxt, byte[] token) {
try {
return ctxt.initSecContext(token, 0, token.length);
} catch (SmbException e) {
throw new RuntimeException(e);
}
}
private String extractNtlmHeaderValue(ServerHttpResponse response) {
return response.getHeaders().get(HttpHeaders.WWW_AUTHENTICATE).get(0).replace("NTLM ", "");
}
@AllArgsConstructor
@Getter
public static class Config {
String domain;
String username;
String password;
}
}
Any ideas?