diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
index 24f29dd670..b5e85f192a 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java
@@ -33,6 +33,7 @@
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.UserTokenHandler;
import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
@@ -153,6 +154,12 @@ public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup.
+ *
+ */
+package org.apache.hc.client5.testing.sync;
+
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.AuthenticationException;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.KerberosConfig;
+import org.apache.hc.client5.http.auth.KerberosConfig.Option;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
+import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
+import org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.utils.Base64;
+import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.io.HttpRequestHandler;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Timeout;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+/**
+ * Tests for {@link SPNegoScheme}.
+ */
+public class TestMutualSpnegoScheme extends AbstractIntegrationTestBase {
+
+ protected TestMutualSpnegoScheme() {
+ super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
+ }
+
+ public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
+
+ private static final String GOOD_TOKEN = "GOOD_TOKEN";
+ private static final byte[] GOOD_TOKEN_BYTES = GOOD_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] GOOD_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_TOKEN_BYTES);
+ private static final String GOOD_TOKEN_B64 = new String(GOOD_TOKEN_B64_BYTES);
+
+ private static final String NO_TOKEN = "";
+ private static final byte[] NO_TOKEN_BYTES = NO_TOKEN.getBytes(StandardCharsets.UTF_8);
+
+ private static final String GOOD_MUTUAL_AUTH_TOKEN = "GOOD_MUTUAL_AUTH_TOKEN";
+ private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_BYTES = GOOD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_MUTUAL_AUTH_TOKEN_BYTES);
+
+ private static final String BAD_MUTUAL_AUTH_TOKEN = "BAD_MUTUAL_AUTH_TOKEN";
+ private static final byte[] BAD_MUTUAL_AUTH_TOKEN_BYTES = BAD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8);
+ private static final byte[] BAD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(BAD_MUTUAL_AUTH_TOKEN_BYTES);
+
+ static KerberosConfig MUTUAL_KERBEROS_CONFIG = KerberosConfig.custom().setRequestMutualAuth(Option.ENABLE).build();
+
+ private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+
+ private static final List SPNEGO_SCHEME_PRIORITY =
+ Collections.unmodifiableList(
+ Arrays.asList(StandardAuthScheme.SPNEGO,
+ StandardAuthScheme.BEARER,
+ StandardAuthScheme.DIGEST,
+ StandardAuthScheme.BASIC));
+
+ @Override
+ protected final List getSchemePriority() {
+ return SPNEGO_SCHEME_PRIORITY;
+ }
+ }
+
+ final AuthenticationStrategy spnegoAuthenticationStrategy = new SpnegoAuthenticationStrategy();
+
+ final CredentialsProvider jaasCredentialsProvider = CredentialsProviderBuilder.create()
+ .add(new AuthScope(null, null, -1, null, null), new UseJaasCredentials())
+ .build();
+
+ /**
+ * This service will continue to ask for authentication.
+ */
+ private static class PleaseNegotiateService implements HttpRequestHandler {
+
+ @Override
+ public void handle(
+ final ClassicHttpRequest request,
+ final ClassicHttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ response.setCode(HttpStatus.SC_UNAUTHORIZED);
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " blablabla"));
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth required "));
+ }
+ }
+
+ /**
+ * This service implements a normal mutualAuth flow
+ */
+ private static class SPNEGOMutualService implements HttpRequestHandler {
+
+ int callCount = 1;
+ final boolean sendMutualToken;
+ final byte[] encodedMutualAuthToken;
+
+ SPNEGOMutualService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken) {
+ this.sendMutualToken = sendMutualToken;
+ this.encodedMutualAuthToken = encodedMutualAuthToken;
+ }
+
+ @Override
+ public void handle(
+ final ClassicHttpRequest request,
+ final ClassicHttpResponse response,
+ final HttpContext context) throws HttpException, IOException {
+ if (callCount == 1) {
+ callCount++;
+ // Send the empty challenge
+ response.setCode(HttpStatus.SC_UNAUTHORIZED);
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO));
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth required "));
+ } else if (callCount == 2) {
+ callCount++;
+ if (request.getHeader("Authorization").getValue().contains(GOOD_TOKEN_B64)) {
+ response.setCode(HttpStatus.SC_OK);
+ if (sendMutualToken) {
+ response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " " + new String(encodedMutualAuthToken)));
+ }
+ response.addHeader(new BasicHeader("Connection", "Keep-Alive"));
+ response.setEntity(new StringEntity("auth successful "));
+ } else {
+ response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
+ * NegotatieScheme with a custom GSSManager that does not require any Jaas or
+ * Kerberos configuration.
+ *
+ */
+ private static class NegotiateSchemeWithMockGssManager extends MutualSpnegoScheme {
+
+ final GSSManager manager = Mockito.mock(GSSManager.class);
+ final GSSName name = Mockito.mock(GSSName.class);
+ final GSSContext context = Mockito.mock(GSSContext.class);
+
+ NegotiateSchemeWithMockGssManager() throws Exception {
+ super(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ Mockito.when(context.initSecContext(
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn("12345678".getBytes());
+ Mockito.when(manager.createName(
+ ArgumentMatchers.anyString(), ArgumentMatchers.any()))
+ .thenReturn(name);
+ Mockito.when(manager.createContext(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
+ .thenReturn(context);
+ }
+
+ @Override
+ protected GSSManager getManager() {
+ return manager;
+ }
+
+ }
+
+ private static class MutualNegotiateSchemeWithMockGssManager extends MutualSpnegoScheme {
+
+ final GSSManager manager = Mockito.mock(GSSManager.class);
+ final GSSName name = Mockito.mock(GSSName.class);
+ final GSSContext context = Mockito.mock(GSSContext.class);
+
+ MutualNegotiateSchemeWithMockGssManager(final boolean established, final boolean mutual) throws Exception {
+ super(MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE);
+ // Initial empty WWW-Authenticate response header
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(NO_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn(GOOD_TOKEN_BYTES);
+ // Valid mutual token
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(GOOD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenReturn(NO_TOKEN_BYTES);
+ // Invalid mutual token
+ Mockito.when(context.initSecContext(
+ AdditionalMatchers.aryEq(BAD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()))
+ .thenThrow(new GSSException(GSSException.DEFECTIVE_CREDENTIAL));
+ // It's hard to mock state, so instead we specify the complete and mutualAuth states
+ // in the constructor
+ Mockito.when(context.isEstablished()).thenReturn(established);
+ Mockito.when(context.getMutualAuthState()).thenReturn(mutual);
+ Mockito.when(manager.createName(
+ ArgumentMatchers.anyString(), ArgumentMatchers.any()))
+ .thenReturn(name);
+ Mockito.when(manager.createContext(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt()))
+ .thenReturn(context);
+ }
+
+ @Override
+ protected GSSManager getManager() {
+ return manager;
+ }
+
+ }
+
+ private static class UseJaasCredentials implements Credentials {
+
+ @Override
+ public char[] getPassword() {
+ return null;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return null;
+ }
+
+ }
+
+ private static class TestAuthSchemeFactory implements AuthSchemeFactory {
+
+ AuthScheme scheme;
+
+ TestAuthSchemeFactory(final AuthScheme scheme) throws Exception {
+ this.scheme = scheme;
+ }
+
+ @Override
+ public AuthScheme create(final HttpContext context) {
+ return scheme;
+ }
+
+ }
+
+
+ /**
+ * Tests that the client will stop connecting to the server if
+ * the server still keep asking for a valid ticket.
+ */
+ @Test
+ void testDontTryToAuthenticateEndlessly() throws Exception {
+ configureServer(t -> {
+ t.register("*", new PleaseNegotiateService());
+ });
+
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager());
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ t.setDefaultCredentialsProvider(jaasCredentialsProvider);
+ });
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ client().execute(target, httpget, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+ return null;
+ });
+ }
+
+ /**
+ * Javadoc specifies that {@link GSSContext#initSecContext(byte[], int, int)} can return null
+ * if no token is generated. Client should be able to deal with this response.
+ */
+ @Test
+ void testNoTokenGeneratedError() throws Exception {
+ configureServer(t -> {
+ t.register("*", new PleaseNegotiateService());
+ });
+
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager());
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ t.setDefaultCredentialsProvider(jaasCredentialsProvider);
+ });
+
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ client().execute(target, httpget, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+ return null;
+ });
+
+ }
+
+ /**
+ * Test the success case for mutual auth
+ */
+ @Test
+ void testMutualSuccess() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+ final HttpHost target = startServer();
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ t.setDefaultCredentialsProvider(jaasCredentialsProvider);
+ });
+
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ client().execute(target, httpget, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ return null;
+ });
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState();
+ }
+
+ /**
+ * No mutual auth response token sent by server.
+ */
+ @Test
+ void testMutualFailureNoToken() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SPNEGOMutualService(false, null));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "valid" token, but we mock the established status to false
+ */
+ @Test
+ void testMutualFailureEstablishedStatusFalse() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "valid" token, but we mock the mutual auth status to false
+ */
+ @Test
+ void testMutualFailureMutualStatusFalse() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, false);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState();
+ }
+
+ /**
+ * Server sends a "bad" token, and GSS throws an exception.
+ */
+ @Test
+ void testMutualFailureBadToken() throws Exception {
+ configureServer(t -> {
+ t.register("*", new SPNEGOMutualService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES));
+ });
+
+ // We except that the initSecContent throws an exception, so the status is irrelevant
+ final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true);
+ final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
+ final Registry authSchemeRegistry = RegistryBuilder.create()
+ .register(StandardAuthScheme.SPNEGO, nsf)
+ .build();
+
+ configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
+ t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
+ });
+
+ final HttpClientContext context = new HttpClientContext();
+ context.setCredentialsProvider(jaasCredentialsProvider);
+
+ final HttpHost target = startServer();
+ final String s = "/path";
+ final HttpGet httpget = new HttpGet(s);
+ try {
+ client().execute(target, httpget, context, response -> {
+ EntityUtils.consume(response.getEntity());
+ Assertions.fail();
+ return null;
+ });
+ Assertions.fail();
+ } catch (final Exception e) {
+ Assertions.assertTrue(e instanceof ClientProtocolException);
+ Assertions.assertTrue(e.getCause() instanceof AuthenticationException);
+ }
+
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).isEstablished();
+ Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState();
+ }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
index 2aaf1fb66a..f4ace069db 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java
@@ -38,6 +38,9 @@
*/
public class AuthExchange {
+ // This only tracks the server state. In particular, even if the state is SUCCESS,
+ // the authentication may still fail if the challenge sent with an authorized response cannot
+ // be validated locally for AuthScheme2 schemes.
public enum State {
UNCHALLENGED, CHALLENGED, HANDSHAKE, FAILURE, SUCCESS
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java
index 22d884a97d..f791198075 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java
@@ -86,6 +86,10 @@
* containing the terminal authorization response, the scheme is considered unsuccessful
* and in FAILED state.
*
+ *
+ * This interface cannot correctly handle some authentication methods, like SPNEGO.
+ * See {@link AuthScheme2} for a more capable interface.
+ *
*
* @since 4.0
*/
@@ -128,6 +132,9 @@ void processChallenge(
* successfully or unsuccessfully), that is, all the required authorization
* challenges have been processed in their entirety.
*
+ * Note that due to some assumptions made about the control flow by the authentication code
+ * returning true will immediately cause the authentication process to fail.
+ *
* @return {@code true} if the authentication process has been completed,
* {@code false} otherwise.
*
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java
new file mode 100644
index 0000000000..7c380d728e
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java
@@ -0,0 +1,102 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.auth;
+
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+/**
+ * This is an improved version of the {@link AuthScheme} interface, amended to be able to handle
+ * a conversation involving multiple challenge-response transactions and adding the ability to check
+ * the results of a final token sent together with the successful HTTP request as required by
+ * RFC 4559 and RFC 7546.
+ *
+ * @since 5.5
+ */
+public interface AuthScheme2 extends AuthScheme {
+
+ /**
+ * Processes the given auth challenge. Some authentication schemes may involve multiple
+ * challenge-response exchanges. Such schemes must be able to maintain internal state
+ * when dealing with sequential challenges.
+ *
+ * The {@link AuthScheme} interface implicitly assumes that that the token passed here is
+ * simply stored in this method, and the actual authentication takes place in
+ * {@link org.apache.hc.client5.http.auth.AuthScheme#generateAuthResponse(HttpHost, HttpRequest, HttpContext) generateAuthResponse }
+ * and/or {@link org.apache.hc.client5.http.auth.AuthScheme#isResponseReady(HttpHost, HttpRequest, HttpContext) generateAuthResponse },
+ * as only those methods receive the HttpHost, and only those can throw an
+ * AuthenticationException.
+ *
+ * This new methods signature makes it possible to process the token and throw an
+ * AuthenticationException immediately even when no response is sent (i.e. processing the mutual
+ * authentication response)
+ *
+ * When {@link isChallengeExpected} returns true, but no challenge was sent, then this method must
+ * be called with a null {@link AuthChallenge} so that the Scheme can handle this situation.
+ *
+ * @param host HTTP host
+ * @param authChallenge the auth challenge or null if no challenge was received
+ * @param context HTTP context
+ * @param challenged true if the response was unauthorised (401/407)
+ * @throws AuthenticationException in case the authentication process is unsuccessful.
+ * @since 5.5
+ */
+ void processChallenge(
+ HttpHost host,
+ AuthChallenge authChallenge,
+ HttpContext context,
+ boolean challenged) throws AuthenticationException;
+
+ /**
+ * The old processChallenge signature is unfit for use in AuthScheme2.
+ * If the old signature is sufficient for a scheme, then it should implement {@link AuthScheme}
+ * instead AuthScheme2.
+ */
+ @Override
+ default void processChallenge(
+ AuthChallenge authChallenge,
+ HttpContext context) throws MalformedChallengeException {
+ throw new UnsupportedOperationException("on AuthScheme2 implementations only the four "
+ + "argument processChallenge method can be called");
+ }
+
+ /**
+ * Indicates that the even authorized (i.e. not 401 or 407) responses must be processed
+ * by this Scheme.
+ *
+ * The original AuthScheme interface only processes unauthorised responses.
+ * This method indicates that non unauthorised responses are expected to contain challenges
+ * and must be processed by the Scheme.
+ * This is required to implement the SPENGO RFC and Kerberos mutual authentication.
+ *
+ * @return true if responses with non 401/407 response codes must be processed by the scheme.
+ * @since 5.5
+ */
+ boolean isChallengeExpected();
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
index 508eeb9b0e..2793b8e1f7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java
@@ -35,11 +35,7 @@
*
* @since 4.6
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
*/
-@Deprecated
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class KerberosConfig implements Cloneable {
@@ -53,25 +49,28 @@ public enum Option {
public static final KerberosConfig DEFAULT = new Builder().build();
- private final Option stripPort;
- private final Option useCanonicalHostname;
- private final Option requestDelegCreds;
+ private final Option stripPort; //Effective default is ENABLE
+ private final Option useCanonicalHostname; //Effective default is ENABLE
+ private final Option requestDelegCreds; //Effective default is DISABLE
+ private final Option requestMutualAuth; //Effective default is DISABLE
/**
* Intended for CDI compatibility
*/
protected KerberosConfig() {
- this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT);
+ this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT, Option.DEFAULT);
}
KerberosConfig(
final Option stripPort,
final Option useCanonicalHostname,
- final Option requestDelegCreds) {
+ final Option requestDelegCreds,
+ final Option requestMutualAuth) {
super();
this.stripPort = stripPort;
this.useCanonicalHostname = useCanonicalHostname;
this.requestDelegCreds = requestDelegCreds;
+ this.requestMutualAuth = requestMutualAuth;
}
public Option getStripPort() {
@@ -86,6 +85,10 @@ public Option getRequestDelegCreds() {
return requestDelegCreds;
}
+ public Option getRequestMutualAuth() {
+ return requestMutualAuth;
+ }
+
@Override
protected KerberosConfig clone() throws CloneNotSupportedException {
return (KerberosConfig) super.clone();
@@ -98,6 +101,7 @@ public String toString() {
builder.append("stripPort=").append(stripPort);
builder.append(", useCanonicalHostname=").append(useCanonicalHostname);
builder.append(", requestDelegCreds=").append(requestDelegCreds);
+ builder.append(", requestMutualAuth=").append(requestMutualAuth);
builder.append("]");
return builder.toString();
}
@@ -110,7 +114,9 @@ public static KerberosConfig.Builder copy(final KerberosConfig config) {
return new Builder()
.setStripPort(config.getStripPort())
.setUseCanonicalHostname(config.getUseCanonicalHostname())
- .setRequestDelegCreds(config.getRequestDelegCreds());
+ .setRequestDelegCreds(config.getRequestDelegCreds())
+ .setRequestMutualAuth(config.getRequestMutualAuth()
+ );
}
public static class Builder {
@@ -118,12 +124,14 @@ public static class Builder {
private Option stripPort;
private Option useCanonicalHostname;
private Option requestDelegCreds;
+ private Option requestMutualAuth;
Builder() {
super();
this.stripPort = Option.DEFAULT;
this.useCanonicalHostname = Option.DEFAULT;
this.requestDelegCreds = Option.DEFAULT;
+ this.requestMutualAuth = Option.DEFAULT;
}
public Builder setStripPort(final Option stripPort) {
@@ -151,11 +159,17 @@ public Builder setRequestDelegCreds(final Option requestDelegCreds) {
return this;
}
+ public Builder setRequestMutualAuth(final Option requestMutualAuth) {
+ this.requestMutualAuth = requestMutualAuth;
+ return this;
+ }
+
public KerberosConfig build() {
return new KerberosConfig(
stripPort,
useCanonicalHostname,
- requestDelegCreds);
+ requestDelegCreds,
+ requestMutualAuth);
}
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
index 92bab8d4f3..e40963b2a8 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java
@@ -37,14 +37,7 @@
* Kerberos specific {@link Credentials} representation based on {@link GSSCredential}.
*
* @since 4.4
- *
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
- *
- * @see UsernamePasswordCredentials
- * @see BearerToken
*/
-@Deprecated
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class KerberosCredentials implements Credentials, Serializable {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
index 1345282c0b..79232cac10 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
@@ -66,18 +66,14 @@ private StandardAuthScheme() {
/**
* SPNEGO authentication scheme as defined in RFC 4559 and RFC 4178.
- *
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
*/
- @Deprecated
public static final String SPNEGO = "Negotiate";
/**
* Kerberos authentication scheme as defined in RFC 4120.
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Do not use. The Kerberos scheme was never standardized, and its
+ * implementation uses the old deprecated non mutual auth capable logic.
*/
@Deprecated
public static final String KERBEROS = "Kerberos";
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
index 0440a1322f..9409d6cfa4 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
@@ -72,6 +72,10 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy {
StandardAuthScheme.DIGEST,
StandardAuthScheme.BASIC));
+ protected List getSchemePriority() {
+ return DEFAULT_SCHEME_PRIORITY;
+ }
+
@Override
public List select(
final ChallengeType challengeType,
@@ -95,7 +99,7 @@ public List select(
Collection authPrefs = challengeType == ChallengeType.TARGET ?
config.getTargetPreferredAuthSchemes() : config.getProxyPreferredAuthSchemes();
if (authPrefs == null) {
- authPrefs = DEFAULT_SCHEME_PRIORITY;
+ authPrefs = getSchemePriority();
}
if (LOG.isDebugEnabled()) {
LOG.debug("{} Authentication schemes in the order of preference: {}", exchangeId, authPrefs);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
index 33802920fa..a97afef70d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java
@@ -43,7 +43,9 @@
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
import org.apache.hc.client5.http.async.AsyncExecRuntime;
import org.apache.hc.client5.http.auth.AuthExchange;
+import org.apache.hc.client5.http.auth.AuthenticationException;
import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper;
import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
@@ -515,10 +517,11 @@ private boolean needAuthentication(
final AuthExchange proxyAuthExchange,
final HttpHost proxy,
final HttpResponse response,
- final HttpClientContext context) {
+ final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
final RequestConfig config = context.getRequestConfigOrDefault();
if (config.isAuthenticationEnabled()) {
final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
+ final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
if (authCacheKeeper != null) {
if (proxyAuthRequested) {
@@ -528,7 +531,7 @@ private boolean needAuthentication(
}
}
- if (proxyAuthRequested) {
+ if (proxyAuthRequested || proxyMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
index 907b23e46b..579607a6d9 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java
@@ -38,7 +38,9 @@
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
import org.apache.hc.client5.http.async.AsyncExecRuntime;
import org.apache.hc.client5.http.auth.AuthExchange;
+import org.apache.hc.client5.http.auth.AuthenticationException;
import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
import org.apache.hc.client5.http.impl.RequestSupport;
@@ -305,11 +307,12 @@ private boolean needAuthentication(
final HttpHost target,
final String pathPrefix,
final HttpResponse response,
- final HttpClientContext context) {
+ final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
final RequestConfig config = context.getRequestConfigOrDefault();
if (config.isAuthenticationEnabled()) {
final boolean targetAuthRequested = authenticator.isChallenged(
target, ChallengeType.TARGET, response, targetAuthExchange, context);
+ final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange);
if (authCacheKeeper != null) {
if (targetAuthRequested) {
@@ -321,6 +324,7 @@ private boolean needAuthentication(
final boolean proxyAuthRequested = authenticator.isChallenged(
proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
+ final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
if (authCacheKeeper != null) {
if (proxyAuthRequested) {
@@ -330,7 +334,7 @@ private boolean needAuthentication(
}
}
- if (targetAuthRequested) {
+ if (targetAuthRequested || targetMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response,
targetAuthStrategy, targetAuthExchange, context);
@@ -340,7 +344,7 @@ private boolean needAuthentication(
return updated;
}
- if (proxyAuthRequested) {
+ if (proxyAuthRequested || proxyMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
index 773746b612..2953416f34 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
@@ -60,8 +60,9 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Do not use. This class implements functionality for the old deprecated non mutual
+ * authentication capable {@link SPNegoScheme} and {@link KerberosScheme} classes.
+ * The new mutual authentication capable implementation is {@link MutualGSSSchemeBase}.
*/
@Deprecated
public abstract class GGSSchemeBase implements AuthScheme {
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java
index cd9f7ce723..1deab416c6 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java
@@ -38,12 +38,14 @@
import org.apache.hc.client5.http.auth.AuthChallenge;
import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthScheme2;
import org.apache.hc.client5.http.auth.AuthenticationException;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.CredentialsProvider;
import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.FormattedHeader;
import org.apache.hc.core5.http.Header;
@@ -69,6 +71,7 @@
*
* @since 4.3
*/
+@Internal
@Contract(threading = ThreadingBehavior.STATELESS)
public final class HttpAuthenticator {
@@ -81,12 +84,13 @@ public HttpAuthenticator() {
}
/**
- * Determines whether the given response represents an authentication challenge.
+ * Determines whether the given response represents an authentication challenge, and updates
+ * the autheExchange status.
*
* @param host the hostname of the opposite endpoint.
* @param challengeType the challenge type (target or proxy).
* @param response the response message head.
- * @param authExchange the current authentication exchange state.
+ * @param authExchange the current authentication exchange state. Gets updated.
* @param context the current execution context.
* @return {@code true} if the response message represents an authentication challenge,
* {@code false} otherwise.
@@ -97,32 +101,17 @@ public boolean isChallenged(
final HttpResponse response,
final AuthExchange authExchange,
final HttpContext context) {
- final int challengeCode;
- switch (challengeType) {
- case TARGET:
- challengeCode = HttpStatus.SC_UNAUTHORIZED;
- break;
- case PROXY:
- challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
- break;
- default:
- throw new IllegalStateException("Unexpected challenge type: " + challengeType);
- }
-
- final HttpClientContext clientContext = HttpClientContext.cast(context);
- final String exchangeId = clientContext.getExchangeId();
-
- if (response.getCode() == challengeCode) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} Authentication required", exchangeId);
- }
+ if (checkChallenged(challengeType, response, context)) {
return true;
}
switch (authExchange.getState()) {
case CHALLENGED:
case HANDSHAKE:
if (LOG.isDebugEnabled()) {
- LOG.debug("{} Authentication succeeded", exchangeId);
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ // The mutual auth may still fail
+ LOG.debug("{} Server has accepted authorization", exchangeId);
}
authExchange.setState(AuthExchange.State.SUCCESS);
break;
@@ -135,37 +124,64 @@ public boolean isChallenged(
}
/**
- * Updates the {@link AuthExchange} state based on the challenge presented in the response message
- * using the given {@link AuthenticationStrategy}.
+ * Determines whether the given response represents an authentication challenge, without
+ * changing the AuthExchange state.
*
- * @param host the hostname of the opposite endpoint.
* @param challengeType the challenge type (target or proxy).
* @param response the response message head.
- * @param authStrategy the authentication strategy.
- * @param authExchange the current authentication exchange state.
* @param context the current execution context.
- * @return {@code true} if the authentication state has been updated,
- * {@code false} if unchanged.
+ * @return {@code true} if the response message represents an authentication challenge,
+ * {@code false} otherwise.
*/
- public boolean updateAuthState(
- final HttpHost host,
- final ChallengeType challengeType,
- final HttpResponse response,
- final AuthenticationStrategy authStrategy,
- final AuthExchange authExchange,
- final HttpContext context) {
+ private boolean checkChallenged(final ChallengeType challengeType, final HttpResponse response, final HttpContext context) {
+ final int challengeCode;
+ switch (challengeType) {
+ case TARGET:
+ challengeCode = HttpStatus.SC_UNAUTHORIZED;
+ break;
+ case PROXY:
+ challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
+ break;
+ default:
+ throw new IllegalStateException("Unexpected challenge type: " + challengeType);
+ }
- final HttpClientContext clientContext = HttpClientContext.cast(context);
- final String exchangeId = clientContext.getExchangeId();
+ if (response.getCode() == challengeCode) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Authentication required", exchangeId);
+ }
+ return true;
+ }
+ return false;
+ }
- if (LOG.isDebugEnabled()) {
- LOG.debug("{} {} requested authentication", exchangeId, host.toHostString());
+ /**
+ * Determines if the scheme requires an auth challenge for responses that do not
+ * have challenge HTTP code. (i.e whether it needs a mutual authentication token)
+ *
+ * @param authExchange
+ * @return true is authExchange's scheme is AuthScheme2, which currently expects
+ * a WWW-Authenticate header even for authorized HTTP responses
+ */
+ public boolean isChallengeExpected(final AuthExchange authExchange) {
+ final AuthScheme authScheme = authExchange.getAuthScheme();
+ if (authScheme != null && authScheme instanceof AuthScheme2) {
+ return ((AuthScheme2)authScheme).isChallengeExpected();
+ } else {
+ return false;
}
+ }
- final Header[] headers = response.getHeaders(
- challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE);
+ public Map extractChallengeMap(final ChallengeType challengeType,
+ final HttpResponse response, final HttpClientContext context) {
+ final Header[] headers =
+ response.getHeaders(
+ challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE
+ : HttpHeaders.WWW_AUTHENTICATE);
final Map challengeMap = new HashMap<>();
- for (final Header header: headers) {
+ for (final Header header : headers) {
final CharArrayBuffer buffer;
final int pos;
if (header instanceof FormattedHeader) {
@@ -186,52 +202,109 @@ public boolean updateAuthState(
authChallenges = parser.parse(challengeType, buffer, cursor);
} catch (final ParseException ex) {
if (LOG.isWarnEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue());
}
continue;
}
- for (final AuthChallenge authChallenge: authChallenges) {
+ for (final AuthChallenge authChallenge : authChallenges) {
final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT);
if (!challengeMap.containsKey(schemeName)) {
challengeMap.put(schemeName, authChallenge);
}
}
}
+ return challengeMap;
+ }
+
+ /**
+ * Updates the {@link AuthExchange} state based on the challenge presented in the response message
+ * using the given {@link AuthenticationStrategy}.
+ *
+ * @param host the hostname of the opposite endpoint.
+ * @param challengeType the challenge type (target or proxy).
+ * @param response the response message head.
+ * @param authStrategy the authentication strategy.
+ * @param authExchange the current authentication exchange state.
+ * @param context the current execution context.
+ * @return {@code true} if the request needs-to be re-sent ,
+ * {@code false} if the authentication is complete (successful or not).
+ *
+ * @throws AuthenticationException if the AuthScheme throws one. In most cases this indicates a
+ * client side problem, as final server error responses are simply returned.
+ * @throws MalformedChallengeException if the AuthScheme throws one. In most cases this indicates a
+ * client side problem, as final server error responses are simply returned.
+ */
+ public boolean updateAuthState(
+ final HttpHost host,
+ final ChallengeType challengeType,
+ final HttpResponse response,
+ final AuthenticationStrategy authStrategy,
+ final AuthExchange authExchange,
+ final HttpContext context) throws AuthenticationException, MalformedChallengeException {
+
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ final boolean challenged = checkChallenged(challengeType, response, context);
+ final boolean isChallengeExpected = isChallengeExpected(authExchange);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("{} {} requested authentication", exchangeId, host.toHostString());
+ }
+
+ final Map challengeMap = extractChallengeMap(challengeType, response, clientContext);
+
if (challengeMap.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("{} Response contains no valid authentication challenges", exchangeId);
}
- authExchange.reset();
- return false;
+ if (!isChallengeExpected) {
+ authExchange.reset();
+ return false;
+ }
}
switch (authExchange.getState()) {
case FAILURE:
return false;
case SUCCESS:
- authExchange.reset();
- break;
+ if (!isChallengeExpected) {
+ authExchange.reset();
+ break;
+ }
+ // otherwise fall through
case CHALLENGED:
+ // fall through
case HANDSHAKE:
Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme");
+ // fall through
case UNCHALLENGED:
final AuthScheme authScheme = authExchange.getAuthScheme();
+ // AuthScheme is only set if we have already sent an auth response, either
+ // because we have received a challenge for it, or preemptively.
if (authScheme != null) {
final String schemeName = authScheme.getName();
final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
- if (challenge != null) {
+ if (challenge != null || isChallengeExpected) {
if (LOG.isDebugEnabled()) {
- LOG.debug("{} Authorization challenge processed", exchangeId);
+ LOG.debug("{} Processing authorization challenge {}", exchangeId, challenge);
}
try {
- authScheme.processChallenge(challenge, context);
- } catch (final MalformedChallengeException ex) {
+ if (authScheme instanceof AuthScheme2) {
+ ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged);
+ } else {
+ authScheme.processChallenge(challenge, context);
+ }
+ } catch (final AuthenticationException | MalformedChallengeException ex) {
if (LOG.isWarnEnabled()) {
- LOG.warn("{} {}", exchangeId, ex.getMessage());
+ LOG.warn("Exception processing Challange {}", exchangeId, ex);
}
authExchange.reset();
authExchange.setState(AuthExchange.State.FAILURE);
- return false;
+ if (isChallengeExpected) {
+ throw ex;
+ }
}
if (authScheme.isChallengeComplete()) {
if (LOG.isDebugEnabled()) {
@@ -241,7 +314,14 @@ public boolean updateAuthState(
authExchange.setState(AuthExchange.State.FAILURE);
return false;
}
- authExchange.setState(AuthExchange.State.HANDSHAKE);
+ if (!challenged) {
+ // There are no more challanges sent after the 200 message,
+ // and if we get here, then the mutual auth phase has succeeded.
+ authExchange.setState(AuthExchange.State.SUCCESS);
+ return false;
+ } else {
+ authExchange.setState(AuthExchange.State.HANDSHAKE);
+ }
return true;
}
authExchange.reset();
@@ -249,6 +329,9 @@ public boolean updateAuthState(
}
}
+ // We reach this if we fell through above because the authScheme has not yet been set, or if
+ // we receive a 401/407 response for an unexpected scheme. Normally this processes the first
+ // 401/407 response
final List preferredSchemes = authStrategy.select(challengeType, challengeMap, context);
final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
if (credsProvider == null) {
@@ -263,16 +346,23 @@ public boolean updateAuthState(
LOG.debug("{} Selecting authentication options", exchangeId);
}
for (final AuthScheme authScheme: preferredSchemes) {
+ // We only respond to the the first successfully processed challenge. However, the
+ // original AuthScheme API does not really process the challenge at this point, so we need
+ // to process/store each challenge here anyway.
try {
final String schemeName = authScheme.getName();
final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
- authScheme.processChallenge(challenge, context);
+ if (authScheme instanceof AuthScheme2) {
+ ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged);
+ } else {
+ authScheme.processChallenge(challenge, context);
+ }
if (authScheme.isResponseReady(host, credsProvider, context)) {
authOptions.add(authScheme);
}
} catch (final AuthenticationException | MalformedChallengeException ex) {
if (LOG.isWarnEnabled()) {
- LOG.warn(ex.getMessage());
+ LOG.warn("Exception while processing Challange", ex);
}
}
}
@@ -331,12 +421,14 @@ public void addAuthResponse(
}
try {
final String authResponse = authScheme.generateAuthResponse(host, request, context);
- final Header header = new BasicHeader(
- challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
- authResponse);
- request.addHeader(header);
+ if (authResponse != null) {
+ final Header header = new BasicHeader(
+ challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
+ authResponse);
+ request.addHeader(header);
+ }
break;
- } catch (final AuthenticationException ex) {
+ } catch (final AuthenticationException ex ) {
if (LOG.isWarnEnabled()) {
LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
}
@@ -347,6 +439,9 @@ public void addAuthResponse(
Asserts.notNull(authScheme, "AuthScheme");
default:
}
+ // This is the SUCCESS and HANDSHAKE states, same as the initial response.
+ // This only happens if the NEGOTIATE handshake requires multiple requests, which is
+ // defined in the RFC, but unlikely in practice.
if (authScheme != null) {
try {
final String authResponse = authScheme.generateAuthResponse(host, request, context);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
index 656f29633a..9b2adb96d2 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
@@ -41,9 +41,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Do not use. The Kerberos authentication scheme was never standardised.
+ * Use {@link MutualSpnegoScheme} or some other scheme instead.
*
+ * @see MutualSpnegoScheme
* @see BasicScheme
* @see BearerScheme
*/
@@ -70,8 +71,8 @@ public String getName() {
}
@Override
- protected byte[] generateToken(final byte[] input, final String serviceName, final String authServer) throws GSSException {
- return generateGSSToken(input, new Oid(KERBEROS_OID), serviceName, authServer);
+ protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
+ return generateGSSToken(input, new Oid(KERBEROS_OID), gssServiceName, gssHostname);
}
@Override
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
index 25930f0997..9c6cf9349a 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java
@@ -45,9 +45,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Do not use. The Kerberos authentication scheme was never standardised.
+ * Use {@link MutualSpnegoScheme} or some other scheme instead.
*
+ * @see MutualSpnegoSchemeFactory
* @see BasicSchemeFactory
* @see BearerSchemeFactory
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java
new file mode 100644
index 0000000000..9fbee575a6
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java
@@ -0,0 +1,348 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth;
+
+import java.net.UnknownHostException;
+import java.security.Principal;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthChallenge;
+import org.apache.hc.client5.http.auth.AuthScheme2;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.AuthenticationException;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.InvalidCredentialsException;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.auth.KerberosConfig;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.utils.Base64;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Common behaviour for the new mutual authentication capable {@code GSS} based authentication
+ * schemes.
+ *
+ * This class is derived from the old {@link GGSScheme} class, which was deprecated in 5.3.
+ *
+ * @since 5.5
+ *
+ * @see GGSSchemeBase
+ */
+public abstract class MutualGssSchemeBase implements AuthScheme2 {
+
+ enum State {
+ UNINITIATED,
+ TOKEN_READY,
+ TOKEN_SENT,
+ SUCCEEDED,
+ FAILED,
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(MutualGssSchemeBase.class);
+ private static final String NO_TOKEN = "";
+ private static final String KERBEROS_SCHEME = "HTTP";
+
+ // The GSS spec does not specify how long the conversation can be. This should be plenty.
+ // Realistically, we get one initial token, then one maybe one more for mutual authentication.
+ private static final int MAX_GSS_CHALLENGES = 3;
+ private final KerberosConfig config;
+ private final DnsResolver dnsResolver;
+ private final boolean mutualAuth;
+ private int challengesLeft = MAX_GSS_CHALLENGES;
+
+ /** Authentication process state */
+ private State state;
+ private GSSCredential gssCredential;
+ private GSSContext gssContext;
+ private String challenge;
+ private byte[] queuedToken = new byte[0];
+
+ MutualGssSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) {
+ super();
+ this.config = config != null ? config : KerberosConfig.DEFAULT;
+ this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
+ this.mutualAuth = config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE;
+ this.state = State.UNINITIATED;
+ }
+
+ MutualGssSchemeBase(final KerberosConfig config) {
+ this(config, SystemDefaultDnsResolver.INSTANCE);
+ }
+
+ MutualGssSchemeBase() {
+ this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ }
+
+ @Override
+ public String getRealm() {
+ return null;
+ }
+
+ // The AuthScheme API maps awkwardly to GSSAPI, where proccessChallange and generateAuthResponse
+ // map to the same single method call. Hence the generated token is only stored in this method.
+ @Override
+ public void processChallenge(
+ final HttpHost host,
+ final AuthChallenge authChallenge,
+ final HttpContext context,
+ final boolean challenged) throws AuthenticationException {
+
+ if (challengesLeft-- <= 0 ) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId);
+ }
+ // TODO: Should we throw an exception ? There is a test for this behaviour.
+ state = State.FAILED;
+ return;
+ }
+
+ final byte[] challengeToken = Base64.decodeBase64(authChallenge == null ? null : authChallenge.getValue());
+
+ final String gssHostname;
+ String hostname = host.getHostName();
+ if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE) {
+ try {
+ hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
+ } catch (final UnknownHostException ignore) {
+ }
+ }
+ if (config.getStripPort() != KerberosConfig.Option.DISABLE) {
+ gssHostname = hostname;
+ } else {
+ gssHostname = hostname + ":" + host.getPort();
+ }
+
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSS init {}", exchangeId, gssHostname);
+ }
+ try {
+ queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname);
+ switch (state) {
+ case UNINITIATED:
+ if (challenge != NO_TOKEN) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken);
+ }
+ // TODO Should we fail ? That would break existing tests that send a token
+ // in the first response, which is against the RFC.
+ }
+ state = State.TOKEN_READY;
+ break;
+ case TOKEN_SENT:
+ if (challenged) {
+ state = State.TOKEN_READY;
+ } else if (mutualAuth) {
+ // We should have received a valid mutualAuth token
+ if (!gssContext.isEstablished()) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext =
+ HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSSContext is not established ", exchangeId);
+ }
+ state = State.FAILED;
+ // TODO should we have specific exception(s) for these ?
+ throw new AuthenticationException(
+ "requireMutualAuth is set but GSSContext is not established");
+ } else if (!gssContext.getMutualAuthState()) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext =
+ HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have"
+ + " mutualAuthState set", exchangeId);
+ }
+ state = State.FAILED;
+ throw new AuthenticationException(
+ "requireMutualAuth is set but GSSContext mutualAuthState is not set");
+ } else {
+ state = State.SUCCEEDED;
+ }
+ }
+ break;
+ default:
+ state = State.FAILED;
+ throw new IllegalStateException("Illegal state: " + state);
+
+ }
+ } catch (final GSSException gsse) {
+ state = State.FAILED;
+ if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL
+ || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) {
+ throw new InvalidCredentialsException(gsse.getMessage(), gsse);
+ }
+ if (gsse.getMajor() == GSSException.NO_CRED) {
+ throw new InvalidCredentialsException(gsse.getMessage(), gsse);
+ }
+ if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN
+ || gsse.getMajor() == GSSException.DUPLICATE_TOKEN
+ || gsse.getMajor() == GSSException.OLD_TOKEN) {
+ throw new AuthenticationException(gsse.getMessage(), gsse);
+ }
+ // other error
+ throw new AuthenticationException(gsse.getMessage(), gsse);
+ }
+ }
+
+ protected GSSManager getManager() {
+ return GSSManager.getInstance();
+ }
+
+ /**
+ * @since 4.4
+ */
+ protected byte[] generateGSSToken(
+ final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException {
+ final GSSManager manager = getManager();
+ final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
+
+ if (gssContext == null) {
+ gssContext = createGSSContext(manager, oid, peerName, gssCredential);
+ }
+ if (input != null) {
+ return gssContext.initSecContext(input, 0, input.length);
+ }
+ return gssContext.initSecContext(new byte[] {}, 0, 0);
+ }
+
+ /**
+ * @since 5.0
+ */
+ protected GSSContext createGSSContext(
+ final GSSManager manager,
+ final Oid oid,
+ final GSSName peerName,
+ final GSSCredential gssCredential) throws GSSException {
+ final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential,
+ GSSContext.DEFAULT_LIFETIME);
+ gssContext.requestMutualAuth(true);
+ if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) {
+ gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE);
+ }
+ if (config.getRequestMutualAuth() != KerberosConfig.Option.DEFAULT) {
+ gssContext.requestMutualAuth(config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE);
+ }
+ return gssContext;
+ }
+ /**
+ * @since 4.4
+ */
+ protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException;
+
+ @Override
+ public boolean isChallengeComplete() {
+ // For the mutual authentication response, this is should technically return true.
+ // However, the HttpAuthenticator immediately fails the authentication
+ // process if we return true, so we only return true here if the authentication has failed.
+ return this.state == State.FAILED;
+ }
+
+ @Override
+ public boolean isChallengeExpected() {
+ return state == State.TOKEN_SENT && mutualAuth;
+ }
+
+ @Override
+ public boolean isResponseReady(
+ final HttpHost host,
+ final CredentialsProvider credentialsProvider,
+ final HttpContext context) throws AuthenticationException {
+
+ Args.notNull(host, "Auth host");
+ Args.notNull(credentialsProvider, "CredentialsProvider");
+
+ final Credentials credentials = credentialsProvider.getCredentials(
+ new AuthScope(host, null, getName()), context);
+ if (credentials instanceof org.apache.hc.client5.http.auth.KerberosCredentials) {
+ this.gssCredential = ((org.apache.hc.client5.http.auth.KerberosCredentials) credentials).getGSSCredential();
+ } else {
+ this.gssCredential = null;
+ }
+ return true;
+ }
+
+ @Override
+ public Principal getPrincipal() {
+ return null;
+ }
+
+ // Format the queued token and update the state.
+ // All token processing is done in processChallenge()
+ @Override
+ public String generateAuthResponse(
+ final HttpHost host,
+ final HttpRequest request,
+ final HttpContext context) throws AuthenticationException {
+ Args.notNull(host, "HTTP host");
+ Args.notNull(request, "HTTP request");
+ switch (state) {
+ case UNINITIATED:
+ throw new AuthenticationException(getName() + " authentication has not been initiated");
+ case FAILED:
+ throw new AuthenticationException(getName() + " authentication has failed");
+ case SUCCEEDED:
+ return null;
+ case TOKEN_READY:
+ state = State.TOKEN_SENT;
+ final Base64 codec = new Base64(0);
+ final String tokenstr = new String(codec.encode(queuedToken));
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr);
+ }
+ return StandardAuthScheme.SPNEGO + " " + tokenstr;
+ default:
+ throw new IllegalStateException("Illegal state: " + state);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getName() + "{" + this.state + " " + challenge + '}';
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java
new file mode 100644
index 0000000000..5403912ba0
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java
@@ -0,0 +1,113 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth;
+
+import org.apache.hc.client5.http.AuthenticationStrategy;
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.core5.annotation.Experimental;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.Oid;
+
+/**
+ * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication
+ * scheme.
+ *
+ * This is the new mutual authentication capable Scheme which replaces the old deprecated non mutual
+ * authentication capable {@link SPNegoScheme}
+ *
+ *
+ *
+ * Note that this scheme is not enabled by default. To use it, you need create a custom
+ * {@link AuthenticationStrategy} and a custom {@link AuthSchemeFactory} {@link Registry},
+ * and set them on the HttpClientBuilder.
+ *
+ *
+ *
+ * {@code
+ * private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy {
+ * private static final List SPNEGO_SCHEME_PRIORITY =
+ * Collections.unmodifiableList(
+ * Arrays.asList(StandardAuthScheme.SPNEGO
+ * // Add other Schemes as needed
+ * );
+ *
+ * @Override
+ * protected final List getSchemePriority() {
+ * return SPNEGO_SCHEME_PRIORITY;
+ * }
+ * }
+ *
+ * AuthenticationStrategy mutualStrategy = new SpnegoAuthenticationStrategy();
+ *
+ * AuthSchemeFactory mutualFactory = new MutualSpnegoSchemeFactory();
+ * Registry mutualSchemeRegistry = RegistryBuilder.create()
+ * .register(StandardAuthScheme.SPNEGO, mutualFactory)
+ * //register other schemes as needed
+ * .build();
+ *
+ * CloseableHttpClient mutualClient = HttpClientBuilder.create()
+ * .setTargetAuthenticationStrategy(mutualStrategy);
+ * .setDefaultAuthSchemeRegistry(mutualSchemeRegistry);
+ * .build();
+ * }
+ *
+ *
+ * @since 5.5
+ */
+@Experimental
+public class MutualSpnegoScheme extends MutualGssSchemeBase {
+
+ private static final String SPNEGO_OID = "1.3.6.1.5.5.2";
+
+ /**
+ * @since 5.0
+ */
+ public MutualSpnegoScheme(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) {
+ super(config, dnsResolver);
+ }
+
+ public MutualSpnegoScheme() {
+ super();
+ }
+
+ @Override
+ public String getName() {
+ return StandardAuthScheme.SPNEGO;
+ }
+
+ @Override
+ protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
+ return generateGSSToken(input, new Oid(SPNEGO_OID), gssServiceName, gssHostname);
+ }
+
+ @Override
+ public boolean isConnectionBased() {
+ return true;
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java
new file mode 100644
index 0000000000..5880f47adc
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java
@@ -0,0 +1,76 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.auth;
+
+import org.apache.hc.client5.http.DnsResolver;
+import org.apache.hc.client5.http.SystemDefaultDnsResolver;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Experimental;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+/**
+ * {@link AuthSchemeFactory} implementation that creates and initialises
+ * {@link MutualSpnegoScheme} instances.
+ *
+ * This replaces the old deprecated {@link SPNegoSchemeFactory}
+ *
+ *
+ * @since 5.5
+ *
+ * @see SPNegoSchemeFactory
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+@Experimental
+public class MutualSpnegoSchemeFactory implements AuthSchemeFactory {
+
+ /**
+ * Singleton instance for the default configuration.
+ */
+ public static final MutualSpnegoSchemeFactory DEFAULT = new MutualSpnegoSchemeFactory(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT,
+ SystemDefaultDnsResolver.INSTANCE);
+
+ private final org.apache.hc.client5.http.auth.KerberosConfig config;
+ private final DnsResolver dnsResolver;
+
+ /**
+ * @since 5.0
+ */
+ public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) {
+ super();
+ this.config = config;
+ this.dnsResolver = dnsResolver;
+ }
+
+ @Override
+ public AuthScheme create(final HttpContext context) {
+ return new MutualSpnegoScheme(this.config, this.dnsResolver);
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
index 7971ff935d..6d9f9408fe 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
@@ -36,15 +36,15 @@
* SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication
* scheme.
*
- * Please note this class is considered experimental and may be discontinued or removed
- * in the future.
+ * This class implements the old deprecated non mutual authentication capable SPNEGO implementation.
+ * Use {@link MutualSpnegoScheme} instead.
*
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Use {@link MutualSpnegoScheme} or some other auth scheme instead.
*
+ * @see MutualSpnegoScheme
* @see BasicScheme
* @see BearerScheme
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
index 14d8528c5e..1231d47e49 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoSchemeFactory.java
@@ -39,15 +39,15 @@
* {@link AuthSchemeFactory} implementation that creates and initializes
* {@link SPNegoScheme} instances.
*
- * Please note this class is considered experimental and may be discontinued or removed
- * in the future.
+ * This factory creates the old deprecated non mutual authentication capable SPNEGO implementation.
+ * Use {@link MutualSpnegoAuthFactory} instead.
*
*
* @since 4.2
*
- * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
- * supported. Consider using Basic or Bearer authentication with TLS instead.
+ * @deprecated Use {@link MutualSpnegoAuthFactory} or some other auth scheme instead.
*
+ * @see MutualSpnegoAuthFactory
* @see BasicSchemeFactory
* @see BearerSchemeFactory
*/
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
index 482d6be154..dd36d335a7 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java
@@ -253,6 +253,7 @@ private ClassicHttpResponse createTunnelToTarget(
if (config.isAuthenticationEnabled()) {
final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
+ final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
if (authCacheKeeper != null) {
if (proxyAuthRequested) {
@@ -262,7 +263,7 @@ private ClassicHttpResponse createTunnelToTarget(
}
}
- if (proxyAuthRequested) {
+ if (proxyAuthRequested || proxyMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index bfebce0eaf..41517e2e6f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
@@ -34,7 +34,9 @@
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.SchemePortResolver;
import org.apache.hc.client5.http.auth.AuthExchange;
+import org.apache.hc.client5.http.auth.AuthenticationException;
import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.classic.ExecRuntime;
@@ -189,6 +191,7 @@ public ClassicHttpResponse execute(
authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
}
+ //The is where the actual network communications happens (eventually)
final ClassicHttpResponse response = chain.proceed(request, scope);
if (Method.TRACE.isSame(request.getMethod())) {
@@ -218,6 +221,8 @@ public ClassicHttpResponse execute(
EntityUtils.consume(responseEntity);
} else {
execRuntime.disconnectEndpoint();
+ // We don't have any connection based AuthScheme2 implementations.
+ // If one existed, we'd have think about how to handle it
if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
&& proxyAuthExchange.isConnectionBased()) {
if (LOG.isDebugEnabled()) {
@@ -265,11 +270,12 @@ private boolean needAuthentication(
final HttpHost target,
final String pathPrefix,
final HttpResponse response,
- final HttpClientContext context) {
- final RequestConfig config = context.getRequestConfigOrDefault();
+ final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
+ final RequestConfig config = context.getRequestConfigOrDefault();
if (config.isAuthenticationEnabled()) {
final boolean targetAuthRequested = authenticator.isChallenged(
target, ChallengeType.TARGET, response, targetAuthExchange, context);
+ final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange);
if (authCacheKeeper != null) {
if (targetAuthRequested) {
@@ -281,6 +287,7 @@ private boolean needAuthentication(
final boolean proxyAuthRequested = authenticator.isChallenged(
proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
+ final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange);
if (authCacheKeeper != null) {
if (proxyAuthRequested) {
@@ -290,7 +297,7 @@ private boolean needAuthentication(
}
}
- if (targetAuthRequested) {
+ if (targetAuthRequested || targetMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response,
targetAuthStrategy, targetAuthExchange, context);
@@ -300,7 +307,7 @@ private boolean needAuthentication(
return updated;
}
- if (proxyAuthRequested) {
+ if (proxyAuthRequested || proxyMutualAuthRequired) {
final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
proxyAuthStrategy, proxyAuthExchange, context);
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java
index a4657a26ab..0f5347495a 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java
@@ -175,7 +175,8 @@ public Socket tunnel(
if (status < 200) {
throw new HttpException("Unexpected response to CONNECT request: " + response);
}
- if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) {
+ if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)
+ || authenticator.isChallengeExpected(proxyAuthExchange)) {
if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response,
this.proxyAuthStrategy, this.proxyAuthExchange, context)) {
// Retry request
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java
index 9107e88016..61df88722c 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java
@@ -38,6 +38,7 @@
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.Credentials;
import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.MalformedChallengeException;
import org.apache.hc.client5.http.auth.StandardAuthScheme;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
@@ -150,7 +151,7 @@ void testAuthenticationNotRequestedSuccess2() {
}
@Test
- void testAuthentication() {
+ void testAuthentication() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -179,7 +180,7 @@ void testAuthentication() {
}
@Test
- void testAuthenticationCredentialsForBasic() {
+ void testAuthenticationCredentialsForBasic() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response =
new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
@@ -205,7 +206,7 @@ void testAuthenticationCredentialsForBasic() {
}
@Test
- void testAuthenticationNoChallenges() {
+ void testAuthenticationNoChallenges() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
@@ -216,7 +217,7 @@ void testAuthenticationNoChallenges() {
}
@Test
- void testAuthenticationNoSupportedChallenges() {
+ void testAuthenticationNoSupportedChallenges() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "This realm=\"test\""));
@@ -229,7 +230,7 @@ void testAuthenticationNoSupportedChallenges() {
}
@Test
- void testAuthenticationNoCredentials() {
+ void testAuthenticationNoCredentials() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -242,7 +243,7 @@ void testAuthenticationNoCredentials() {
}
@Test
- void testAuthenticationFailed() {
+ void testAuthenticationFailed() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -260,7 +261,7 @@ void testAuthenticationFailed() {
}
@Test
- void testAuthenticationFailedPreviously() {
+ void testAuthenticationFailedPreviously() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -277,7 +278,7 @@ void testAuthenticationFailedPreviously() {
}
@Test
- void testAuthenticationFailure() {
+ void testAuthenticationFailure() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -295,7 +296,7 @@ void testAuthenticationFailure() {
}
@Test
- void testAuthenticationHandshaking() {
+ void testAuthenticationHandshaking() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\""));
@@ -314,7 +315,7 @@ void testAuthenticationHandshaking() {
}
@Test
- void testAuthenticationNoMatchingChallenge() {
+ void testAuthenticationNoMatchingChallenge() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"1234\""));
@@ -342,7 +343,7 @@ void testAuthenticationNoMatchingChallenge() {
}
@Test
- void testAuthenticationException() {
+ void testAuthenticationException() throws AuthenticationException, MalformedChallengeException {
final HttpHost host = new HttpHost("somehost", 80);
final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED");
response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "blah blah blah"));
diff --git a/pom.xml b/pom.xml
index 5b0bff4b95..6404722637 100644
--- a/pom.xml
+++ b/pom.xml
@@ -270,6 +270,8 @@
com.github.siom79.japicmp
japicmp-maven-plugin
+
+ true
${project.groupId}