From 01354e29da66e65573b314ba7873e81ee184a5c0 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 26 Aug 2024 17:54:47 +0200
Subject: [PATCH] HTTPCLIENT-1625 Completely overhaul GSS-API-based
authentication backend
---
.../async/StandardTestClientBuilder.java | 7 +
.../async/TestAsyncClientBuilder.java | 5 +
.../sync/StandardTestClientBuilder.java | 7 +
.../extension/sync/TestClientBuilder.java | 7 +-
.../testing/sync/TestSPNegoScheme.java | 525 ++++++++++++++++++
.../hc/client5/http/auth/AuthExchange.java | 3 +
.../hc/client5/http/auth/AuthScheme.java | 7 +
.../hc/client5/http/auth/AuthSchemeV2.java | 102 ++++
.../hc/client5/http/auth/KerberosConfig.java | 36 +-
.../http/auth/KerberosCredentials.java | 7 -
.../client5/http/auth/StandardAuthScheme.java | 8 -
.../impl/DefaultAuthenticationStrategy.java | 2 +
.../http/impl/async/AsyncConnectExec.java | 7 +-
.../http/impl/async/AsyncProtocolExec.java | 10 +-
.../client5/http/impl/auth/GGSSchemeBase.java | 216 ++++---
.../http/impl/auth/HttpAuthenticator.java | 217 +++++---
.../http/impl/auth/KerberosScheme.java | 7 -
.../client5/http/impl/auth/SPNegoScheme.java | 7 -
.../http/impl/classic/ConnectExec.java | 3 +-
.../http/impl/classic/ProtocolExec.java | 15 +-
.../http/impl/classic/ProxyClient.java | 3 +-
.../http/impl/auth/TestHttpAuthenticator.java | 23 +-
22 files changed, 1029 insertions(+), 195 deletions(-)
create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java
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 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.auth.CredentialsProviderBuilder;
+import org.apache.hc.client5.http.impl.auth.SPNegoScheme;
+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 TestSPNegoScheme extends AbstractIntegrationTestBase {
+
+ protected TestSPNegoScheme() {
+ 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();
+
+
+ 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 SPNegoScheme {
+
+ 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 SPNegoScheme {
+
+ 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
+ public 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.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
+ public 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.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
+ public 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.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
+ public 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.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
+ public 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.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
+ public 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.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
+ public 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.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..9fb5a5d435 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 AuthSchemeV2 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..290df24e9d 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 corrently handle some authentication methods, like SPENGO.
+ * See {@link AuthSchemeV2} 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/AuthSchemeV2.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java
new file mode 100644
index 0000000000..be7d052cdc
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.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 AuthSchemeV2 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 AuthSchemeV2.
+ * If the old signature is sufficient for a scheme, then it should implement {@link AuthScheme}
+ * instead AuthSchemeV2.
+ */
+ @Override
+ default void processChallenge(
+ AuthChallenge authChallenge,
+ HttpContext context) throws MalformedChallengeException {
+ throw new UnsupportedOperationException("on AuthSchemeV2 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 AuthScheme(V1) 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..4ede224e2b 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,20 +66,12 @@ 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
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..2bd521e009 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
@@ -68,6 +68,8 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy {
private static final List DEFAULT_SCHEME_PRIORITY =
Collections.unmodifiableList(Arrays.asList(
+ StandardAuthScheme.SPNEGO,
+ StandardAuthScheme.KERBEROS,
StandardAuthScheme.BEARER,
StandardAuthScheme.DIGEST,
StandardAuthScheme.BASIC));
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 c78a7db29a..47e4d9ed0e 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;
@@ -501,10 +503,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) {
@@ -514,7 +517,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 aa3bb94114..5693e513de 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
@@ -32,14 +32,14 @@
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.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeV2;
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.MalformedChallengeException;
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;
@@ -60,44 +60,51 @@
*
* @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
-public abstract class GGSSchemeBase implements AuthScheme {
+// FIXME The class name looks like a Typo. Rename in 6.0 ?
+public abstract class GGSSchemeBase implements AuthSchemeV2 {
enum State {
UNINITIATED,
- CHALLENGE_RECEIVED,
- TOKEN_GENERATED,
+ TOKEN_READY,
+ TOKEN_SENT,
+ SUCCEEDED,
FAILED,
}
private static final Logger LOG = LoggerFactory.getLogger(GGSSchemeBase.class);
private static final String NO_TOKEN = "";
private static final String KERBEROS_SCHEME = "HTTP";
- private final org.apache.hc.client5.http.auth.KerberosConfig config;
+
+ // 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[] token;
+ private byte[] queuedToken = new byte[0];
- GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) {
+ GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) {
super();
- this.config = config != null ? config : org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT;
+ 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;
}
- GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config) {
+ GGSSchemeBase(final KerberosConfig config) {
this(config, SystemDefaultDnsResolver.INSTANCE);
}
GGSSchemeBase() {
- this(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
}
@Override
@@ -105,24 +112,115 @@ 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) throws MalformedChallengeException {
- Args.notNull(authChallenge, "AuthChallenge");
-
- this.challenge = authChallenge.getValue() != null ? authChallenge.getValue() : NO_TOKEN;
+ final HttpContext context,
+ final boolean challenged) throws AuthenticationException {
- if (state == State.UNINITIATED) {
- token = Base64.decodeBase64(challenge.getBytes());
- state = State.CHALLENGE_RECEIVED;
- } else {
+ if (challengesLeft-- <= 0 ) {
if (LOG.isDebugEnabled()) {
final HttpClientContext clientContext = HttpClientContext.cast(context);
final String exchangeId = clientContext.getExchangeId();
- LOG.debug("{} Authentication already attempted", exchangeId);
+ 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 authServer;
+ 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) {
+ authServer = hostname;
+ } else {
+ authServer = hostname + ":" + host.getPort();
+ }
+
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final String exchangeId = clientContext.getExchangeId();
+ LOG.debug("{} GSS init {}", exchangeId, authServer);
+ }
+ try {
+ queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, authServer);
+ switch (state) {
+ case UNINITIATED:
+ if (challenge != NO_TOKEN) {
+ if (LOG.isDebugEnabled()) {
+ final HttpClientContext clientContext = HttpClientContext.adapt(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.adapt(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.adapt(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);
}
}
@@ -138,7 +236,9 @@ protected byte[] generateGSSToken(
final GSSManager manager = getManager();
final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE);
- final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential);
+ if (gssContext == null) {
+ gssContext = createGSSContext(manager, oid, serverName, gssCredential);
+ }
if (input != null) {
return gssContext.initSecContext(input, 0, input.length);
}
@@ -156,8 +256,11 @@ protected GSSContext createGSSContext(
final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
- if (config.getRequestDelegCreds() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DEFAULT) {
- gssContext.requestCredDeleg(config.getRequestDelegCreds() == org.apache.hc.client5.http.auth.KerberosConfig.Option.ENABLE);
+ 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;
}
@@ -168,7 +271,15 @@ protected GSSContext createGSSContext(
@Override
public boolean isChallengeComplete() {
- return this.state == State.TOKEN_GENERATED || this.state == State.FAILED;
+ // 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
@@ -195,6 +306,8 @@ 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,
@@ -207,53 +320,16 @@ public String generateAuthResponse(
throw new AuthenticationException(getName() + " authentication has not been initiated");
case FAILED:
throw new AuthenticationException(getName() + " authentication has failed");
- case CHALLENGE_RECEIVED:
- try {
- final String authServer;
- String hostname = host.getHostName();
- if (config.getUseCanonicalHostname() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE){
- try {
- hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
- } catch (final UnknownHostException ignore){
- }
- }
- if (config.getStripPort() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE) {
- authServer = hostname;
- } else {
- authServer = hostname + ":" + host.getPort();
- }
-
- if (LOG.isDebugEnabled()) {
- final HttpClientContext clientContext = HttpClientContext.cast(context);
- final String exchangeId = clientContext.getExchangeId();
- LOG.debug("{} init {}", exchangeId, authServer);
- }
- token = generateToken(token, KERBEROS_SCHEME, authServer);
- state = State.TOKEN_GENERATED;
- } 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());
- }
- case TOKEN_GENERATED:
+ case SUCCEEDED:
+ return null;
+ case TOKEN_READY:
+ state = State.TOKEN_SENT;
final Base64 codec = new Base64(0);
- final String tokenstr = new String(codec.encode(token));
+ 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 response '{}' back to the auth server", exchangeId, tokenstr);
+ LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr);
}
return StandardAuthScheme.SPNEGO + " " + tokenstr;
default:
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..d373a69914 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,6 +38,7 @@
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.AuthSchemeV2;
import org.apache.hc.client5.http.auth.AuthenticationException;
import org.apache.hc.client5.http.auth.ChallengeType;
import org.apache.hc.client5.http.auth.CredentialsProvider;
@@ -81,12 +82,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 +99,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 +122,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 AuthExchangeV2, 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 AuthSchemeV2) {
+ return ((AuthSchemeV2)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 +200,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 AuthSchemeV2) {
+ ((AuthSchemeV2)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 (!challenged) {
+ throw ex;
+ }
}
if (authScheme.isChallengeComplete()) {
if (LOG.isDebugEnabled()) {
@@ -241,7 +312,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 +327,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 +344,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
+ // AuthScheme(V1) 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 AuthSchemeV2) {
+ ((AuthSchemeV2)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 +419,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 +437,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..2679b07ece 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
@@ -40,14 +40,7 @@
*
*
* @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.
- *
- * @see BasicScheme
- * @see BearerScheme
*/
-@Deprecated
@Experimental
public class KerberosScheme extends GGSSchemeBase {
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..dfa37a8ad6 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
@@ -41,14 +41,7 @@
*
*
* @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.
- *
- * @see BasicScheme
- * @see BearerScheme
*/
-@Deprecated
@Experimental
public class SPNegoScheme extends GGSSchemeBase {
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 1c5cbab752..93a85e194c 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
@@ -251,6 +251,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) {
@@ -260,7 +261,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..7df0259da9 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 AuthSchemeV2 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"));