1

I can't find any good sample or doc that explains this clearly. I can successfully authenticate with the (older) RestTemplate:

HttpClientBuilder httpClient = HttpClients.custom();
BasicCredentialsProvider provider = new BasicCredentialsProvider();
Credentials cred = new NTCredentials("my-user", "my-password", null, "my-domain");

provider.setCredentials(AuthScope.ANY, cred);
httpClient.setDefaultCredentialsProvider(provider);

HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.httpClient = httpClient.build();

RestTemplate restTemplate = new RestTemplate(request);

restTemplate.getForEntity("https://my.url.com", String.class)

I haven't been able to find a way to pass NTCredentials (or Credentials) to WebClient, have tried

WebClient client = WebClient.builder()
.filter(ExchangeFilterFunctions.basicAuthentication("user", "password"))
.build();

also

WebClient client = WebClient.builder().build();
client.get().headers(h -> h.setBasicAuth("user", "password"))...

But neither approach worked with WebClient

Questions:

  1. How do you do Windows/NTLM authentication with Spring WebClient?
  2. Is there anyway I can get NTLM or Windows auth working without supplying user/password when running in Windows using currrent user context?
Jason
  • 3,844
  • 1
  • 21
  • 40

2 Answers2

1

I have recently been looking into NTLM for Spring WebClient. First I tried it with Apache HttpClient, but got no response on request. I didn't investigate the cause further...

Since I wanted to use the netty HttpClient anyway, I implemented my own ExchangeFilterFunction using the JCIFS implementation (the same as Apache HttpClient uses). Similar to that:

public final class NtlmAuthorizedClientExchangeFilterFunction implements ExchangeFilterFunction {

    private final NtlmPasswordAuthentication ntlmPasswordAuthentication;
    private final boolean doSigning;

    public NtlmAuthorizedClientExchangeFilterFunction(String domain, String username, String password, boolean doSigning, int lmCompatibility) {
        this.ntlmPasswordAuthentication = new NtlmPasswordAuthentication(domain, username, password);
        this.doSigning = doSigning;
        System.setProperty("jcifs.smb.lmCompatibility", Integer.toString(lmCompatibility));
    }


    @Override
    public Mono<ClientResponse> filter(final ClientRequest request, final ExchangeFunction next) {
        NtlmContext ntlmContext = new NtlmContext(ntlmPasswordAuthentication, doSigning);
        try {
            return next.exchange(addNtlmHeader(request, ntlmContext.initSecContext(new byte[0], 0, 0)))
                .publishOn(Schedulers.single()) // this is necessary to make sure that the requests are processed sequential and thus http keep alive is working
                .flatMap(clientResponse -> {
                    List<String> ntlmAuthHeaders = getNtlmAuthHeaders(clientResponse);
                    if (ntlmAuthHeaders.isEmpty()) return Mono.error(...);
                    String ntlmHeader = ntlmAuthHeaders.get(0);
                    if (ntlmHeader.length() <= 5) return Mono.error(...);
                    try {
                        byte[] type2 = Base64.decode(ntlmHeader.substring(5));
                        return next.exchange(addNtlmHeader(request, ntlmContext.initSecContext(type2, 0, type2.length)));
                    } catch (IOException e) {
                        return Mono.error(...);
                    }
                });
        } catch (SmbException e) {
            return Mono.error(...);
        }
    }

    @NotNull
    private static List<String> getNtlmAuthHeaders(ClientResponse clientResponse) {
        List<String> wwwAuthHeaders = clientResponse.headers().header(HttpHeaders.WWW_AUTHENTICATE);
        List<String> ntlmAuthHeaders = wwwAuthHeaders.stream().filter(h -> h.startsWith("NTLM")).sorted(Comparator.comparingInt(String::length)).collect(Collectors.toList());
        return ntlmAuthHeaders;
    }

    private ClientRequest addNtlmHeader(ClientRequest clientRequest, byte[] ntlmPayload) {
        ClientRequest.Builder request = ClientRequest
            .from(clientRequest)
            .header(HttpHeaders.AUTHORIZATION, "NTLM ".concat(Base64.encode(ntlmPayload)));
        return request.build();
    }

I don't have an answer to your second question. My system was a Linux.

spCH
  • 59
  • 1
  • 6
  • Great answer!!! Do you know why it does not work without .publishOn(Schedulers.single()) ?? is it something related to next exchange function and how it works ? – Oleg Maksymuk Aug 31 '23 at 21:34
0
webClientBuilder.filter((request, next) -> {

            final NtlmPasswordAuthentication ntlmPasswordAuthentication =
                    new NtlmPasswordAuthentication(
                            domain,
                            username,
                            password);
            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_PREFIX.concat(Base64.encode(token1)))
                    .build();


            final ExchangeFunction exchangeFunction = ExchangeFunctions.create(singleConnKeepAliveConnector());

            return exchangeFunction
                    .exchange(ntlmChallenge)
                    .flatMap(clientResponse -> {

                        byte[] token2 = ntlm(ctxt, Base64.decode(extractNtlmHeaderValue(clientResponse)));

                        return exchangeFunction.exchange(ClientRequest.from(request)
                                .header(HttpHeaders.AUTHORIZATION, NTLM_PREFIX.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_PREFIX, "");
         
    }

    private ClientHttpConnector singleConnKeepAliveConnector() {

        final HttpClient httpClient = HttpClient.create(
                ConnectionProvider.builder("single-conn").maxConnections(1).build());

        return new ReactorClientHttpConnector(httpClient);
    }

NTLM_PREFIX="NTLM "
Oleg Maksymuk
  • 441
  • 4
  • 10