2

I'm using Apache HttpClient to connect to a web server which is running Jetty and using WAFFLE for authentication. I'm having a problem where the server sends three WWW-Authenticate headers (because it does support three authentication schemes) but then on the client, CredentialsProvider is being asked three times for the same credentials.

If I connect to the same server with the built-in Java URL class, it gets the resource without asking me for credentials.

The HTTP client setup is fairly straight-forward:

RequestConfig requestConfig =
    HttpClientUtils.createDefaultRequestConfig();
HttpClientBuilder builder = HttpClients.custom()
    .setDefaultCredentialsProvider(credentialsProvider)
    .setDefaultCookieStore(cookieStore)
    .setDefaultRequestConfig(requestConfig);
return builder.build();

The CredentialsProvider is a custom one which delegates to our own framework for prompting the user for that information.

public class AdaptedCredentialsProvider
    implements CredentialsProvider
{
    private final CredentialsProvider internal =
        new BasicCredentialsProvider();
        //I tried this too:
        //new SystemDefaultCredentialsProvider();

    private final CredentialsPrompt prompt;

    public AdaptedCredentialsProvider(CredentialsPrompt prompt) {
        this.prompt = prompt;
    }

    @Override
    public void setCredentials(AuthScope authscope,
                               Credentials credentials) {
        internal.setCredentials(authscope, credentials);
    }

    @Override
    public Credentials getCredentials(@NotNull AuthScope authScope) {
        System.err.println("Asked for credentials, scheme: " +
                           authScope.getScheme());

        Credentials localCredentials =
            internal.getCredentials(authScope);
        if (localCredentials != null) {
            return localCredentials;
        }

        if (authScope.getHost() != null) {
            OurCredentials credentials = prompt.prompt(
                authScope.getHost(),
                authScope.getPort());

            if (credentials != null) {
                switch (authScope.getScheme()) {
                    case "NEGOTIATE":
                    case "NTLM":
                        return new NTCredentials(
                            credentials.getUsername(),
                            new String(credentials.getPassword()),
                            null, null);

                    default:
                        return new UsernamePasswordCredentials(
                            credentials.getUsername(),
                            new String(credentials.getPassword()));
                }
            }
        }

        return null;
    }

    @Override
    public void clear() {
        internal.clear();
    }
}

A timeline of the requests and responses with the login requests interspersed is interesting to look at...

=== Initial request
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
=== Response
HTTP/1.1 401 Unauthorized
Date: Mon, 27 Apr 2015 02:44:23 GMT
Set-Cookie: JSESSIONID=1lsrefm3yzmmz15n8md7efdc7f;Path=/;Secure
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="Test"
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 289

=== Asked for credentials for host: 192.168.1.162 - scheme: NEGOTIATE

=== GUI checking whether the credentials work...
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
=== Response
HTTP/1.1 401 Unauthorized
Date: Mon, 27 Apr 2015 02:44:30 GMT
Set-Cookie: JSESSIONID=ucsnvsvawp301injh6ijwbywe;Path=/;Secure
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="Test"
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 289
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Authorization: Basic <REDACTED>
=== Response
HTTP/1.1 200 OK
Date: Mon, 27 Apr 2015 02:44:30 GMT
Set-Cookie: JSESSIONID=1la5wb3fje1fy1qxu14weqxaqm;Path=/;Secure
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 0

=== Asked for credentials for host: 192.168.1.162 - scheme: NTLM

=== GUI checking whether the credentials work...
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
=== Response
HTTP/1.1 401 Unauthorized
Date: Mon, 27 Apr 2015 02:44:33 GMT
Set-Cookie: JSESSIONID=5pmtxxhinky91bzoj18yx6zkz;Path=/;Secure
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="Test"
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 289
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Authorization: Basic <REDACTED>
=== Response
HTTP/1.1 200 OK
Date: Mon, 27 Apr 2015 02:44:33 GMT
Set-Cookie: JSESSIONID=16jhmw3fxuhsy1edjtpquiw564;Path=/;Secure
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 0

=== Asked for credentials for host: 192.168.1.162 - scheme: BASIC

=== GUI checking whether the credentials work...
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate    
=== Response
HTTP/1.1 401 Unauthorized
Date: Mon, 27 Apr 2015 02:44:36 GMT
Set-Cookie: JSESSIONID=2yjw3xcoerq5wdtqu1etxur0;Path=/;Secure
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="Test"
Cache-Control: must-revalidate,no-cache,no-store
Content-Type: text/html; charset=ISO-8859-1
Content-Length: 289
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Authorization: Basic <REDACTED>
=== Response
HTTP/1.1 200 OK
Date: Mon, 27 Apr 2015 02:44:36 GMT
Set-Cookie: JSESSIONID=cvf6mvdagknk1mb893u7ylozb;Path=/;Secure
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 0

=== Presumably here it has decided it doesn't support NEGOTIATE so
=== it's going with NTLM.
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Authorization: NTLM <REDACTED>
=== Response
HTTP/1.1 401 Unauthorized
Date: Mon, 27 Apr 2015 02:44:36 GMT
Set-Cookie: JSESSIONID=es21dv6pnktmqoxr9qs52n3e;Path=/;Secure
Expires: Thu, 01 Jan 1970 00:00:00 GMT
WWW-Authenticate: NTLM <REDACTED>
Transfer-Encoding: chunked

0
=== Request
GET /api/noop HTTP/1.1
Host: 192.168.1.162:27443
Connection: Keep-Alive
Accept-Encoding: gzip,deflate
Authorization: NTLM <REDACTED>
=== Response
HTTP/1.1 200 OK
Date: Mon, 27 Apr 2015 02:44:36 GMT
Set-Cookie: JSESSIONID=1nzdiyw90zun218drgkobi3y6g;Path=/;Secure
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Length: 0

It seems like it isn't even using the credentials it's asking for. Eventually it somehow breaks out of this weird cycle and then tries to use them.

It eventually logs in, but how can I stop it asking for the same information three times? (I know I could roll this sort of thing into the CredentialsProvider, but I didn't have to do that with the other API and it seemed like an awkward way to work around it for this one.)

In fact, how can I stop it asking for this information at all? Largely the point of using Negotiate in the first place is to avoid being prompted for a password, but this thing is always prompting me for a password and I don't think it should be.

Update: Half the problem is solved. I discovered that HttpClient didn't support single sign-on at all (bit misleading, IMO, to claim you support NTLM while not supporting the only purpose of using NTLM!) But version 4.4 has experimental support for it.

The experimental support appears to let me login but I still get prompted for a password for the Basic. So I'm back to the question of how to disable this behaviour of asking the credentials provider for every single auth scheme. Ideally, it should just ask for the first one and then fall back to the next one in the sequence.

Hakanai
  • 12,010
  • 10
  • 62
  • 132

1 Answers1

0

First, you should cache the credentials instead of asking the user N times. You can use a cache with a timeout to increase security and avoid keeping passwords in memory for too long.

You also have to make sure that you use the same credentialsProvider and cookieStore for every request.

I'm not sure why Java's URL can connect. Look for Authenticator.setDefault() in your code; without that, Java's URL class can't do any authentication.

[EDIT] The reason why your code asks for a password every time is because you wrote it that way:

  • There is no path through your code which wouldn't ask for a password when the server asks for credentials
  • You're not caching username and password, so it's asking again every time
  • The Oracle Java runtime can create credentials for at least NTLM without asking for a password. Try to investigate how it does that.

Related:

Community
  • 1
  • 1
Aaron Digulla
  • 321,842
  • 108
  • 597
  • 820
  • Authenticator only handles asking for a username and password. The code which handles authentication headers lives directly in the Sun implementation of HttpURLConnection and works without even prompting for a username and password. (I have tried caching the credentials too, but what I found was that it only uses them if the AuthScope is exactly the same. But it's a different AuthScope every time it calls it. In any case, some users say that if it even asks *once*, it isn't working.) – Hakanai Apr 27 '15 at 23:13
  • Which method of `HttpURLConnection` handles the authentication ? – Aaron Digulla Apr 28 '15 at 07:29
  • in the Oracle implementation, getInputStream0 (which is a gigantic block of code) has the check for status 401 and then lots and lots of special cases for handling each possible header and state. – Hakanai Apr 29 '15 at 00:54
  • Took me some time to find the [sources of that class](http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/sun/net/www/protocol/http/HttpURLConnection.java). Looking at the code, I'm seeing that you can enable logging for this class. That should give you an idea why it can/how it connects. – Aaron Digulla Apr 29 '15 at 08:56
  • I'm looking at the loop at line 1364 (link in the previous comment) which iterates over values of `WWW-Authenticate` and I'm wondering what the variables are after the loop. Looking at the response headers in your question, it should end with `inNegotiate = true;` After that, it should create the `AuthenticationHeader` in line 1658, disconnect and restart the loop. I'm wondering where it might take authentication info from. – Aaron Digulla Apr 29 '15 at 09:03
  • When I debug it, what I see it doing is deciding to choose Negotiate. Then it goes into some Negotiator utility which decides that negotiation cannot be done for some reason. Then back to the loop it tries NTLM and ultimately that works. So it seems like it supports some kind of fallback if it can figure out in advance that negotiation won't work. Anyway we sort of know how it connects, the question is why does HttpClient insist on trying all schemes at once, which forces me to provide a password. – Hakanai Apr 29 '15 at 23:52
  • @Trejkaz: Your code is asking the user for a password right before `case "NTLM":` with `prompt.prompt()`. So it doesn't do everything at once, the logic is wrong. – Aaron Digulla Apr 30 '15 at 09:33
  • The logic appears to be correct. The method contract requires me to return a Credentials. To construct one, I have to have a username and password. Therefore, by the time you get to that point in the code, you have to ask the user for that information. By comparison, in the case of java.net.Authenticator, the Authenticator is not even called if NTLM single sign-on succeeds. The parallel behaviour in this code would be that if NTLM single sign-on succeeds, the CredentialsProvider would not be called at all. – Hakanai May 01 '15 at 03:40
  • @Trejkaz What I means is that you have to structure your code differently if you want to avoid asking for the password all the time. It should be possible to create `NTCredentials` without asking for a password; check how `HttpURLConnection` does it. – Aaron Digulla May 04 '15 at 07:39
  • I take it you haven't looked at the constructor of NTCredentials, but I'll spoil it for you - it requires a username and password. – Hakanai May 05 '15 at 03:07
  • You missed my point: `HttpURLConnection` can create a useful credentials objects without asking for a password. I don't know how it does that but that's the next thing that I'd try. – Aaron Digulla May 05 '15 at 07:43
  • yup. I also managed to get NTLM working with no password if you disable Basic auth entirely - but then when the automatic login doesn't work, you would have no fallback, which is worse. – Hakanai May 06 '15 at 23:22
  • Well, if you would cache the login, people would have to enter it just once as long as the program runs. That would bring the problem down to a single annoyance. I understand your frustration that the auth framework of HC isn't working as you want but I can't help you there. I don't know enough about HTTP authentication to give you good advice how to retry a request until you found an authentication that works. – Aaron Digulla May 07 '15 at 07:36