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())) {