From 01354e29da66e65573b314ba7873e81ee184a5c0 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Mon, 26 Aug 2024 17:54:47 +0200 Subject: [PATCH] HTTPCLIENT-1625 Completely overhaul GSS-API-based authentication backend --- .../async/StandardTestClientBuilder.java | 7 + .../async/TestAsyncClientBuilder.java | 5 + .../sync/StandardTestClientBuilder.java | 7 + .../extension/sync/TestClientBuilder.java | 7 +- .../testing/sync/TestSPNegoScheme.java | 525 ++++++++++++++++++ .../hc/client5/http/auth/AuthExchange.java | 3 + .../hc/client5/http/auth/AuthScheme.java | 7 + .../hc/client5/http/auth/AuthSchemeV2.java | 102 ++++ .../hc/client5/http/auth/KerberosConfig.java | 36 +- .../http/auth/KerberosCredentials.java | 7 - .../client5/http/auth/StandardAuthScheme.java | 8 - .../impl/DefaultAuthenticationStrategy.java | 2 + .../http/impl/async/AsyncConnectExec.java | 7 +- .../http/impl/async/AsyncProtocolExec.java | 10 +- .../client5/http/impl/auth/GGSSchemeBase.java | 216 ++++--- .../http/impl/auth/HttpAuthenticator.java | 217 +++++--- .../http/impl/auth/KerberosScheme.java | 7 - .../client5/http/impl/auth/SPNegoScheme.java | 7 - .../http/impl/classic/ConnectExec.java | 3 +- .../http/impl/classic/ProtocolExec.java | 15 +- .../http/impl/classic/ProxyClient.java | 3 +- .../http/impl/auth/TestHttpAuthenticator.java | 23 +- 22 files changed, 1029 insertions(+), 195 deletions(-) create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSPNegoScheme.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java index 24f29dd670..b5e85f192a 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java @@ -33,6 +33,7 @@ import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.UserTokenHandler; import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; @@ -153,6 +154,12 @@ public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup. + * + */ +package org.apache.hc.client5.testing.sync; + + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.AuthenticationException; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.KerberosConfig; +import org.apache.hc.client5.http.auth.KerberosConfig.Option; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; +import org.apache.hc.client5.http.impl.auth.SPNegoScheme; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.http.utils.Base64; +import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.apache.hc.core5.http.io.HttpRequestHandler; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.Timeout; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.AdditionalMatchers; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +/** + * Tests for {@link SPNegoScheme}. + */ +public class TestSPNegoScheme extends AbstractIntegrationTestBase { + + protected TestSPNegoScheme() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD); + } + + public static final Timeout TIMEOUT = Timeout.ofMinutes(1); + + private static final String GOOD_TOKEN = "GOOD_TOKEN"; + private static final byte[] GOOD_TOKEN_BYTES = GOOD_TOKEN.getBytes(StandardCharsets.UTF_8); + private static final byte[] GOOD_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_TOKEN_BYTES); + private static final String GOOD_TOKEN_B64 = new String(GOOD_TOKEN_B64_BYTES); + + private static final String NO_TOKEN = ""; + private static final byte[] NO_TOKEN_BYTES = NO_TOKEN.getBytes(StandardCharsets.UTF_8); + + private static final String GOOD_MUTUAL_AUTH_TOKEN = "GOOD_MUTUAL_AUTH_TOKEN"; + private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_BYTES = GOOD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8); + private static final byte[] GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(GOOD_MUTUAL_AUTH_TOKEN_BYTES); + + private static final String BAD_MUTUAL_AUTH_TOKEN = "BAD_MUTUAL_AUTH_TOKEN"; + private static final byte[] BAD_MUTUAL_AUTH_TOKEN_BYTES = BAD_MUTUAL_AUTH_TOKEN.getBytes(StandardCharsets.UTF_8); + private static final byte[] BAD_MUTUAL_AUTH_TOKEN_B64_BYTES = Base64.encodeBase64(BAD_MUTUAL_AUTH_TOKEN_BYTES); + + static KerberosConfig MUTUAL_KERBEROS_CONFIG = KerberosConfig.custom().setRequestMutualAuth(Option.ENABLE).build(); + + + final CredentialsProvider jaasCredentialsProvider = CredentialsProviderBuilder.create() + .add(new AuthScope(null, null, -1, null, null), new UseJaasCredentials()) + .build(); + + /** + * This service will continue to ask for authentication. + */ + private static class PleaseNegotiateService implements HttpRequestHandler { + + @Override + public void handle( + final ClassicHttpRequest request, + final ClassicHttpResponse response, + final HttpContext context) throws HttpException, IOException { + response.setCode(HttpStatus.SC_UNAUTHORIZED); + response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " blablabla")); + response.addHeader(new BasicHeader("Connection", "Keep-Alive")); + response.setEntity(new StringEntity("auth required ")); + } + } + + /** + * This service implements a normal mutualAuth flow + */ + private static class SPNEGOMutualService implements HttpRequestHandler { + + int callCount = 1; + final boolean sendMutualToken; + final byte[] encodedMutualAuthToken; + + SPNEGOMutualService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken){ + this.sendMutualToken = sendMutualToken; + this.encodedMutualAuthToken = encodedMutualAuthToken; + } + + @Override + public void handle( + final ClassicHttpRequest request, + final ClassicHttpResponse response, + final HttpContext context) throws HttpException, IOException { + if (callCount == 1) { + callCount++; + // Send the empty challenge + response.setCode(HttpStatus.SC_UNAUTHORIZED); + response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO)); + response.addHeader(new BasicHeader("Connection", "Keep-Alive")); + response.setEntity(new StringEntity("auth required ")); + } else if(callCount == 2) { + callCount++; + if(request.getHeader("Authorization").getValue().contains(GOOD_TOKEN_B64)) { + response.setCode(HttpStatus.SC_OK); + if (sendMutualToken) { + response.addHeader(new BasicHeader("WWW-Authenticate", StandardAuthScheme.SPNEGO + " " + new String(encodedMutualAuthToken))); + } + response.addHeader(new BasicHeader("Connection", "Keep-Alive")); + response.setEntity(new StringEntity("auth successful ")); + } else { + response.setCode(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } + } + } + } + + /** + * NegotatieScheme with a custom GSSManager that does not require any Jaas or + * Kerberos configuration. + * + */ + private static class NegotiateSchemeWithMockGssManager extends SPNegoScheme { + + final GSSManager manager = Mockito.mock(GSSManager.class); + final GSSName name = Mockito.mock(GSSName.class); + final GSSContext context = Mockito.mock(GSSContext.class); + + NegotiateSchemeWithMockGssManager() throws Exception { + super(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + Mockito.when(context.initSecContext( + ArgumentMatchers.any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + .thenReturn("12345678".getBytes()); + Mockito.when(manager.createName( + ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(name); + Mockito.when(manager.createContext( + ArgumentMatchers.any(), ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt())) + .thenReturn(context); + } + + @Override + protected GSSManager getManager() { + return manager; + } + + } + + private static class MutualNegotiateSchemeWithMockGssManager extends SPNegoScheme { + + final GSSManager manager = Mockito.mock(GSSManager.class); + final GSSName name = Mockito.mock(GSSName.class); + final GSSContext context = Mockito.mock(GSSContext.class); + + MutualNegotiateSchemeWithMockGssManager(final boolean established, final boolean mutual) throws Exception { + super(MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); + // Initial empty WWW-Authenticate response header + Mockito.when(context.initSecContext( + AdditionalMatchers.aryEq(NO_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + .thenReturn(GOOD_TOKEN_BYTES); + // Valid mutual token + Mockito.when(context.initSecContext( + AdditionalMatchers.aryEq(GOOD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + .thenReturn(NO_TOKEN_BYTES); + // Invalid mutual token + Mockito.when(context.initSecContext( + AdditionalMatchers.aryEq(BAD_MUTUAL_AUTH_TOKEN_BYTES), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) + .thenThrow(new GSSException(GSSException.DEFECTIVE_CREDENTIAL)); + // It's hard to mock state, so instead we specify the complete and mutualAuth states + // in the constructor + Mockito.when(context.isEstablished()).thenReturn(established); + Mockito.when(context.getMutualAuthState()).thenReturn(mutual); + Mockito.when(manager.createName( + ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(name); + Mockito.when(manager.createContext( + ArgumentMatchers.any(), ArgumentMatchers.any(), + ArgumentMatchers.any(), ArgumentMatchers.anyInt())) + .thenReturn(context); + } + + @Override + protected GSSManager getManager() { + return manager; + } + + } + + private static class UseJaasCredentials implements Credentials { + + @Override + public char[] getPassword() { + return null; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + } + + private static class TestAuthSchemeFactory implements AuthSchemeFactory { + + AuthScheme scheme; + + TestAuthSchemeFactory(final AuthScheme scheme) throws Exception { + this.scheme = scheme; + } + + @Override + public AuthScheme create(final HttpContext context) { + return scheme; + } + + } + + + /** + * Tests that the client will stop connecting to the server if + * the server still keep asking for a valid ticket. + */ + @Test + public void testDontTryToAuthenticateEndlessly() throws Exception { + configureServer(t -> { + t.register("*", new PleaseNegotiateService()); + }); + + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager()); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + t.setDefaultCredentialsProvider(jaasCredentialsProvider); + }); + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + client().execute(target, httpget, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode()); + return null; + }); + } + + /** + * Javadoc specifies that {@link GSSContext#initSecContext(byte[], int, int)} can return null + * if no token is generated. Client should be able to deal with this response. + */ + @Test + public void testNoTokenGeneratedError() throws Exception { + configureServer(t -> { + t.register("*", new PleaseNegotiateService()); + }); + + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager()); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + t.setDefaultCredentialsProvider(jaasCredentialsProvider); + }); + + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + client().execute(target, httpget, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode()); + return null; + }); + + } + + /** + * Test the success case for mutual auth + */ + @Test + public void testMutualSuccess() throws Exception { + configureServer(t -> { + t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + }); + final HttpHost target = startServer(); + + final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true); + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + t.setDefaultCredentialsProvider(jaasCredentialsProvider); + }); + + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + client().execute(target, httpget, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + return null; + }); + + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished(); + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState(); + } + + /** + * No mutual auth response token sent by server. + */ + @Test + public void testMutualFailureNoToken() throws Exception { + configureServer(t -> { + t.register("*", new SPNEGOMutualService(false, null)); + }); + + final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false); + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + }); + + final HttpClientContext context = new HttpClientContext(); + context.setCredentialsProvider(jaasCredentialsProvider); + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + try { + client().execute(target, httpget, context, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.fail(); + return null; + }); + Assertions.fail(); + } catch (final Exception e) { + Assertions.assertTrue(e instanceof ClientProtocolException); + Assertions.assertTrue(e.getCause() instanceof AuthenticationException); + } + + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished(); + Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState(); + } + + /** + * Server sends a "valid" token, but we mock the established status to false + */ + @Test + public void testMutualFailureEstablishedStatusFalse() throws Exception { + configureServer(t -> { + t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + }); + + final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false); + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + }); + + final HttpClientContext context = new HttpClientContext(); + context.setCredentialsProvider(jaasCredentialsProvider); + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + try { + client().execute(target, httpget, context, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.fail(); + return null; + }); + Assertions.fail(); + } catch (final Exception e) { + Assertions.assertTrue(e instanceof ClientProtocolException); + Assertions.assertTrue(e.getCause() instanceof AuthenticationException); + } + + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished(); + Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState(); + } + + /** + * Server sends a "valid" token, but we mock the mutual auth status to false + */ + @Test + public void testMutualFailureMutualStatusFalse() throws Exception { + configureServer(t -> { + t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + }); + + final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, false); + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + }); + + final HttpClientContext context = new HttpClientContext(); + context.setCredentialsProvider(jaasCredentialsProvider); + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + try { + client().execute(target, httpget, context, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.fail(); + return null; + }); + Assertions.fail(); + } catch (final Exception e) { + Assertions.assertTrue(e instanceof ClientProtocolException); + Assertions.assertTrue(e.getCause() instanceof AuthenticationException); + } + + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished(); + Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).getMutualAuthState(); + } + + /** + * Server sends a "bad" token, and GSS throws an exception. + */ + @Test + public void testMutualFailureBadToken() throws Exception { + configureServer(t -> { + t.register("*", new SPNEGOMutualService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + }); + + // We except that the initSecContent throws an exception, so the status is irrelevant + final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, true); + final AuthSchemeFactory nsf = new TestAuthSchemeFactory(mockAuthScheme); + final Registry authSchemeRegistry = RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, nsf) + .build(); + + configureClient(t -> { + t.setDefaultAuthSchemeRegistry(authSchemeRegistry); + }); + + final HttpClientContext context = new HttpClientContext(); + context.setCredentialsProvider(jaasCredentialsProvider); + + final HttpHost target = startServer(); + final String s = "/path"; + final HttpGet httpget = new HttpGet(s); + try { + client().execute(target, httpget, context, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.fail(); + return null; + }); + Assertions.fail(); + } catch (final Exception e) { + Assertions.assertTrue(e instanceof ClientProtocolException); + Assertions.assertTrue(e.getCause() instanceof AuthenticationException); + } + + Mockito.verify(mockAuthScheme.context, Mockito.never()).isEstablished(); + Mockito.verify(mockAuthScheme.context, Mockito.never()).getMutualAuthState(); + } +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java index 2aaf1fb66a..9fb5a5d435 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthExchange.java @@ -38,6 +38,9 @@ */ public class AuthExchange { + // This only tracks the server state. In particular, even if the state is SUCCESS, + // the authentication may still fail if the challenge sent with an authorized response cannot + // be validated locally for AuthSchemeV2 schemes. public enum State { UNCHALLENGED, CHALLENGED, HANDSHAKE, FAILURE, SUCCESS diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java index 22d884a97d..290df24e9d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme.java @@ -86,6 +86,10 @@ * containing the terminal authorization response, the scheme is considered unsuccessful * and in FAILED state. *

+ *

+ * This interface cannot corrently handle some authentication methods, like SPENGO. + * See {@link AuthSchemeV2} for a more capable interface. + *

* * @since 4.0 */ @@ -128,6 +132,9 @@ void processChallenge( * successfully or unsuccessfully), that is, all the required authorization * challenges have been processed in their entirety. * + * Note that due to some assumptions made about the control flow by the authentication code + * returning true will immediately cause the authentication process to fail. + * * @return {@code true} if the authentication process has been completed, * {@code false} otherwise. * diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java new file mode 100644 index 0000000000..be7d052cdc --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthSchemeV2.java @@ -0,0 +1,102 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.auth; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * This is an improved version of the {@link AuthScheme} interface, amended to be able to handle + * a conversation involving multiple challenge-response transactions and adding the ability to check + * the results of a final token sent together with the successful HTTP request as required by + * RFC 4559 and RFC 7546. + * + * @since 5.5 + */ +public interface AuthSchemeV2 extends AuthScheme { + + /** + * Processes the given auth challenge. Some authentication schemes may involve multiple + * challenge-response exchanges. Such schemes must be able to maintain internal state + * when dealing with sequential challenges. + * + * The {@link AuthScheme} interface implicitly assumes that that the token passed here is + * simply stored in this method, and the actual authentication takes place in + * {@link org.apache.hc.client5.http.auth.AuthScheme#generateAuthResponse(HttpHost, HttpRequest, HttpContext) generateAuthResponse } + * and/or {@link org.apache.hc.client5.http.auth.AuthScheme#isResponseReady(HttpHost, HttpRequest, HttpContext) generateAuthResponse }, + * as only those methods receive the HttpHost, and only those can throw an + * AuthenticationException. + * + * This new methods signature makes it possible to process the token and throw an + * AuthenticationException immediately even when no response is sent (i.e. processing the mutual + * authentication response) + * + * When {@link isChallengeExpected} returns true, but no challenge was sent, then this method must + * be called with a null {@link AuthChallenge} so that the Scheme can handle this situation. + * + * @param host HTTP host + * @param authChallenge the auth challenge or null if no challenge was received + * @param context HTTP context + * @param challenged true if the response was unauthorised (401/407) + * @throws AuthenticationException in case the authentication process is unsuccessful. + * @since 5.5 + */ + void processChallenge( + HttpHost host, + AuthChallenge authChallenge, + HttpContext context, + boolean challenged) throws AuthenticationException; + + /** + * The old processChallenge signature is unfit for use in AuthSchemeV2. + * If the old signature is sufficient for a scheme, then it should implement {@link AuthScheme} + * instead AuthSchemeV2. + */ + @Override + default void processChallenge( + AuthChallenge authChallenge, + HttpContext context) throws MalformedChallengeException { + throw new UnsupportedOperationException("on AuthSchemeV2 implementations only the four " + + "argument processChallenge method can be called"); + } + + /** + * Indicates that the even authorized (i.e. not 401 or 407) responses must be processed + * by this Scheme. + * + * The AuthScheme(V1) interface only processes unauthorised responses. + * This method indicates that non unauthorised responses are expected to contain challenges + * and must be processed by the Scheme. + * This is required to implement the SPENGO RFC and Kerberos mutual authentication. + * + * @return true if responses with non 401/407 response codes must be processed by the scheme. + * @since 5.5 + */ + boolean isChallengeExpected(); + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java index 508eeb9b0e..2793b8e1f7 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java @@ -35,11 +35,7 @@ * * @since 4.6 * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. - * */ -@Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) public class KerberosConfig implements Cloneable { @@ -53,25 +49,28 @@ public enum Option { public static final KerberosConfig DEFAULT = new Builder().build(); - private final Option stripPort; - private final Option useCanonicalHostname; - private final Option requestDelegCreds; + private final Option stripPort; //Effective default is ENABLE + private final Option useCanonicalHostname; //Effective default is ENABLE + private final Option requestDelegCreds; //Effective default is DISABLE + private final Option requestMutualAuth; //Effective default is DISABLE /** * Intended for CDI compatibility */ protected KerberosConfig() { - this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT); + this(Option.DEFAULT, Option.DEFAULT, Option.DEFAULT, Option.DEFAULT); } KerberosConfig( final Option stripPort, final Option useCanonicalHostname, - final Option requestDelegCreds) { + final Option requestDelegCreds, + final Option requestMutualAuth) { super(); this.stripPort = stripPort; this.useCanonicalHostname = useCanonicalHostname; this.requestDelegCreds = requestDelegCreds; + this.requestMutualAuth = requestMutualAuth; } public Option getStripPort() { @@ -86,6 +85,10 @@ public Option getRequestDelegCreds() { return requestDelegCreds; } + public Option getRequestMutualAuth() { + return requestMutualAuth; + } + @Override protected KerberosConfig clone() throws CloneNotSupportedException { return (KerberosConfig) super.clone(); @@ -98,6 +101,7 @@ public String toString() { builder.append("stripPort=").append(stripPort); builder.append(", useCanonicalHostname=").append(useCanonicalHostname); builder.append(", requestDelegCreds=").append(requestDelegCreds); + builder.append(", requestMutualAuth=").append(requestMutualAuth); builder.append("]"); return builder.toString(); } @@ -110,7 +114,9 @@ public static KerberosConfig.Builder copy(final KerberosConfig config) { return new Builder() .setStripPort(config.getStripPort()) .setUseCanonicalHostname(config.getUseCanonicalHostname()) - .setRequestDelegCreds(config.getRequestDelegCreds()); + .setRequestDelegCreds(config.getRequestDelegCreds()) + .setRequestMutualAuth(config.getRequestMutualAuth() + ); } public static class Builder { @@ -118,12 +124,14 @@ public static class Builder { private Option stripPort; private Option useCanonicalHostname; private Option requestDelegCreds; + private Option requestMutualAuth; Builder() { super(); this.stripPort = Option.DEFAULT; this.useCanonicalHostname = Option.DEFAULT; this.requestDelegCreds = Option.DEFAULT; + this.requestMutualAuth = Option.DEFAULT; } public Builder setStripPort(final Option stripPort) { @@ -151,11 +159,17 @@ public Builder setRequestDelegCreds(final Option requestDelegCreds) { return this; } + public Builder setRequestMutualAuth(final Option requestMutualAuth) { + this.requestMutualAuth = requestMutualAuth; + return this; + } + public KerberosConfig build() { return new KerberosConfig( stripPort, useCanonicalHostname, - requestDelegCreds); + requestDelegCreds, + requestMutualAuth); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java index 92bab8d4f3..e40963b2a8 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosCredentials.java @@ -37,14 +37,7 @@ * Kerberos specific {@link Credentials} representation based on {@link GSSCredential}. * * @since 4.4 - * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. - * - * @see UsernamePasswordCredentials - * @see BearerToken */ -@Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) public class KerberosCredentials implements Credentials, Serializable { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java index 1345282c0b..4ede224e2b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java @@ -66,20 +66,12 @@ private StandardAuthScheme() { /** * SPNEGO authentication scheme as defined in RFC 4559 and RFC 4178. - * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. */ - @Deprecated public static final String SPNEGO = "Negotiate"; /** * Kerberos authentication scheme as defined in RFC 4120. - * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. */ - @Deprecated public static final String KERBEROS = "Kerberos"; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java index 0440a1322f..2bd521e009 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java @@ -68,6 +68,8 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy { private static final List DEFAULT_SCHEME_PRIORITY = Collections.unmodifiableList(Arrays.asList( + StandardAuthScheme.SPNEGO, + StandardAuthScheme.KERBEROS, StandardAuthScheme.BEARER, StandardAuthScheme.DIGEST, StandardAuthScheme.BASIC)); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java index c78a7db29a..47e4d9ed0e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java @@ -43,7 +43,9 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; @@ -501,10 +503,11 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -514,7 +517,7 @@ private boolean needAuthentication( } } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java index 907b23e46b..579607a6d9 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java @@ -38,7 +38,9 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; import org.apache.hc.client5.http.impl.RequestSupport; @@ -305,11 +307,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -321,6 +324,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -330,7 +334,7 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { + if (targetAuthRequested || targetMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); @@ -340,7 +344,7 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java index aa3bb94114..5693e513de 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/GGSSchemeBase.java @@ -32,14 +32,14 @@ import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthChallenge; -import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeV2; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.InvalidCredentialsException; -import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.KerberosConfig; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.core5.http.HttpHost; @@ -60,44 +60,51 @@ * * @since 4.2 * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. */ -@Deprecated -public abstract class GGSSchemeBase implements AuthScheme { +// FIXME The class name looks like a Typo. Rename in 6.0 ? +public abstract class GGSSchemeBase implements AuthSchemeV2 { enum State { UNINITIATED, - CHALLENGE_RECEIVED, - TOKEN_GENERATED, + TOKEN_READY, + TOKEN_SENT, + SUCCEEDED, FAILED, } private static final Logger LOG = LoggerFactory.getLogger(GGSSchemeBase.class); private static final String NO_TOKEN = ""; private static final String KERBEROS_SCHEME = "HTTP"; - private final org.apache.hc.client5.http.auth.KerberosConfig config; + + // The GSS spec does not specify how long the conversation can be. This should be plenty. + // Realistically, we get one initial token, then one maybe one more for mutual authentication. + private static final int MAX_GSS_CHALLENGES = 3; + private final KerberosConfig config; private final DnsResolver dnsResolver; + private final boolean mutualAuth; + private int challengesLeft = MAX_GSS_CHALLENGES; /** Authentication process state */ private State state; private GSSCredential gssCredential; + private GSSContext gssContext; private String challenge; - private byte[] token; + private byte[] queuedToken = new byte[0]; - GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config, final DnsResolver dnsResolver) { + GGSSchemeBase(final KerberosConfig config, final DnsResolver dnsResolver) { super(); - this.config = config != null ? config : org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT; + this.config = config != null ? config : KerberosConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; + this.mutualAuth = config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE; this.state = State.UNINITIATED; } - GGSSchemeBase(final org.apache.hc.client5.http.auth.KerberosConfig config) { + GGSSchemeBase(final KerberosConfig config) { this(config, SystemDefaultDnsResolver.INSTANCE); } GGSSchemeBase() { - this(org.apache.hc.client5.http.auth.KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + this(KerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); } @Override @@ -105,24 +112,115 @@ public String getRealm() { return null; } + // The AuthScheme API maps awkwardly to GSSAPI, where proccessChallange and generateAuthResponse + // map to the same single method call. Hence the generated token is only stored in this method. @Override public void processChallenge( + final HttpHost host, final AuthChallenge authChallenge, - final HttpContext context) throws MalformedChallengeException { - Args.notNull(authChallenge, "AuthChallenge"); - - this.challenge = authChallenge.getValue() != null ? authChallenge.getValue() : NO_TOKEN; + final HttpContext context, + final boolean challenged) throws AuthenticationException { - if (state == State.UNINITIATED) { - token = Base64.decodeBase64(challenge.getBytes()); - state = State.CHALLENGE_RECEIVED; - } else { + if (challengesLeft-- <= 0 ) { if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Authentication already attempted", exchangeId); + LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId); } + // TODO: Should we throw an exception ? There is a test for this behaviour. state = State.FAILED; + return; + } + + final byte[] challengeToken = Base64.decodeBase64(authChallenge== null ? null : authChallenge.getValue()); + + final String authServer; + String hostname = host.getHostName(); + if (config.getUseCanonicalHostname() != KerberosConfig.Option.DISABLE){ + try { + hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); + } catch (final UnknownHostException ignore){ + } + } + if (config.getStripPort() != KerberosConfig.Option.DISABLE) { + authServer = hostname; + } else { + authServer = hostname + ":" + host.getPort(); + } + + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.adapt(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSS init {}", exchangeId, authServer); + } + try { + queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, authServer); + switch (state) { + case UNINITIATED: + if (challenge != NO_TOKEN) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.adapt(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken); + } + // TODO Should we fail ? That would break existing tests that send a token + // in the first response, which is against the RFC. + } + state = State.TOKEN_READY; + break; + case TOKEN_SENT: + if (challenged) { + state = State.TOKEN_READY; + } else if (mutualAuth){ + // We should have received a valid mutualAuth token + if (!gssContext.isEstablished()) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = + HttpClientContext.adapt(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSSContext is not established ", exchangeId); + } + state = State.FAILED; + // TODO should we have specific exception(s) for these ? + throw new AuthenticationException( + "requireMutualAuth is set but GSSContext is not established"); + } else if (!gssContext.getMutualAuthState()) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = + HttpClientContext.adapt(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have" + + " mutualAuthState set", exchangeId); + } + state = State.FAILED; + throw new AuthenticationException( + "requireMutualAuth is set but GSSContext mutualAuthState is not set"); + } else { + state = State.SUCCEEDED; + } + } + break; + default: + state = State.FAILED; + throw new IllegalStateException("Illegal state: " + state); + + } + } catch (final GSSException gsse) { + state = State.FAILED; + if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL + || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.NO_CRED) { + throw new InvalidCredentialsException(gsse.getMessage(), gsse); + } + if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN + || gsse.getMajor() == GSSException.DUPLICATE_TOKEN + || gsse.getMajor() == GSSException.OLD_TOKEN) { + throw new AuthenticationException(gsse.getMessage(), gsse); + } + // other error + throw new AuthenticationException(gsse.getMessage(), gsse); } } @@ -138,7 +236,9 @@ protected byte[] generateGSSToken( final GSSManager manager = getManager(); final GSSName serverName = manager.createName(serviceName + "@" + authServer, GSSName.NT_HOSTBASED_SERVICE); - final GSSContext gssContext = createGSSContext(manager, oid, serverName, gssCredential); + if (gssContext == null) { + gssContext = createGSSContext(manager, oid, serverName, gssCredential); + } if (input != null) { return gssContext.initSecContext(input, 0, input.length); } @@ -156,8 +256,11 @@ protected GSSContext createGSSContext( final GSSContext gssContext = manager.createContext(serverName.canonicalize(oid), oid, gssCredential, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); - if (config.getRequestDelegCreds() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DEFAULT) { - gssContext.requestCredDeleg(config.getRequestDelegCreds() == org.apache.hc.client5.http.auth.KerberosConfig.Option.ENABLE); + if (config.getRequestDelegCreds() != KerberosConfig.Option.DEFAULT) { + gssContext.requestCredDeleg(config.getRequestDelegCreds() == KerberosConfig.Option.ENABLE); + } + if (config.getRequestMutualAuth() != KerberosConfig.Option.DEFAULT) { + gssContext.requestMutualAuth(config.getRequestMutualAuth() == KerberosConfig.Option.ENABLE); } return gssContext; } @@ -168,7 +271,15 @@ protected GSSContext createGSSContext( @Override public boolean isChallengeComplete() { - return this.state == State.TOKEN_GENERATED || this.state == State.FAILED; + // For the mutual authentication response, this is should technically return true. + // However, the HttpAuthenticator immediately fails the authentication + // process if we return true, so we only return true here if the authentication has failed. + return this.state == State.FAILED; + } + + @Override + public boolean isChallengeExpected() { + return state == State.TOKEN_SENT && mutualAuth; } @Override @@ -195,6 +306,8 @@ public Principal getPrincipal() { return null; } + // Format the queued token and update the state. + // All token processing is done in processChallenge() @Override public String generateAuthResponse( final HttpHost host, @@ -207,53 +320,16 @@ public String generateAuthResponse( throw new AuthenticationException(getName() + " authentication has not been initiated"); case FAILED: throw new AuthenticationException(getName() + " authentication has failed"); - case CHALLENGE_RECEIVED: - try { - final String authServer; - String hostname = host.getHostName(); - if (config.getUseCanonicalHostname() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE){ - try { - hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); - } catch (final UnknownHostException ignore){ - } - } - if (config.getStripPort() != org.apache.hc.client5.http.auth.KerberosConfig.Option.DISABLE) { - authServer = hostname; - } else { - authServer = hostname + ":" + host.getPort(); - } - - if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} init {}", exchangeId, authServer); - } - token = generateToken(token, KERBEROS_SCHEME, authServer); - state = State.TOKEN_GENERATED; - } catch (final GSSException gsse) { - state = State.FAILED; - if (gsse.getMajor() == GSSException.DEFECTIVE_CREDENTIAL - || gsse.getMajor() == GSSException.CREDENTIALS_EXPIRED) { - throw new InvalidCredentialsException(gsse.getMessage(), gsse); - } - if (gsse.getMajor() == GSSException.NO_CRED ) { - throw new InvalidCredentialsException(gsse.getMessage(), gsse); - } - if (gsse.getMajor() == GSSException.DEFECTIVE_TOKEN - || gsse.getMajor() == GSSException.DUPLICATE_TOKEN - || gsse.getMajor() == GSSException.OLD_TOKEN) { - throw new AuthenticationException(gsse.getMessage(), gsse); - } - // other error - throw new AuthenticationException(gsse.getMessage()); - } - case TOKEN_GENERATED: + case SUCCEEDED: + return null; + case TOKEN_READY: + state = State.TOKEN_SENT; final Base64 codec = new Base64(0); - final String tokenstr = new String(codec.encode(token)); + final String tokenstr = new String(codec.encode(queuedToken)); if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Sending response '{}' back to the auth server", exchangeId, tokenstr); + LOG.debug("{} Sending GSS response '{}' back to the auth server", exchangeId, tokenstr); } return StandardAuthScheme.SPNEGO + " " + tokenstr; default: diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java index cd9f7ce723..d373a69914 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java @@ -38,6 +38,7 @@ import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthSchemeV2; import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.CredentialsProvider; @@ -81,12 +82,13 @@ public HttpAuthenticator() { } /** - * Determines whether the given response represents an authentication challenge. + * Determines whether the given response represents an authentication challenge, and updates + * the autheExchange status. * * @param host the hostname of the opposite endpoint. * @param challengeType the challenge type (target or proxy). * @param response the response message head. - * @param authExchange the current authentication exchange state. + * @param authExchange the current authentication exchange state. Gets updated. * @param context the current execution context. * @return {@code true} if the response message represents an authentication challenge, * {@code false} otherwise. @@ -97,32 +99,17 @@ public boolean isChallenged( final HttpResponse response, final AuthExchange authExchange, final HttpContext context) { - final int challengeCode; - switch (challengeType) { - case TARGET: - challengeCode = HttpStatus.SC_UNAUTHORIZED; - break; - case PROXY: - challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; - break; - default: - throw new IllegalStateException("Unexpected challenge type: " + challengeType); - } - - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - - if (response.getCode() == challengeCode) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication required", exchangeId); - } + if (checkChallenged(challengeType, response, context)) { return true; } switch (authExchange.getState()) { case CHALLENGED: case HANDSHAKE: if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication succeeded", exchangeId); + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + // The mutual auth may still fail + LOG.debug("{} Server has accepted authorization", exchangeId); } authExchange.setState(AuthExchange.State.SUCCESS); break; @@ -135,37 +122,64 @@ public boolean isChallenged( } /** - * Updates the {@link AuthExchange} state based on the challenge presented in the response message - * using the given {@link AuthenticationStrategy}. + * Determines whether the given response represents an authentication challenge, without + * changing the AuthExchange state. * - * @param host the hostname of the opposite endpoint. * @param challengeType the challenge type (target or proxy). * @param response the response message head. - * @param authStrategy the authentication strategy. - * @param authExchange the current authentication exchange state. * @param context the current execution context. - * @return {@code true} if the authentication state has been updated, - * {@code false} if unchanged. + * @return {@code true} if the response message represents an authentication challenge, + * {@code false} otherwise. */ - public boolean updateAuthState( - final HttpHost host, - final ChallengeType challengeType, - final HttpResponse response, - final AuthenticationStrategy authStrategy, - final AuthExchange authExchange, - final HttpContext context) { + private boolean checkChallenged(final ChallengeType challengeType, final HttpResponse response, final HttpContext context) { + final int challengeCode; + switch (challengeType) { + case TARGET: + challengeCode = HttpStatus.SC_UNAUTHORIZED; + break; + case PROXY: + challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; + break; + default: + throw new IllegalStateException("Unexpected challenge type: " + challengeType); + } - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); + if (response.getCode() == challengeCode) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Authentication required", exchangeId); + } + return true; + } + return false; + } - if (LOG.isDebugEnabled()) { - LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); + /** + * Determines if the scheme requires an auth challenge for responses that do not + * have challenge HTTP code. (i.e whether it needs a mutual authentication token) + * + * @param authExchange + * @return true is authExchange's scheme is AuthExchangeV2, which currently expects + * a WWW-Authenticate header even for authorized HTTP responses + */ + public boolean isChallengeExpected(final AuthExchange authExchange) { + final AuthScheme authScheme = authExchange.getAuthScheme(); + if (authScheme != null && authScheme instanceof AuthSchemeV2) { + return ((AuthSchemeV2)authScheme).isChallengeExpected(); + } else { + return false; } + } - final Header[] headers = response.getHeaders( - challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE); + public Map extractChallengeMap(final ChallengeType challengeType, + final HttpResponse response, final HttpClientContext context) { + final Header[] headers = + response.getHeaders( + challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE + : HttpHeaders.WWW_AUTHENTICATE); final Map challengeMap = new HashMap<>(); - for (final Header header: headers) { + for (final Header header : headers) { final CharArrayBuffer buffer; final int pos; if (header instanceof FormattedHeader) { @@ -186,52 +200,109 @@ public boolean updateAuthState( authChallenges = parser.parse(challengeType, buffer, cursor); } catch (final ParseException ex) { if (LOG.isWarnEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue()); } continue; } - for (final AuthChallenge authChallenge: authChallenges) { + for (final AuthChallenge authChallenge : authChallenges) { final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT); if (!challengeMap.containsKey(schemeName)) { challengeMap.put(schemeName, authChallenge); } } } + return challengeMap; + } + + /** + * Updates the {@link AuthExchange} state based on the challenge presented in the response message + * using the given {@link AuthenticationStrategy}. + * + * @param host the hostname of the opposite endpoint. + * @param challengeType the challenge type (target or proxy). + * @param response the response message head. + * @param authStrategy the authentication strategy. + * @param authExchange the current authentication exchange state. + * @param context the current execution context. + * @return {@code true} if the request needs-to be re-sent , + * {@code false} if the authentication is complete (successful or not). + * + * @throws AuthenticationException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + * @throws MalformedChallengeException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + */ + public boolean updateAuthState( + final HttpHost host, + final ChallengeType challengeType, + final HttpResponse response, + final AuthenticationStrategy authStrategy, + final AuthExchange authExchange, + final HttpContext context) throws AuthenticationException, MalformedChallengeException { + + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + final boolean challenged = checkChallenged(challengeType, response, context); + final boolean isChallengeExpected = isChallengeExpected(authExchange); + + if (LOG.isDebugEnabled()) { + LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); + } + + final Map challengeMap = extractChallengeMap(challengeType, response, clientContext); + if (challengeMap.isEmpty()) { if (LOG.isDebugEnabled()) { LOG.debug("{} Response contains no valid authentication challenges", exchangeId); } - authExchange.reset(); - return false; + if (!isChallengeExpected) { + authExchange.reset(); + return false; + } } switch (authExchange.getState()) { case FAILURE: return false; case SUCCESS: - authExchange.reset(); - break; + if (!isChallengeExpected) { + authExchange.reset(); + break; + } + // otherwise fall through case CHALLENGED: + // fall through case HANDSHAKE: Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme"); + // fall through case UNCHALLENGED: final AuthScheme authScheme = authExchange.getAuthScheme(); + // AuthScheme is only set if we have already sent an auth response, either + // because we have received a challenge for it, or preemptively. if (authScheme != null) { final String schemeName = authScheme.getName(); final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - if (challenge != null) { + if (challenge != null || isChallengeExpected) { if (LOG.isDebugEnabled()) { - LOG.debug("{} Authorization challenge processed", exchangeId); + LOG.debug("{} Processing authorization challenge {}", exchangeId, challenge); } try { - authScheme.processChallenge(challenge, context); - } catch (final MalformedChallengeException ex) { + if (authScheme instanceof AuthSchemeV2) { + ((AuthSchemeV2)authScheme).processChallenge(host, challenge, context, challenged); + } else { + authScheme.processChallenge(challenge, context); + } + } catch (final AuthenticationException | MalformedChallengeException ex) { if (LOG.isWarnEnabled()) { - LOG.warn("{} {}", exchangeId, ex.getMessage()); + LOG.warn("Exception processing Challange {}", exchangeId, ex); } authExchange.reset(); authExchange.setState(AuthExchange.State.FAILURE); - return false; + if (!challenged) { + throw ex; + } } if (authScheme.isChallengeComplete()) { if (LOG.isDebugEnabled()) { @@ -241,7 +312,14 @@ public boolean updateAuthState( authExchange.setState(AuthExchange.State.FAILURE); return false; } - authExchange.setState(AuthExchange.State.HANDSHAKE); + if (!challenged) { + // There are no more challanges sent after the 200 message, + // and if we get here, then the mutual auth phase has succeeded. + authExchange.setState(AuthExchange.State.SUCCESS); + return false; + } else { + authExchange.setState(AuthExchange.State.HANDSHAKE); + } return true; } authExchange.reset(); @@ -249,6 +327,9 @@ public boolean updateAuthState( } } + // We reach this if we fell through above because the authScheme has not yet been set, or if + // we receive a 401/407 response for an unexpected scheme. Normally this processes the first + // 401/407 response final List preferredSchemes = authStrategy.select(challengeType, challengeMap, context); final CredentialsProvider credsProvider = clientContext.getCredentialsProvider(); if (credsProvider == null) { @@ -263,16 +344,23 @@ public boolean updateAuthState( LOG.debug("{} Selecting authentication options", exchangeId); } for (final AuthScheme authScheme: preferredSchemes) { + // We only respond to the the first successfully processed challenge. However, the + // AuthScheme(V1) API does not really process the challenge at this point, so we need + // to process/store each challenge here anyway. try { final String schemeName = authScheme.getName(); final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - authScheme.processChallenge(challenge, context); + if (authScheme instanceof AuthSchemeV2) { + ((AuthSchemeV2)authScheme).processChallenge(host, challenge, context, challenged); + } else { + authScheme.processChallenge(challenge, context); + } if (authScheme.isResponseReady(host, credsProvider, context)) { authOptions.add(authScheme); } } catch (final AuthenticationException | MalformedChallengeException ex) { if (LOG.isWarnEnabled()) { - LOG.warn(ex.getMessage()); + LOG.warn("Exception while processing Challange", ex); } } } @@ -331,12 +419,14 @@ public void addAuthResponse( } try { final String authResponse = authScheme.generateAuthResponse(host, request, context); - final Header header = new BasicHeader( - challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, - authResponse); - request.addHeader(header); + if (authResponse != null) { + final Header header = new BasicHeader( + challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, + authResponse); + request.addHeader(header); + } break; - } catch (final AuthenticationException ex) { + } catch (final AuthenticationException ex ) { if (LOG.isWarnEnabled()) { LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage()); } @@ -347,6 +437,9 @@ public void addAuthResponse( Asserts.notNull(authScheme, "AuthScheme"); default: } + // This is the SUCCESS and HANDSHAKE states, same as the initial response. + // This only happens if the NEGOTIATE handshake requires multiple requests, which is + // defined in the RFC, but unlikely in practice. if (authScheme != null) { try { final String authResponse = authScheme.generateAuthResponse(host, request, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java index 656f29633a..2679b07ece 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java @@ -40,14 +40,7 @@ *

* * @since 4.2 - * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. - * - * @see BasicScheme - * @see BearerScheme */ -@Deprecated @Experimental public class KerberosScheme extends GGSSchemeBase { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java index 7971ff935d..dfa37a8ad6 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java @@ -41,14 +41,7 @@ *

* * @since 4.2 - * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. - * - * @see BasicScheme - * @see BearerScheme */ -@Deprecated @Experimental public class SPNegoScheme extends GGSSchemeBase { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java index 1c5cbab752..93a85e194c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java @@ -251,6 +251,7 @@ private ClassicHttpResponse createTunnelToTarget( if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -260,7 +261,7 @@ private ClassicHttpResponse createTunnelToTarget( } } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java index bfebce0eaf..7df0259da9 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java @@ -34,7 +34,9 @@ import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; @@ -189,6 +191,7 @@ public ClassicHttpResponse execute( authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context); } + //The is where the actual network communications happens (eventually) final ClassicHttpResponse response = chain.proceed(request, scope); if (Method.TRACE.isSame(request.getMethod())) { @@ -218,6 +221,8 @@ public ClassicHttpResponse execute( EntityUtils.consume(responseEntity); } else { execRuntime.disconnectEndpoint(); + // We don't have any connection based AuthSchemeV2 implementations. + // If one existed, we'd have think about how to handle it if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS && proxyAuthExchange.isConnectionBased()) { if (LOG.isDebugEnabled()) { @@ -265,11 +270,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { - final RequestConfig config = context.getRequestConfigOrDefault(); + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { + final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -281,6 +287,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -290,7 +297,7 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { + if (targetAuthRequested || targetMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); @@ -300,7 +307,7 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java index a4657a26ab..0f5347495a 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java @@ -175,7 +175,8 @@ public Socket tunnel( if (status < 200) { throw new HttpException("Unexpected response to CONNECT request: " + response); } - if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) { + if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context) + || authenticator.isChallengeExpected(proxyAuthExchange)) { if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, this.proxyAuthStrategy, this.proxyAuthExchange, context)) { // Retry request diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java index 9107e88016..61df88722c 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java @@ -38,6 +38,7 @@ import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -150,7 +151,7 @@ void testAuthenticationNotRequestedSuccess2() { } @Test - void testAuthentication() { + void testAuthentication() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -179,7 +180,7 @@ void testAuthentication() { } @Test - void testAuthenticationCredentialsForBasic() { + void testAuthenticationCredentialsForBasic() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); @@ -205,7 +206,7 @@ void testAuthenticationCredentialsForBasic() { } @Test - void testAuthenticationNoChallenges() { + void testAuthenticationNoChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); @@ -216,7 +217,7 @@ void testAuthenticationNoChallenges() { } @Test - void testAuthenticationNoSupportedChallenges() { + void testAuthenticationNoSupportedChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "This realm=\"test\"")); @@ -229,7 +230,7 @@ void testAuthenticationNoSupportedChallenges() { } @Test - void testAuthenticationNoCredentials() { + void testAuthenticationNoCredentials() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -242,7 +243,7 @@ void testAuthenticationNoCredentials() { } @Test - void testAuthenticationFailed() { + void testAuthenticationFailed() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -260,7 +261,7 @@ void testAuthenticationFailed() { } @Test - void testAuthenticationFailedPreviously() { + void testAuthenticationFailedPreviously() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -277,7 +278,7 @@ void testAuthenticationFailedPreviously() { } @Test - void testAuthenticationFailure() { + void testAuthenticationFailure() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -295,7 +296,7 @@ void testAuthenticationFailure() { } @Test - void testAuthenticationHandshaking() { + void testAuthenticationHandshaking() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -314,7 +315,7 @@ void testAuthenticationHandshaking() { } @Test - void testAuthenticationNoMatchingChallenge() { + void testAuthenticationNoMatchingChallenge() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"1234\"")); @@ -342,7 +343,7 @@ void testAuthenticationNoMatchingChallenge() { } @Test - void testAuthenticationException() { + void testAuthenticationException() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "blah blah blah"));