Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

401 when Server responds with multiple WWW-Authenticate due to HTTPCLIENT-1489 #133

Open
micheljung opened this issue Feb 26, 2019 · 1 comment

Comments

@micheljung
Copy link
Contributor

Since this project uses Apache's httpclient 4.3.3, it suffers from https://jira.apache.org/jira/browse/HTTPCLIENT-1489

The issue has been resolved in httpclient 5.0 Alpha1 but users can't just upgrade because the API is incompatible.

If I come up with a workaround, I'll post it here or create a pull request.

@micheljung
Copy link
Contributor Author

Here comes my workaround. Specify a class:

import org.apache.http.FormattedHeader;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AUTH;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.impl.client.TargetAuthenticationStrategy;
import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args;
import org.apache.http.util.CharArrayBuffer;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * Workaround for <a href="https://jira.apache.org/jira/browse/HTTPCLIENT-1489">HTTPCLIENT-1489</a>.
 */
public class HttpClient1489TargetAuthenticationStrategy extends TargetAuthenticationStrategy {

    private final String challengeName;

    public HttpClient1489TargetAuthenticationStrategy(String challengeName) {
        this.challengeName = challengeName;
    }

    /**
     * Generates a map of challenge auth-scheme =&gt; Header entries.
     *
     * @return map: key=lower-cased auth-scheme name, value=Header that contains the challenge
     */
    @Override
    public Map<String, Header> getChallenges(
            final HttpHost authhost,
            final HttpResponse response,
            final HttpContext context
    ) throws MalformedChallengeException {

        Args.notNull(response, "HTTP response");
        final Header[] headers = filterChallenge(response.getHeaders(AUTH.WWW_AUTH), challengeName);
        final Map<String, Header> map = new HashMap<>(headers.length);

        for (final Header header : headers) {
            final CharArrayBuffer buffer;
            int pos;
            if (header instanceof FormattedHeader) {
                buffer = ((FormattedHeader) header).getBuffer();
                pos = ((FormattedHeader) header).getValuePos();
            } else {
                final String s = header.getValue();
                if (s == null) {
                    throw new MalformedChallengeException("Header value is null");
                }
                buffer = new CharArrayBuffer(s.length());
                buffer.append(s);
                pos = 0;
            }
            while (pos < buffer.length() && HTTP.isWhitespace(buffer.charAt(pos))) {
                pos++;
            }
            final int beginIndex = pos;
            while (pos < buffer.length() && !HTTP.isWhitespace(buffer.charAt(pos))) {
                pos++;
            }
            final int endIndex = pos;
            final String s = buffer.substring(beginIndex, endIndex);
            map.put(s.toLowerCase(Locale.ROOT), header);
        }
        return map;
    }

    /**
     * Removes all but the specified {@code WWW-Authenticate} challenge.
     * <p>
     * For instance, the header:
     * <pre>
     *   WWW-Authenticate: X-MobileMe-AuthToken realm="Newcastle", Basic realm="Newcastle"
     * </pre>
     * becomes:
     * <pre>
     *   WWW-Authenticate: X-MobileMe-AuthToken realm="Newcastle"
     * </pre>
     * if this class has been instantiated with "X-MobileMe-AuthToken" or:
     * <pre>
     *   WWW-Authenticate: Basic realm="Newcastle
     * </pre>
     * if this class has been instantiated with "Basic". An exception is thrown if the specified
     * challenge could not be found.
     * </p>
     */
    private Header[] filterChallenge(Header[] headers) {
      // CAVEAT: Calling header.getElements() here is prone to error if the base64 string ends with "="
      return Arrays.stream(headers)
              .map(header -> Arrays.stream(header.getValue().split(","))
                      .map(String::trim)
                      .filter(headerElement -> headerElement
                              .toLowerCase(Locale.US)
                              .startsWith(challengeName.toLowerCase(Locale.US)))
                      .findFirst()
                      .orElseThrow(() -> new IllegalArgumentException("There must be exactly one challenge with name '"
                              + challengeName + "' in headers: " + Arrays.toString(headers)))
              )
              .map(headerElement -> new BasicHeader(AUTH.WWW_AUTH, headerElement))
              .toArray(Header[]::new);
    }
}

And create your own HttpClient, with the customized authentication strategy set:

  private static HttpClient buildHttpClient() {
    HttpClientBuilder builder = HttpClientBuilder.create();
    Lookup<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create()
            .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory(true)).build();
    builder.setDefaultAuthSchemeRegistry(authSchemeRegistry);

    BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
    credentialsProvider.setCredentials(new AuthScope(null, -1, null), new NullCredentials());
    builder.setDefaultCredentialsProvider(credentialsProvider);

    builder.setTargetAuthenticationStrategy(new HttpClient1489TargetAuthenticationStrategy("negotiate"));

    return builder.build();
  }

  private static class NullCredentials implements Credentials {

    @Override
    public Principal getUserPrincipal() {
      return null;
    }

    @Override
    public String getPassword() {
      return null;
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant