From f299390cefd74ff96f5b961b3136aa3c62e95d13 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 26 Aug 2024 17:54:47 +0200
Subject: [PATCH 01/13] 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 | 5 +
.../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, 1028 insertions(+), 194 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 bce1873ea9..1df164a8c6 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;
@@ -159,6 +160,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..8a0fa936c5 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 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 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..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 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..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"));
From 60f728c43c0e14361491fbad4e6e4f90d38a9e3c Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 9 Sep 2024 10:15:11 +0200
Subject: [PATCH 02/13] rename AuthSchemeV2 to AuthScheme2, and some internal
GSS variable renames
---
.../hc/client5/http/auth/AuthExchange.java | 2 +-
.../hc/client5/http/auth/AuthScheme.java | 4 +--
.../{AuthSchemeV2.java => AuthScheme2.java} | 10 +++----
.../client5/http/impl/auth/GGSSchemeBase.java | 26 +++++++++----------
.../http/impl/auth/HttpAuthenticator.java | 18 ++++++-------
.../http/impl/auth/KerberosScheme.java | 4 +--
.../client5/http/impl/auth/SPNegoScheme.java | 4 +--
.../http/impl/classic/ProtocolExec.java | 2 +-
8 files changed, 35 insertions(+), 35 deletions(-)
rename httpclient5/src/main/java/org/apache/hc/client5/http/auth/{AuthSchemeV2.java => AuthScheme2.java} (94%)
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 9fb5a5d435..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
@@ -40,7 +40,7 @@ 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.
+ // 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 8a0fa936c5..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
@@ -87,8 +87,8 @@
* and in FAILED state.
*
*
- * This interface cannot correctly handle some authentication methods, like SPENGO.
- * See {@link AuthSchemeV2} for a more capable interface.
+ * This interface cannot correctly handle some authentication methods, like SPNEGO.
+ * See {@link AuthScheme2} for a more capable interface.
*
*
* @since 4.0
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/AuthScheme2.java
similarity index 94%
rename from httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java
rename to httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java
index be7d052cdc..7c380d728e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java
@@ -38,7 +38,7 @@
*
* @since 5.5
*/
-public interface AuthSchemeV2 extends AuthScheme {
+public interface AuthScheme2 extends AuthScheme {
/**
* Processes the given auth challenge. Some authentication schemes may involve multiple
@@ -73,15 +73,15 @@ void processChallenge(
boolean challenged) throws AuthenticationException;
/**
- * The old processChallenge signature is unfit for use in AuthSchemeV2.
+ * 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 AuthSchemeV2.
+ * instead AuthScheme2.
*/
@Override
default void processChallenge(
AuthChallenge authChallenge,
HttpContext context) throws MalformedChallengeException {
- throw new UnsupportedOperationException("on AuthSchemeV2 implementations only the four "
+ throw new UnsupportedOperationException("on AuthScheme2 implementations only the four "
+ "argument processChallenge method can be called");
}
@@ -89,7 +89,7 @@ default void processChallenge(
* 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.
+ * 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.
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 5693e513de..8103513691 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,7 +32,7 @@
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.AuthSchemeV2;
+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;
@@ -62,7 +62,7 @@
*
*/
// FIXME The class name looks like a Typo. Rename in 6.0 ?
-public abstract class GGSSchemeBase implements AuthSchemeV2 {
+public abstract class GGSSchemeBase implements AuthScheme2 {
enum State {
UNINITIATED,
@@ -134,7 +134,7 @@ public void processChallenge(
final byte[] challengeToken = Base64.decodeBase64(authChallenge== null ? null : authChallenge.getValue());
- final String authServer;
+ final String gssHostname;
String hostname = host.getHostName();
if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE){
try {
@@ -143,18 +143,18 @@ public void processChallenge(
}
}
if (config.getStripPort() != KerberosConfig.Option.DISABLE) {
- authServer = hostname;
+ gssHostname = hostname;
} else {
- authServer = hostname + ":" + host.getPort();
+ gssHostname = hostname + ":" + host.getPort();
}
if (LOG.isDebugEnabled()) {
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final String exchangeId = clientContext.getExchangeId();
- LOG.debug("{} GSS init {}", exchangeId, authServer);
+ LOG.debug("{} GSS init {}", exchangeId, gssHostname);
}
try {
- queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, authServer);
+ queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname);
switch (state) {
case UNINITIATED:
if (challenge != NO_TOKEN) {
@@ -232,12 +232,12 @@ protected GSSManager getManager() {
* @since 4.4
*/
protected byte[] generateGSSToken(
- final byte[] input, final Oid oid, final String serviceName, final String authServer) throws GSSException {
+ final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException {
final GSSManager manager = getManager();
- final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE);
+ final GSSName spn = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
if (gssContext == null) {
- gssContext = createGSSContext(manager, oid, serverName, gssCredential);
+ gssContext = createGSSContext(manager, oid, spn, gssCredential);
}
if (input != null) {
return gssContext.initSecContext(input, 0, input.length);
@@ -251,9 +251,9 @@ protected byte[] generateGSSToken(
protected GSSContext createGSSContext(
final GSSManager manager,
final Oid oid,
- final GSSName serverName,
+ final GSSName spn,
final GSSCredential gssCredential) throws GSSException {
- final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential,
+ final GSSContext gssContext = manager.createContext(spn.canonicalize(oid), oid, gssCredential,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) {
@@ -267,7 +267,7 @@ protected GSSContext createGSSContext(
/**
* @since 4.4
*/
- protected abstract byte[] generateToken(byte[] input, String serviceName, String authServer) throws GSSException;
+ protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException;
@Override
public boolean isChallengeComplete() {
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 d373a69914..4fa091006c 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,7 +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.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;
@@ -160,13 +160,13 @@ private boolean checkChallenged(final ChallengeType challengeType, final HttpRes
* 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
+ * @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 AuthSchemeV2) {
- return ((AuthSchemeV2)authScheme).isChallengeExpected();
+ if (authScheme != null && authScheme instanceof AuthScheme2) {
+ return ((AuthScheme2)authScheme).isChallengeExpected();
} else {
return false;
}
@@ -289,8 +289,8 @@ public boolean updateAuthState(
LOG.debug("{} Processing authorization challenge {}", exchangeId, challenge);
}
try {
- if (authScheme instanceof AuthSchemeV2) {
- ((AuthSchemeV2)authScheme).processChallenge(host, challenge, context, challenged);
+ if (authScheme instanceof AuthScheme2) {
+ ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged);
} else {
authScheme.processChallenge(challenge, context);
}
@@ -345,13 +345,13 @@ public boolean updateAuthState(
}
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
+ // 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));
- if (authScheme instanceof AuthSchemeV2) {
- ((AuthSchemeV2)authScheme).processChallenge(host, challenge, context, challenged);
+ if (authScheme instanceof AuthScheme2) {
+ ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged);
} else {
authScheme.processChallenge(challenge, 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 2679b07ece..4a6ccec3e0 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
@@ -63,8 +63,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/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java
index dfa37a8ad6..2ec37febef 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
@@ -64,8 +64,8 @@ public String getName() {
}
@Override
- protected byte[] generateToken(final byte[] input, final String serviceName, final String authServer) throws GSSException {
- return generateGSSToken(input, new Oid(SPNEGO_OID), serviceName, authServer);
+ protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
+ return generateGSSToken(input, new Oid(SPNEGO_OID), gssServiceName, gssHostname);
}
@Override
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 7df0259da9..9c1c7b7eba 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
@@ -221,7 +221,7 @@ public ClassicHttpResponse execute(
EntityUtils.consume(responseEntity);
} else {
execRuntime.disconnectEndpoint();
- // We don't have any connection based AuthSchemeV2 implementations.
+ // 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()) {
From f40809647b684221ab905b9d25fad120a1de5af9 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 9 Sep 2024 10:18:40 +0200
Subject: [PATCH 03/13] rename gss serverName to PeerName (was spn in previous
commit)
---
.../apache/hc/client5/http/impl/auth/GGSSchemeBase.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
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 8103513691..2ccea125cc 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
@@ -234,10 +234,10 @@ protected GSSManager getManager() {
protected byte[] generateGSSToken(
final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException {
final GSSManager manager = getManager();
- final GSSName spn = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
+ final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
if (gssContext == null) {
- gssContext = createGSSContext(manager, oid, spn, gssCredential);
+ gssContext = createGSSContext(manager, oid, peerName, gssCredential);
}
if (input != null) {
return gssContext.initSecContext(input, 0, input.length);
@@ -251,9 +251,9 @@ protected byte[] generateGSSToken(
protected GSSContext createGSSContext(
final GSSManager manager,
final Oid oid,
- final GSSName spn,
+ final GSSName peerName,
final GSSCredential gssCredential) throws GSSException {
- final GSSContext gssContext = manager.createContext(spn.canonicalize(oid), oid, gssCredential,
+ final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential,
GSSContext.DEFAULT_LIFETIME);
gssContext.requestMutualAuth(true);
if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) {
From 35e94516e0c442de0e19b401f7f4ea7ad989c567 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Tue, 10 Sep 2024 09:28:18 +0200
Subject: [PATCH 04/13] don't add SPNEGO to DefaultAuthenticationStrategy
---
.../testing/sync/TestSPNegoScheme.java | 28 +++++++++++++++++++
.../impl/DefaultAuthenticationStrategy.java | 8 ++++--
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
index 170b6e74b9..37a410e0e0 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
@@ -30,7 +30,11 @@
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;
@@ -43,6 +47,7 @@
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.SPNegoScheme;
import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -101,6 +106,22 @@ protected TestSPNegoScheme() {
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())
@@ -282,6 +303,7 @@ public void testDontTryToAuthenticateEndlessly() throws Exception {
.register(StandardAuthScheme.SPNEGO, nsf)
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
t.setDefaultCredentialsProvider(jaasCredentialsProvider);
});
@@ -311,6 +333,7 @@ public void testNoTokenGeneratedError() throws Exception {
.register(StandardAuthScheme.SPNEGO, nsf)
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
t.setDefaultCredentialsProvider(jaasCredentialsProvider);
});
@@ -344,6 +367,7 @@ public void testMutualSuccess() throws Exception {
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
t.setDefaultCredentialsProvider(jaasCredentialsProvider);
});
@@ -376,6 +400,7 @@ public void testMutualFailureNoToken() throws Exception {
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
});
@@ -416,6 +441,7 @@ public void testMutualFailureEstablishedStatusFalse() throws Exception {
.register(StandardAuthScheme.SPNEGO, nsf)
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
});
@@ -456,6 +482,7 @@ public void testMutualFailureMutualStatusFalse() throws Exception {
.register(StandardAuthScheme.SPNEGO, nsf)
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
});
@@ -498,6 +525,7 @@ public void testMutualFailureBadToken() throws Exception {
.build();
configureClient(t -> {
+ t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
});
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 2bd521e009..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
@@ -68,12 +68,14 @@ 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));
+ protected List getSchemePriority() {
+ return DEFAULT_SCHEME_PRIORITY;
+ }
+
@Override
public List select(
final ChallengeType challengeType,
@@ -97,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);
From 303afa9b5ca6bd3377af7581005b1c6b2c8951f0 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 30 Sep 2024 10:38:07 +0200
Subject: [PATCH 05/13] checkstyle fixes
---
.../testing/sync/TestSPNegoScheme.java | 34 +++++++++----------
.../client5/http/impl/auth/GGSSchemeBase.java | 8 ++---
.../http/impl/classic/ProtocolExec.java | 2 +-
3 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
index 37a410e0e0..1347dd9282 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
@@ -153,7 +153,7 @@ private static class SPNEGOMutualService implements HttpRequestHandler {
final boolean sendMutualToken;
final byte[] encodedMutualAuthToken;
- SPNEGOMutualService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken){
+ SPNEGOMutualService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken) {
this.sendMutualToken = sendMutualToken;
this.encodedMutualAuthToken = encodedMutualAuthToken;
}
@@ -170,9 +170,9 @@ public void handle(
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) {
+ } else if (callCount == 2) {
callCount++;
- if(request.getHeader("Authorization").getValue().contains(GOOD_TOKEN_B64)) {
+ 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)));
@@ -293,7 +293,7 @@ public AuthScheme create(final HttpContext context) {
* the server still keep asking for a valid ticket.
*/
@Test
- public void testDontTryToAuthenticateEndlessly() throws Exception {
+ void testDontTryToAuthenticateEndlessly() throws Exception {
configureServer(t -> {
t.register("*", new PleaseNegotiateService());
});
@@ -308,7 +308,7 @@ public void testDontTryToAuthenticateEndlessly() throws Exception {
t.setDefaultCredentialsProvider(jaasCredentialsProvider);
});
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
client().execute(target, httpget, response -> {
@@ -323,7 +323,7 @@ public void testDontTryToAuthenticateEndlessly() throws Exception {
* if no token is generated. Client should be able to deal with this response.
*/
@Test
- public void testNoTokenGeneratedError() throws Exception {
+ void testNoTokenGeneratedError() throws Exception {
configureServer(t -> {
t.register("*", new PleaseNegotiateService());
});
@@ -339,7 +339,7 @@ public void testNoTokenGeneratedError() throws Exception {
});
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
client().execute(target, httpget, response -> {
@@ -354,11 +354,11 @@ public void testNoTokenGeneratedError() throws Exception {
* Test the success case for mutual auth
*/
@Test
- public void testMutualSuccess() throws Exception {
+ void testMutualSuccess() throws Exception {
configureServer(t -> {
t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
});
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true);
final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme);
@@ -388,7 +388,7 @@ public void testMutualSuccess() throws Exception {
* No mutual auth response token sent by server.
*/
@Test
- public void testMutualFailureNoToken() throws Exception {
+ void testMutualFailureNoToken() throws Exception {
configureServer(t -> {
t.register("*", new SPNEGOMutualService(false, null));
});
@@ -407,7 +407,7 @@ public void testMutualFailureNoToken() throws Exception {
final HttpClientContext context = new HttpClientContext();
context.setCredentialsProvider(jaasCredentialsProvider);
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
try {
@@ -430,7 +430,7 @@ public void testMutualFailureNoToken() throws Exception {
* Server sends a "valid" token, but we mock the established status to false
*/
@Test
- public void testMutualFailureEstablishedStatusFalse() throws Exception {
+ void testMutualFailureEstablishedStatusFalse() throws Exception {
configureServer(t -> {
t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
});
@@ -448,7 +448,7 @@ public void testMutualFailureEstablishedStatusFalse() throws Exception {
final HttpClientContext context = new HttpClientContext();
context.setCredentialsProvider(jaasCredentialsProvider);
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
try {
@@ -471,7 +471,7 @@ public void testMutualFailureEstablishedStatusFalse() throws Exception {
* Server sends a "valid" token, but we mock the mutual auth status to false
*/
@Test
- public void testMutualFailureMutualStatusFalse() throws Exception {
+ void testMutualFailureMutualStatusFalse() throws Exception {
configureServer(t -> {
t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES));
});
@@ -489,7 +489,7 @@ public void testMutualFailureMutualStatusFalse() throws Exception {
final HttpClientContext context = new HttpClientContext();
context.setCredentialsProvider(jaasCredentialsProvider);
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
try {
@@ -512,7 +512,7 @@ public void testMutualFailureMutualStatusFalse() throws Exception {
* Server sends a "bad" token, and GSS throws an exception.
*/
@Test
- public void testMutualFailureBadToken() throws Exception {
+ void testMutualFailureBadToken() throws Exception {
configureServer(t -> {
t.register("*", new SPNEGOMutualService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES));
});
@@ -532,7 +532,7 @@ public void testMutualFailureBadToken() throws Exception {
final HttpClientContext context = new HttpClientContext();
context.setCredentialsProvider(jaasCredentialsProvider);
- final HttpHost target = startServer();
+ final HttpHost target = startServer();
final String s = "/path";
final HttpGet httpget = new HttpGet(s);
try {
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 2ccea125cc..d5a714e94b 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
@@ -132,14 +132,14 @@ public void processChallenge(
return;
}
- final byte[] challengeToken = Base64.decodeBase64(authChallenge== null ? null : authChallenge.getValue());
+ final byte[] challengeToken = Base64.decodeBase64(authChallenge == null ? null : authChallenge.getValue());
final String gssHostname;
String hostname = host.getHostName();
- if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE){
+ if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE) {
try {
hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
- } catch (final UnknownHostException ignore){
+ } catch (final UnknownHostException ignore) {
}
}
if (config.getStripPort() != KerberosConfig.Option.DISABLE) {
@@ -171,7 +171,7 @@ public void processChallenge(
case TOKEN_SENT:
if (challenged) {
state = State.TOKEN_READY;
- } else if (mutualAuth){
+ } else if (mutualAuth) {
// We should have received a valid mutualAuth token
if (!gssContext.isEstablished()) {
if (LOG.isDebugEnabled()) {
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 9c1c7b7eba..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
@@ -270,7 +270,7 @@ private boolean needAuthentication(
final HttpHost target,
final String pathPrefix,
final HttpResponse response,
- final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
+ final HttpClientContext context) throws AuthenticationException, MalformedChallengeException {
final RequestConfig config = context.getRequestConfigOrDefault();
if (config.isAuthenticationEnabled()) {
final boolean targetAuthRequested = authenticator.isChallenged(
From 4da4b14724e530e60f45d34335e15e1c83fafbbc Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 30 Sep 2024 10:44:57 +0200
Subject: [PATCH 06/13] fix some deprecation warnings
---
.../apache/hc/client5/http/impl/auth/GGSSchemeBase.java | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
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 d5a714e94b..245fbb7333 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
@@ -149,7 +149,7 @@ public void processChallenge(
}
if (LOG.isDebugEnabled()) {
- final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ final HttpClientContext clientContext = HttpClientContext.cast(context);
final String exchangeId = clientContext.getExchangeId();
LOG.debug("{} GSS init {}", exchangeId, gssHostname);
}
@@ -159,7 +159,7 @@ public void processChallenge(
case UNINITIATED:
if (challenge != NO_TOKEN) {
if (LOG.isDebugEnabled()) {
- final HttpClientContext clientContext = HttpClientContext.adapt(context);
+ 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);
}
@@ -176,7 +176,7 @@ public void processChallenge(
if (!gssContext.isEstablished()) {
if (LOG.isDebugEnabled()) {
final HttpClientContext clientContext =
- HttpClientContext.adapt(context);
+ HttpClientContext.cast(context);
final String exchangeId = clientContext.getExchangeId();
LOG.debug("{} GSSContext is not established ", exchangeId);
}
@@ -187,7 +187,7 @@ public void processChallenge(
} else if (!gssContext.getMutualAuthState()) {
if (LOG.isDebugEnabled()) {
final HttpClientContext clientContext =
- HttpClientContext.adapt(context);
+ HttpClientContext.cast(context);
final String exchangeId = clientContext.getExchangeId();
LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have"
+ " mutualAuthState set", exchangeId);
From 51148f08a35c0bf92bc45ae064e7ca48e20cc8f8 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 30 Sep 2024 11:08:38 +0200
Subject: [PATCH 07/13] Re-deprecate KerberosScheme
---
.../apache/hc/client5/http/impl/auth/KerberosScheme.java | 6 ++++++
1 file changed, 6 insertions(+)
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 4a6ccec3e0..5b00e0fe19 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,7 +40,13 @@
*
*
* @since 4.2
+ *
+ * @deprecated Do not use. Consider using Spengo, Basic or Bearer authentication with TLS instead.
+ *
+ * @see BasicScheme
+ * @see BearerScheme
*/
+@Deprecated
@Experimental
public class KerberosScheme extends GGSSchemeBase {
From 6e84108f613e0d25cc842129f416829ec5da78d2 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 30 Sep 2024 13:54:14 +0200
Subject: [PATCH 08/13] duplicate and rename mutual SPNEGO related classes
---
...cheme.java => TestMutualSpnegoScheme.java} | 10 +-
.../client5/http/impl/auth/GGSSchemeBase.java | 227 ++++--------
.../http/impl/auth/KerberosScheme.java | 4 +-
.../http/impl/auth/KerberosSchemeFactory.java | 5 +-
.../http/impl/auth/MutualGssSchemeBase.java | 348 ++++++++++++++++++
.../http/impl/auth/MutualSpnegoScheme.java | 113 ++++++
.../impl/auth/MutualSpnegoSchemeFactory.java | 76 ++++
.../client5/http/impl/auth/SPNegoScheme.java | 15 +-
.../http/impl/auth/SPNegoSchemeFactory.java | 8 +-
9 files changed, 639 insertions(+), 167 deletions(-)
rename httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/{TestSPNegoScheme.java => TestMutualSpnegoScheme.java} (99%)
create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java
create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java
create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
similarity index 99%
rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
index 1347dd9282..55b5c08c15 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
@@ -49,7 +49,7 @@
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.SPNegoScheme;
+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;
@@ -80,9 +80,9 @@
/**
* Tests for {@link SPNegoScheme}.
*/
-public class TestSPNegoScheme extends AbstractIntegrationTestBase {
+public class TestMutualSpnegoScheme extends AbstractIntegrationTestBase {
- protected TestSPNegoScheme() {
+ protected TestMutualSpnegoScheme() {
super(URIScheme.HTTP, ClientProtocolLevel.STANDARD);
}
@@ -191,7 +191,7 @@ public void handle(
* Kerberos configuration.
*
*/
- private static class NegotiateSchemeWithMockGssManager extends SPNegoScheme {
+ private static class NegotiateSchemeWithMockGssManager extends MutualSpnegoScheme {
final GSSManager manager = Mockito.mock(GSSManager.class);
final GSSName name = Mockito.mock(GSSName.class);
@@ -218,7 +218,7 @@ protected GSSManager getManager() {
}
- private static class MutualNegotiateSchemeWithMockGssManager extends SPNegoScheme {
+ private static class MutualNegotiateSchemeWithMockGssManager extends MutualSpnegoScheme {
final GSSManager manager = Mockito.mock(GSSManager.class);
final GSSName name = Mockito.mock(GSSName.class);
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 245fbb7333..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
@@ -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.AuthScheme2;
+import org.apache.hc.client5.http.auth.AuthScheme;
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,51 +60,45 @@
*
* @since 4.2
*
+ * @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}.
*/
-// FIXME The class name looks like a Typo. Rename in 6.0 ?
-public abstract class GGSSchemeBase implements AuthScheme2 {
+@Deprecated
+public abstract class GGSSchemeBase implements AuthScheme {
enum State {
UNINITIATED,
- TOKEN_READY,
- TOKEN_SENT,
- SUCCEEDED,
+ CHALLENGE_RECEIVED,
+ TOKEN_GENERATED,
FAILED,
}
private static final Logger LOG = LoggerFactory.getLogger(GGSSchemeBase.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 org.apache.hc.client5.http.auth.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];
+ private byte[] token;
- GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) {
+ GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) {
super();
- this.config = config != null ? config : KerberosConfig.DEFAULT;
+ this.config = config != null ? config : org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT;
this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
- this.mutualAuth = config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE;
this.state = State.UNINITIATED;
}
- GGSSchemeBase(final KerberosConfig config) {
+ GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config) {
this(config, SystemDefaultDnsResolver.INSTANCE);
}
GGSSchemeBase() {
- this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
+ this(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE);
}
@Override
@@ -112,115 +106,24 @@ 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 {
+ final HttpContext context) throws MalformedChallengeException {
+ Args.notNull(authChallenge, "AuthChallenge");
+
+ this.challenge = authChallenge.getValue() != null ? authChallenge.getValue() : NO_TOKEN;
- if (challengesLeft-- <= 0 ) {
+ if (state == State.UNINITIATED) {
+ token = Base64.decodeBase64(challenge.getBytes());
+ state = State.CHALLENGE_RECEIVED;
+ } else {
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);
+ LOG.debug("{} Authentication already attempted", 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);
}
}
@@ -232,13 +135,11 @@ protected GSSManager getManager() {
* @since 4.4
*/
protected byte[] generateGSSToken(
- final byte[] input, final Oid oid, final String gssServiceName, final String gssHostname) throws GSSException {
+ final byte[] input, final Oid oid, final String serviceName, final String authServer) throws GSSException {
final GSSManager manager = getManager();
- final GSSName peerName = manager.createName(gssServiceName + "@" + gssHostname, GSSName.NT_HOSTBASED_SERVICE);
+ final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE);
- if (gssContext == null) {
- gssContext = createGSSContext(manager, oid, peerName, gssCredential);
- }
+ final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential);
if (input != null) {
return gssContext.initSecContext(input, 0, input.length);
}
@@ -251,35 +152,24 @@ protected byte[] generateGSSToken(
protected GSSContext createGSSContext(
final GSSManager manager,
final Oid oid,
- final GSSName peerName,
+ final GSSName serverName,
final GSSCredential gssCredential) throws GSSException {
- final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential,
+ final GSSContext gssContext = manager.createContext(serverName.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);
+ 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);
}
return gssContext;
}
/**
* @since 4.4
*/
- protected abstract byte[] generateToken(byte[] input, String gssServiceName, String gssHostname) throws GSSException;
+ protected abstract byte[] generateToken(byte[] input, String serviceName, String authServer) 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;
+ return this.state == State.TOKEN_GENERATED || this.state == State.FAILED;
}
@Override
@@ -306,8 +196,6 @@ 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,
@@ -320,16 +208,53 @@ public String generateAuthResponse(
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;
+ 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:
final Base64 codec = new Base64(0);
- final String tokenstr = new String(codec.encode(queuedToken));
+ final String tokenstr = new String(codec.encode(token));
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);
+ LOG.debug("{} Sending 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/KerberosScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java
index 5b00e0fe19..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,8 +41,10 @@
*
* @since 4.2
*
- * @deprecated Do not use. Consider using Spengo, 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
*/
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 2ec37febef..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,12 +36,19 @@
* 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 Use {@link MutualSpnegoScheme} or some other auth scheme instead.
+ *
+ * @see MutualSpnegoScheme
+ * @see BasicScheme
+ * @see BearerScheme
*/
+@Deprecated
@Experimental
public class SPNegoScheme extends GGSSchemeBase {
@@ -64,8 +71,8 @@ public String getName() {
}
@Override
- protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
- return generateGSSToken(input, new Oid(SPNEGO_OID), gssServiceName, gssHostname);
+ protected byte[] generateToken(final byte[] input, final String serviceName, final String authServer) throws GSSException {
+ return generateGSSToken(input, new Oid(SPNEGO_OID), serviceName, authServer);
}
@Override
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
*/
From 0b9d3a00d16dc7bacb0d80d9533088f7d033495b Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Mon, 30 Sep 2024 13:57:51 +0200
Subject: [PATCH 09/13] re-deprecate StandardAuthScheme.KERBEROS
---
.../org/apache/hc/client5/http/auth/StandardAuthScheme.java | 4 ++++
1 file changed, 4 insertions(+)
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 4ede224e2b..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
@@ -71,7 +71,11 @@ private StandardAuthScheme() {
/**
* Kerberos authentication scheme as defined in RFC 4120.
+ *
+ * @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";
}
From 3d2c7b1877e6f9d58d35562fcb731727e834a88e Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Sat, 19 Oct 2024 16:41:52 +0200
Subject: [PATCH 10/13] temporarily disable japicmp, and mark HttpAuthenticator
@Internal
---
.../org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java | 2 ++
pom.xml | 2 ++
2 files changed, 4 insertions(+)
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 4fa091006c..4511029783 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
@@ -45,6 +45,7 @@
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;
@@ -70,6 +71,7 @@
*
* @since 4.3
*/
+@Internal
@Contract(threading = ThreadingBehavior.STATELESS)
public final class HttpAuthenticator {
diff --git a/pom.xml b/pom.xml
index 9cc5cda643..69460bc5e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -270,6 +270,8 @@
com.github.siom79.japicmp
japicmp-maven-plugin
+
+ true
${project.groupId}
From 6529528509d75ff176bf610890deca8041ba85a2 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Thu, 24 Oct 2024 15:08:58 +0200
Subject: [PATCH 11/13] slight change in logic, it is equivalent, but more
readable.
---
.../org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 4511029783..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
@@ -302,7 +302,7 @@ public boolean updateAuthState(
}
authExchange.reset();
authExchange.setState(AuthExchange.State.FAILURE);
- if (!challenged) {
+ if (isChallengeExpected) {
throw ex;
}
}
From fc76402893624cd5f2785bb7132fa26a4fb4eb4f Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Tue, 21 Jan 2025 07:13:57 +0100
Subject: [PATCH 12/13] remove MutualSpnegoScheme and related files and changes
---
.../testing/sync/TestMutualSpnegoScheme.java | 553 ------------------
.../hc/client5/http/auth/AuthScheme2.java | 4 +-
.../hc/client5/http/auth/KerberosConfig.java | 36 +-
.../http/auth/KerberosCredentials.java | 7 +
.../client5/http/auth/StandardAuthScheme.java | 8 +-
.../client5/http/impl/auth/GGSSchemeBase.java | 5 +-
.../http/impl/auth/KerberosScheme.java | 9 +-
.../http/impl/auth/KerberosSchemeFactory.java | 5 +-
.../http/impl/auth/MutualGssSchemeBase.java | 348 -----------
.../http/impl/auth/MutualSpnegoScheme.java | 113 ----
.../impl/auth/MutualSpnegoSchemeFactory.java | 76 ---
.../client5/http/impl/auth/SPNegoScheme.java | 8 +-
.../http/impl/auth/SPNegoSchemeFactory.java | 8 +-
13 files changed, 42 insertions(+), 1138 deletions(-)
delete mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
delete mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java
delete mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java
delete mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
deleted file mode 100644
index 55b5c08c15..0000000000
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * ====================================================================
- * 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.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/AuthScheme2.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java
index 7c380d728e..12845d3b74 100644
--- 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
@@ -53,7 +53,7 @@ public interface AuthScheme2 extends AuthScheme {
* 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
+ * AuthenticationException immediately even when no response is sent (i.e. processing an SPNEGO or SCRAM mutual
* authentication response)
*
* When {@link isChallengeExpected} returns true, but no challenge was sent, then this method must
@@ -92,7 +92,7 @@ default void processChallenge(
* 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.
+ * This is required to implement the SPENGO RFC and Kerberos mutual authentication, and SCRAM.
*
* @return true if responses with non 401/407 response codes must be processed by the scheme.
* @since 5.5
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 2793b8e1f7..508eeb9b0e 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,7 +35,11 @@
*
* @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 {
@@ -49,28 +53,25 @@ public enum Option {
public static final KerberosConfig DEFAULT = new Builder().build();
- 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
+ private final Option stripPort;
+ private final Option useCanonicalHostname;
+ private final Option requestDelegCreds;
/**
* Intended for CDI compatibility
*/
protected KerberosConfig() {
- this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT, Option.DEFAULT);
+ this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT);
}
KerberosConfig(
final Option stripPort,
final Option useCanonicalHostname,
- final Option requestDelegCreds,
- final Option requestMutualAuth) {
+ final Option requestDelegCreds) {
super();
this.stripPort = stripPort;
this.useCanonicalHostname = useCanonicalHostname;
this.requestDelegCreds = requestDelegCreds;
- this.requestMutualAuth = requestMutualAuth;
}
public Option getStripPort() {
@@ -85,10 +86,6 @@ public Option getRequestDelegCreds() {
return requestDelegCreds;
}
- public Option getRequestMutualAuth() {
- return requestMutualAuth;
- }
-
@Override
protected KerberosConfig clone() throws CloneNotSupportedException {
return (KerberosConfig) super.clone();
@@ -101,7 +98,6 @@ 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();
}
@@ -114,9 +110,7 @@ public static KerberosConfig.Builder copy(final KerberosConfig config) {
return new Builder()
.setStripPort(config.getStripPort())
.setUseCanonicalHostname(config.getUseCanonicalHostname())
- .setRequestDelegCreds(config.getRequestDelegCreds())
- .setRequestMutualAuth(config.getRequestMutualAuth()
- );
+ .setRequestDelegCreds(config.getRequestDelegCreds());
}
public static class Builder {
@@ -124,14 +118,12 @@ 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) {
@@ -159,17 +151,11 @@ 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,
- requestMutualAuth);
+ requestDelegCreds);
}
}
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 e40963b2a8..92bab8d4f3 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,7 +37,14 @@
* 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 79232cac10..1345282c0b 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,14 +66,18 @@ 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 Kerberos scheme was never standardized, and its
- * implementation uses the old deprecated non mutual auth capable logic.
+ * @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/auth/GGSSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java
index 2953416f34..773746b612 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,9 +60,8 @@
*
* @since 4.2
*
- * @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 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 {
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 9b2adb96d2..656f29633a 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,10 +41,9 @@
*
* @since 4.2
*
- * @deprecated Do not use. The Kerberos authentication scheme was never standardised.
- * Use {@link MutualSpnegoScheme} or some other scheme instead.
+ * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
+ * supported. Consider using Basic or Bearer authentication with TLS instead.
*
- * @see MutualSpnegoScheme
* @see BasicScheme
* @see BearerScheme
*/
@@ -71,8 +70,8 @@ public String getName() {
}
@Override
- protected byte[] generateToken(final byte[] input, final String gssServiceName, final String gssHostname) throws GSSException {
- return generateGSSToken(input, new Oid(KERBEROS_OID), gssServiceName, gssHostname);
+ protected byte[] generateToken(final byte[] input, final String serviceName, final String authServer) throws GSSException {
+ return generateGSSToken(input, new Oid(KERBEROS_OID), serviceName, authServer);
}
@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 9c6cf9349a..25930f0997 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,10 +45,9 @@
*
* @since 4.2
*
- * @deprecated Do not use. The Kerberos authentication scheme was never standardised.
- * Use {@link MutualSpnegoScheme} or some other scheme instead.
+ * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
+ * supported. Consider using Basic or Bearer authentication with TLS 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
deleted file mode 100644
index 9fbee575a6..0000000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * ====================================================================
- * 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
deleted file mode 100644
index 5403912ba0..0000000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * ====================================================================
- * 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
deleted file mode 100644
index 5880f47adc..0000000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * ====================================================================
- * 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 6d9f9408fe..7971ff935d 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.
*
- * This class implements the old deprecated non mutual authentication capable SPNEGO implementation.
- * Use {@link MutualSpnegoScheme} instead.
+ * Please note this class is considered experimental and may be discontinued or removed
+ * in the future.
*
*
* @since 4.2
*
- * @deprecated Use {@link MutualSpnegoScheme} or some other auth scheme instead.
+ * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
+ * supported. Consider using Basic or Bearer authentication with TLS 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 1231d47e49..14d8528c5e 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.
*
- * This factory creates the old deprecated non mutual authentication capable SPNEGO implementation.
- * Use {@link MutualSpnegoAuthFactory} instead.
+ * Please note this class is considered experimental and may be discontinued or removed
+ * in the future.
*
*
* @since 4.2
*
- * @deprecated Use {@link MutualSpnegoAuthFactory} or some other auth scheme instead.
+ * @deprecated Do not use. The GGS based experimental authentication schemes are no longer
+ * supported. Consider using Basic or Bearer authentication with TLS instead.
*
- * @see MutualSpnegoAuthFactory
* @see BasicSchemeFactory
* @see BearerSchemeFactory
*/
From 0520adb26ff708a06152e20f31af99c05c42f7e0 Mon Sep 17 00:00:00 2001
From: Istvan Toth
Date: Tue, 21 Jan 2025 10:41:25 +0100
Subject: [PATCH 13/13] comment fixes
---
.../hc/client5/http/impl/auth/HttpAuthenticator.java | 8 ++++----
.../apache/hc/client5/http/impl/classic/ProtocolExec.java | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
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 1deab416c6..9b4553cd37 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
@@ -228,7 +228,7 @@ public Map extractChallengeMap(final ChallengeType challe
* @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 ,
+ * @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
@@ -428,7 +428,7 @@ public void addAuthResponse(
request.addHeader(header);
}
break;
- } catch (final AuthenticationException ex ) {
+ } catch (final AuthenticationException ex) {
if (LOG.isWarnEnabled()) {
LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
}
@@ -440,8 +440,8 @@ public void addAuthResponse(
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.
+ // This only happens if the handshake requires multiple requests, which is
+ // 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/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java
index 41517e2e6f..976c2da5e4 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
@@ -191,7 +191,7 @@ public ClassicHttpResponse execute(
authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
}
- //The is where the actual network communications happens (eventually)
+ // This is where the actual network communication happens (eventually)
final ClassicHttpResponse response = chain.proceed(request, scope);
if (Method.TRACE.isSame(request.getMethod())) {