From 51de8556134b0cebee6500dd09c4606b87139969 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Thu, 23 Jan 2025 10:58:04 +0100 Subject: [PATCH 01/10] HTTPCLIENT-2358 Implement a mutual authentication capable SPNEGO scheme --- .../testing/sync/TestMutualSpnegoScheme.java | 551 ++++++++++++++++++ .../hc/client5/http/auth/KerberosConfig.java | 8 +- .../http/auth/KerberosCredentials.java | 9 +- .../http/auth/MutualKerberosConfig.java | 159 +++++ .../client5/http/auth/StandardAuthScheme.java | 7 +- .../client5/http/impl/auth/GGSSchemeBase.java | 6 +- .../http/impl/auth/KerberosScheme.java | 7 +- .../http/impl/auth/KerberosSchemeFactory.java | 7 +- .../http/impl/auth/MutualGssSchemeBase.java | 352 +++++++++++ .../http/impl/auth/MutualSpnegoScheme.java | 112 ++++ .../impl/auth/MutualSpnegoSchemeFactory.java | 76 +++ .../client5/http/impl/auth/SPNegoScheme.java | 7 +- .../http/impl/auth/SPNegoSchemeFactory.java | 7 +- 13 files changed, 1283 insertions(+), 25 deletions(-) create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java 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/TestMutualSpnegoScheme.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java new file mode 100644 index 0000000000..c32b66a4e9 --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java @@ -0,0 +1,551 @@ +/* + * ==================================================================== + * 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.MutualKerberosConfig; +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 MutualKerberosConfig MUTUAL_KERBEROS_CONFIG = MutualKerberosConfig.DEFAULT; + + 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(MutualKerberosConfig.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/KerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java index 508eeb9b0e..ca94094e3b 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,9 +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 Do not use. The old GGS based experimental authentication schemes are no longer + * supported. + * Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme + * @see MutualKerberosConfig */ @Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) 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..c30384423b 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 @@ -38,13 +38,12 @@ * * @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 + * Optionally used both by {@link org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme} + * and the old deprecated GGS based experimental authentication schemes. + * + * @see org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme */ -@Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) public class KerberosCredentials implements Credentials, Serializable { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java new file mode 100644 index 0000000000..0fbb6f7ade --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java @@ -0,0 +1,159 @@ +/* + * ==================================================================== + * 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.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; + +/** + * Immutable class encapsulating Kerberos configuration options for MutualSpnegoScheme. + * + * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and + * primitive booleans. + * + * @since 5.5 + * + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class MutualKerberosConfig implements Cloneable { + + + public static final MutualKerberosConfig DEFAULT = new Builder().build(); + + private final boolean stripPort; + private final boolean useCanonicalHostname; + private final boolean requestMutualAuth; + private final boolean requestDelegCreds; + + /** + * Intended for CDI compatibility + */ + protected MutualKerberosConfig() { + this(true, true, true, false); + } + + MutualKerberosConfig( + final boolean stripPort, + final boolean useCanonicalHostname, + final boolean requestMutualAuth, + final boolean requestDelegCreds) { + super(); + this.stripPort = stripPort; + this.useCanonicalHostname = useCanonicalHostname; + this.requestMutualAuth = requestMutualAuth; + this.requestDelegCreds = requestDelegCreds; + } + + public boolean isStripPort() { + return stripPort; + } + + public boolean isUseCanonicalHostname() { + return useCanonicalHostname; + } + + public boolean isRequestDelegCreds() { + return requestDelegCreds; + } + + public boolean isRequestMutualAuth() { + return requestMutualAuth; + } + + @Override + protected MutualKerberosConfig clone() throws CloneNotSupportedException { + return (MutualKerberosConfig) super.clone(); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("["); + 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(); + } + + public static MutualKerberosConfig.Builder custom() { + return new Builder(); + } + + public static MutualKerberosConfig.Builder copy(final MutualKerberosConfig config) { + return new Builder() + .setStripPort(config.isStripPort()) + .setUseCanonicalHostname(config.isUseCanonicalHostname()) + .setRequestDelegCreds(config.isRequestDelegCreds()) + .setRequestMutualAuth(config.isRequestMutualAuth()); + } + + public static class Builder { + + private boolean stripPort = true; + private boolean useCanonicalHostname = true ; + private boolean requestMutualAuth = true; + private boolean requestDelegCreds = false; + + Builder() { + super(); + } + + public Builder setStripPort(final boolean stripPort) { + this.stripPort = stripPort; + return this; + } + + public Builder setUseCanonicalHostname(final boolean useCanonicalHostname) { + this.useCanonicalHostname = useCanonicalHostname; + return this; + } + + public Builder setRequestMutualAuth(final boolean requestMutualAuth) { + this.requestMutualAuth = requestMutualAuth; + return this; + } + + public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { + this.requestDelegCreds = requuestDelegCreds; + return this; + } + + public MutualKerberosConfig build() { + return new MutualKerberosConfig( + stripPort, + useCanonicalHostname, + requestMutualAuth, + requestDelegCreds + ); + } + + } + +} 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..a17e839803 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 @@ -67,16 +67,15 @@ 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. + * Use {@link org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme} instead of the old + * deprecated {@link org.apache.hc.client5.http.impl.auth.SPNegoScheme} */ - @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 + * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer * supported. Consider using Basic or Bearer authentication with TLS instead. */ @Deprecated 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..f5859c9c3d 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 @@ -61,7 +61,11 @@ * @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. + * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS + * instead. + * @see MutualSpnegoScheme + * @see BasicScheme + * @see BearerScheme */ @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 656f29633a..b32eb9176f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosScheme.java @@ -41,9 +41,10 @@ * * @since 4.2 * - * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Consider using Basic or Bearer authentication with TLS instead. - * + * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer + * supported. Use MutualSpnegoScheme, or 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/KerberosSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/KerberosSchemeFactory.java index 25930f0997..74efdccf64 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 old GGS based experimental authentication schemes are no longer + * supported. Use MutualSpnegoSchemeFactory, or 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 new file mode 100644 index 0000000000..f456286dbf --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java @@ -0,0 +1,352 @@ +/* + * ==================================================================== + * 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.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.StandardAuthScheme; +import org.apache.hc.client5.http.auth.MutualKerberosConfig; +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 GGSSchemeBase} class, which was deprecated in 5.3. + * + * @since 5.5 + * + * @see GGSSchemeBase + */ +public abstract class MutualGssSchemeBase implements AuthScheme { + + 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 MutualKerberosConfig 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 MutualKerberosConfig config, final DnsResolver dnsResolver) { + super(); + this.config = config != null ? config : MutualKerberosConfig.DEFAULT; + this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; + this.mutualAuth = config.isRequestMutualAuth(); + this.state = State.UNINITIATED; + } + + MutualGssSchemeBase(final MutualKerberosConfig config) { + this(config, SystemDefaultDnsResolver.INSTANCE); + } + + MutualGssSchemeBase() { + this(MutualKerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + } + + @Override + public String getRealm() { + return null; + } + + // Required by AuthScheme for backwards compatibility + @Override + public void processChallenge(final AuthChallenge authChallenge, + final HttpContext context ) { + // If this gets called, then AuthScheme was changed in an incompatible way + throw new UnsupportedOperationException(); + } + + // 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.isUseCanonicalHostname()) { + try { + hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); + } catch (final UnknownHostException ignore) { + } + } + if (config.isStripPort()) { + 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(mutualAuth); + gssContext.requestCredDeleg(config.isRequestDelegCreds()); + 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..4702714c8f --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java @@ -0,0 +1,112 @@ +/* + * ==================================================================== + * 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.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 org.apache.hc.client5.http.auth.AuthSchemeFactory} + * {@link org.apache.hc.core5.http.config.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
+ *           );
+ *
+ *   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 + */ +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.MutualKerberosConfig 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..3aac15d040 --- /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.MutualKerberosConfig.DEFAULT, + SystemDefaultDnsResolver.INSTANCE); + + private final org.apache.hc.client5.http.auth.MutualKerberosConfig config; + private final DnsResolver dnsResolver; + + /** + * @since 5.0 + */ + public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.MutualKerberosConfig config, final DnsResolver dnsResolver) { + super(); + this.config = config; + this.dnsResolver = dnsResolver; + } + + @Override + public AuthScheme create(final HttpContext context) { + return new MutualSpnegoScheme(this.config, this.dnsResolver); + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/SPNegoScheme.java index 7971ff935d..69cf0c86d4 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 @@ -42,9 +42,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 old GGS based experimental authentication schemes are no longer + * supported. Use MutualSpnegoScheme, or 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 14d8528c5e..7050800e17 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 @@ -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 old GGS based experimental authentication schemes are no longer + * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS + * instead. + * @see MutualSpnegoSchemeFactory * @see BasicSchemeFactory * @see BearerSchemeFactory */ From 1b28a5aff1f29858854f4ee002a61971e93b8df9 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Tue, 28 Jan 2025 16:01:25 +0100 Subject: [PATCH 02/10] MutualKerberosConfig changes --- .../http/auth/MutualKerberosConfig.java | 27 ++++++++++--------- .../http/impl/auth/MutualGssSchemeBase.java | 6 ++--- .../impl/auth/MutualSpnegoSchemeFactory.java | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java index 0fbb6f7ade..460a5ff3ad 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java @@ -36,6 +36,9 @@ * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and * primitive booleans. * + * Compared to {@link KerberosConfig} stripPort has been changed toAddPort, and the default is now + * false (same effect). The default for useCanonicalHostname has been changed to false from true. + * * @since 5.5 * */ @@ -45,7 +48,7 @@ public class MutualKerberosConfig implements Cloneable { public static final MutualKerberosConfig DEFAULT = new Builder().build(); - private final boolean stripPort; + private final boolean addPort; private final boolean useCanonicalHostname; private final boolean requestMutualAuth; private final boolean requestDelegCreds; @@ -54,7 +57,7 @@ public class MutualKerberosConfig implements Cloneable { * Intended for CDI compatibility */ protected MutualKerberosConfig() { - this(true, true, true, false); + this(false, false, true, false); } MutualKerberosConfig( @@ -63,14 +66,14 @@ protected MutualKerberosConfig() { final boolean requestMutualAuth, final boolean requestDelegCreds) { super(); - this.stripPort = stripPort; + this.addPort = stripPort; this.useCanonicalHostname = useCanonicalHostname; this.requestMutualAuth = requestMutualAuth; this.requestDelegCreds = requestDelegCreds; } - public boolean isStripPort() { - return stripPort; + public boolean isAddPort() { + return addPort; } public boolean isUseCanonicalHostname() { @@ -94,7 +97,7 @@ protected MutualKerberosConfig clone() throws CloneNotSupportedException { public String toString() { final StringBuilder builder = new StringBuilder(); builder.append("["); - builder.append("stripPort=").append(stripPort); + builder.append("addPort=").append(addPort); builder.append(", useCanonicalHostname=").append(useCanonicalHostname); builder.append(", requestDelegCreds=").append(requestDelegCreds); builder.append(", requestMutualAuth=").append(requestMutualAuth); @@ -108,7 +111,7 @@ public static MutualKerberosConfig.Builder custom() { public static MutualKerberosConfig.Builder copy(final MutualKerberosConfig config) { return new Builder() - .setStripPort(config.isStripPort()) + .setAddPort(config.isAddPort()) .setUseCanonicalHostname(config.isUseCanonicalHostname()) .setRequestDelegCreds(config.isRequestDelegCreds()) .setRequestMutualAuth(config.isRequestMutualAuth()); @@ -116,8 +119,8 @@ public static MutualKerberosConfig.Builder copy(final MutualKerberosConfig confi public static class Builder { - private boolean stripPort = true; - private boolean useCanonicalHostname = true ; + private boolean addPort = false; + private boolean useCanonicalHostname = false; private boolean requestMutualAuth = true; private boolean requestDelegCreds = false; @@ -125,8 +128,8 @@ public static class Builder { super(); } - public Builder setStripPort(final boolean stripPort) { - this.stripPort = stripPort; + public Builder setAddPort(final boolean addPort) { + this.addPort = addPort; return this; } @@ -147,7 +150,7 @@ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { public MutualKerberosConfig build() { return new MutualKerberosConfig( - stripPort, + addPort, useCanonicalHostname, requestMutualAuth, requestDelegCreds 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 index f456286dbf..33021f5197 100644 --- 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 @@ -153,10 +153,10 @@ public void processChallenge( } catch (final UnknownHostException ignore) { } } - if (config.isStripPort()) { - gssHostname = hostname; - } else { + if (config.isAddPort()) { gssHostname = hostname + ":" + host.getPort(); + } else { + gssHostname = hostname; } if (LOG.isDebugEnabled()) { 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 index 3aac15d040..4711ffee49 100644 --- 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 @@ -60,7 +60,7 @@ public class MutualSpnegoSchemeFactory implements AuthSchemeFactory { private final DnsResolver dnsResolver; /** - * @since 5.0 + * @since 5.5 */ public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.MutualKerberosConfig config, final DnsResolver dnsResolver) { super(); From 5a11bce39854b68ccc4d98538df679543e7a34f0 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Thu, 13 Feb 2025 08:45:00 +0100 Subject: [PATCH 03/10] Add containerized apache httpd + mod_auth_gssapi tests Fix GSSCredential handling --- .../client5/testing/util/SecurityUtils.java | 221 ++++++++++++++++++ .../ApacheHTTPDSquidCompatibilityIT.java | 50 +++- .../compatibility/ContainerImages.java | 54 ++++- .../HttpAsyncClientCompatibilityTest.java | 76 ++++++ ...HttpAsyncClientHttp1CompatibilityTest.java | 36 ++- .../spnego/KeytabConfiguration.java | 73 ++++++ .../spnego/SpnegoAuthenticationStrategy.java | 49 ++++ .../compatibility/spnego/SpnegoTestUtil.java | 136 +++++++++++ .../spnego/UseJaasCredentials.java | 45 ++++ .../sync/HttpClientCompatibilityTest.java | 58 +++++ .../test/resources/docker/httpd/httpd.conf | 17 +- .../src/test/resources/docker/httpd/start.sh | 19 ++ .../src/test/resources/docker/kdc/krb5.conf | 35 +++ .../src/test/resources/docker/kdc/start.sh | 36 +++ .../src/test/resources/log4j2.xml | 6 +- .../http/impl/auth/MutualGssSchemeBase.java | 25 +- pom.xml | 27 ++- 17 files changed, 940 insertions(+), 23 deletions(-) create mode 100644 httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java create mode 100644 httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java create mode 100644 httpclient5-testing/src/test/resources/docker/httpd/start.sh create mode 100644 httpclient5-testing/src/test/resources/docker/kdc/krb5.conf create mode 100644 httpclient5-testing/src/test/resources/docker/kdc/start.sh diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java new file mode 100644 index 0000000000..ae3ad76ecc --- /dev/null +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/util/SecurityUtils.java @@ -0,0 +1,221 @@ +/* + * ==================================================================== + * 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.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; +import javax.security.auth.Subject; + +import org.apache.hc.core5.annotation.Internal; + +/** + * This class is based on SecurityUtils in Apache Avatica which is loosely based on SecurityUtils in + * Jetty 12.0 + *

+ * Collections of utility methods to deal with the scheduled removal of the security classes defined + * by JEP 411. + *

+ */ +@Internal +public class SecurityUtils { + private static final MethodHandle CALL_AS = lookupCallAs(); + private static final MethodHandle CURRENT = lookupCurrent(); + private static final MethodHandle DO_PRIVILEGED = lookupDoPrivileged(); + + private SecurityUtils() { + } + + private static MethodHandle lookupCallAs() { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + try { + // Subject.doAs() is deprecated for removal and replaced by Subject.callAs(). + // Lookup first the new API, since for Java versions where both exist, the + // new API delegates to the old API (for example Java 18, 19 and 20). + // Otherwise (Java 17), lookup the old API. + return lookup.findStatic(Subject.class, "callAs", + MethodType.methodType(Object.class, Subject.class, Callable.class)); + } catch (final NoSuchMethodException x) { + try { + // Lookup the old API. + final MethodType oldSignature = + MethodType.methodType(Object.class, Subject.class, + PrivilegedExceptionAction.class); + final MethodHandle doAs = + lookup.findStatic(Subject.class, "doAs", oldSignature); + // Convert the Callable used in the new API to the PrivilegedAction used in the + // old + // API. + final MethodType convertSignature = + MethodType.methodType(PrivilegedExceptionAction.class, Callable.class); + final MethodHandle converter = + lookup.findStatic(SecurityUtils.class, + "callableToPrivilegedExceptionAction", convertSignature); + return MethodHandles.filterArguments(doAs, 1, converter); + } catch (final NoSuchMethodException e) { + throw new AssertionError(e); + } + } + } catch (final IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupDoPrivileged() { + try { + // Use reflection to work with Java versions that have and don't have AccessController. + final Class klass = + ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController"); + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + return lookup.findStatic(klass, "doPrivileged", + MethodType.methodType(Object.class, PrivilegedAction.class)); + } catch (final NoSuchMethodException | IllegalAccessException x) { + // Assume that single methods won't be removed from AcessController + throw new AssertionError(x); + } catch (final ClassNotFoundException e) { + return null; + } + } + + private static MethodHandle lookupCurrent() { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + // Subject.getSubject(AccessControlContext) is deprecated for removal and replaced by + // Subject.current(). + // Lookup first the new API, since for Java versions where both exists, the + // new API delegates to the old API (for example Java 18, 19 and 20). + // Otherwise (Java 17), lookup the old API. + return lookup.findStatic(Subject.class, "current", + MethodType.methodType(Subject.class)); + } catch (final NoSuchMethodException e) { + final MethodHandle getContext = lookupGetContext(); + final MethodHandle getSubject = lookupGetSubject(); + return MethodHandles.filterReturnValue(getContext, getSubject); + } catch (final IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetSubject() { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + final Class contextklass = + ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + return lookup.findStatic(Subject.class, "getSubject", + MethodType.methodType(Subject.class, contextklass)); + } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetContext() { + try { + // Use reflection to work with Java versions that have and don't have AccessController. + final Class controllerKlass = + ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController"); + final Class contextklass = + ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + return lookup.findStatic(controllerKlass, "getContext", + MethodType.methodType(contextklass)); + } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + /** + * Maps to AccessController#doPrivileged if available, otherwise returns action.run(). + * @param action the action to run + * @return the result of running the action + * @param the type of the result + */ + public static T doPrivileged(final PrivilegedAction action) { + // Keep this method short and inlineable. + if (DO_PRIVILEGED == null) { + return action.run(); + } + return doPrivileged(DO_PRIVILEGED, action); + } + + private static T doPrivileged(final MethodHandle doPrivileged, final PrivilegedAction action) { + try { + return (T) doPrivileged.invoke(action); + } catch (final Throwable t) { + throw sneakyThrow(t); + } + } + + /** + * Maps to Subject.callAs() if available, otherwise maps to Subject.doAs() + * @param subject the subject this action runs as + * @param action the action to run + * @return the result of the action + * @param the type of the result + * @throws CompletionException + */ + public static T callAs(final Subject subject, final Callable action) throws CompletionException { + try { + return (T) CALL_AS.invoke(subject, action); + } catch (final PrivilegedActionException e) { + throw new CompletionException(e.getCause()); + } catch (final Throwable t) { + throw sneakyThrow(t); + } + } + + /** + * Maps to Subject.currect() is available, otherwise maps to Subject.getSubject() + * @return the current subject + */ + public static Subject currentSubject() { + try { + return (Subject) CURRENT.invoke(); + } catch (final Throwable t) { + throw sneakyThrow(t); + } + } + + @SuppressWarnings("unused") + private static PrivilegedExceptionAction + callableToPrivilegedExceptionAction(final Callable callable) { + return callable::call; + } + + @SuppressWarnings("unchecked") + private static RuntimeException sneakyThrow(final Throwable e) throws E { + throw (E) e; + } +} diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java index 68ed99e54b..b933f5ff28 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java @@ -26,11 +26,20 @@ */ package org.apache.hc.client5.testing.compatibility; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import javax.security.auth.Subject; + import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.testing.compatibility.async.CachingHttpAsyncClientCompatibilityTest; import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientCompatibilityTest; import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientHttp1CompatibilityTest; import org.apache.hc.client5.testing.compatibility.async.HttpAsyncClientProxyCompatibilityTest; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; import org.apache.hc.client5.testing.compatibility.sync.CachingHttpClientCompatibilityTest; import org.apache.hc.client5.testing.compatibility.sync.HttpClientCompatibilityTest; import org.apache.hc.client5.testing.compatibility.sync.HttpClientProxyCompatibilityTest; @@ -38,6 +47,7 @@ import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.http2.HttpVersionPolicy; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.testcontainers.containers.GenericContainer; @@ -49,11 +59,24 @@ class ApacheHTTPDSquidCompatibilityIT { private static Network NETWORK = Network.newNetwork(); + private static final Path KEYTAB_DIR = SpnegoTestUtil.createKeytabDir(); + + @Container + static final GenericContainer KDC = ContainerImages.KDC(NETWORK, KEYTAB_DIR); @Container - static final GenericContainer HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK); + static final GenericContainer HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK, KEYTAB_DIR); @Container static final GenericContainer SQUID = ContainerImages.squid(NETWORK); + private static Path KRB5_CONF_PATH; + private static Subject spnegoSubject; + + @BeforeAll + static void init() throws IOException { + KRB5_CONF_PATH = SpnegoTestUtil.prepareKrb5Conf(KDC.getHost() + ":" + KDC.getMappedPort(ContainerImages.KDC_PORT)); + spnegoSubject = SpnegoTestUtil.loginFromKeytab("testclient", KEYTAB_DIR.resolve("testclient.keytab")); + } + static HttpHost targetContainerHost() { return new HttpHost(URIScheme.HTTP.id, HTTPD_CONTAINER.getHost(), HTTPD_CONTAINER.getMappedPort(ContainerImages.HTTP_PORT)); } @@ -82,7 +105,20 @@ static HttpHost proxyPwProtectedContainerHost() { static void cleanup() { SQUID.close(); HTTPD_CONTAINER.close(); + KDC.close(); NETWORK.close(); + try { + Files.delete(KRB5_CONF_PATH); + Files.delete(KRB5_CONF_PATH.getParent()); + try ( Stream dirStream = Files.walk(KEYTAB_DIR)) { + dirStream + .filter(Files::isRegularFile) + .map(Path::toFile) + .forEach(File::delete); + } + } catch (final IOException e) { + //We leave some files around in tmp + } } @Nested @@ -90,7 +126,7 @@ static void cleanup() { class ClassicDirectHttp extends HttpClientCompatibilityTest { public ClassicDirectHttp() throws Exception { - super(targetContainerHost(), null, null); + super(targetContainerHost(), null, null, spnegoSubject); } } @@ -120,7 +156,7 @@ public ClassicViaPwProtectedProxyHttp() throws Exception { class ClassicDirectHttpTls extends HttpClientCompatibilityTest { public ClassicDirectHttpTls() throws Exception { - super(targetContainerTlsHost(), null, null); + super(targetContainerTlsHost(), null, null, spnegoSubject); } } @@ -150,7 +186,7 @@ public ClassicViaPwProtectedProxyHttpTls() throws Exception { class AsyncDirectHttp1 extends HttpAsyncClientHttp1CompatibilityTest { public AsyncDirectHttp1() throws Exception { - super(targetContainerHost(), null, null); + super(targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); } } @@ -180,7 +216,7 @@ public AsyncViaPwProtectedProxyHttp1() throws Exception { class AsyncDirectHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest { public AsyncDirectHttp1Tls() throws Exception { - super(targetContainerTlsHost(), null, null); + super(targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); } } @@ -210,7 +246,7 @@ public AsyncViaPwProtectedProxyHttp1Tls() throws Exception { class AsyncDirectHttp2 extends HttpAsyncClientCompatibilityTest { public AsyncDirectHttp2() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null); + super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); } } @@ -220,7 +256,7 @@ public AsyncDirectHttp2() throws Exception { class AsyncDirectHttp2Tls extends HttpAsyncClientCompatibilityTest { public AsyncDirectHttp2Tls() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null); + super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java index 0cb835b1f6..30633b05d6 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java @@ -27,6 +27,7 @@ package org.apache.hc.client5.testing.compatibility; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.Random; import org.apache.hc.client5.http.utils.ByteArrayBuilder; @@ -45,6 +46,8 @@ public final class ContainerImages { public final static String WEB_SERVER = "test-httpd"; public final static int HTTP_PORT = 8080; public final static int HTTPS_PORT = 8443; + public final static String KDC_SERVER = "test-kdc"; + public final static int KDC_PORT = 88; public final static String PROXY = "test-proxy"; public final static int PROXY_PORT = 8888; public final static int PROXY_PW_PROTECTED_PORT = 8889; @@ -62,12 +65,14 @@ static byte[] randomData(final int max) { return builder.toByteArray(); } - public static GenericContainer apacheHttpD(final Network network) { + public static GenericContainer apacheHttpD(final Network network, final Path keytabsHostPath) { return new GenericContainer<>(new ImageFromDockerfile() .withFileFromClasspath("server-cert.pem", "docker/server-cert.pem") .withFileFromClasspath("server-key.pem", "docker/server-key.pem") .withFileFromClasspath("httpd.conf", "docker/httpd/httpd.conf") .withFileFromClasspath("httpd-ssl.conf", "docker/httpd/httpd-ssl.conf") + .withFileFromClasspath("start.sh", "docker/httpd/start.sh") + .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf") .withFileFromTransferable("111", Transferable.of(randomData(10240))) .withFileFromTransferable("222", Transferable.of(randomData(10240))) .withFileFromTransferable("333", Transferable.of(randomData(10240))) @@ -78,6 +83,7 @@ public static GenericContainer apacheHttpD(final Network network) { .env("var_dir", "/var/httpd") .env("www_dir", "${var_dir}/www") .env("private_dir", "${www_dir}/private") + .env("private_spnego_dir", "${www_dir}/private_spnego") .run("mkdir ${httpd_home}/ssl") .copy("server-cert.pem", "${httpd_home}/ssl/") .copy("server-key.pem", "${httpd_home}/ssl/") @@ -86,14 +92,30 @@ public static GenericContainer apacheHttpD(final Network network) { .copy("111", "${www_dir}/") .copy("222", "${www_dir}/") .copy("333", "${www_dir}/") + .copy("start.sh", "/usr/local/bin/") .run("mkdir -p ${private_dir}") + .run("mkdir -p ${private_spnego_dir}") //# user: testuser; pwd: nopassword - .run("echo \"testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${private_dir}/.htpasswd") - .run("echo \"testuser:Restricted Files:73deccd22e07066db8c405e5364335f5\" > ${private_dir}/.htpasswd_digest") - .run("echo \"Big Secret\" > ${private_dir}/big-secret.txt") + .run("echo \"testuser:{SHA}0Ybo2sSKJNARW1aNCrLJ6Lguats=\" > ${private_dir}/.htpasswd;" + + "echo \"testuser:Restricted Files:73deccd22e07066db8c405e5364335f5\" > ${private_dir}/.htpasswd_digest;" + + "echo \"Big Secret\" > ${private_dir}/big-secret.txt;" + + "echo \"Big Secret\" > ${private_spnego_dir}/big-secret.txt") + .env("MOD_AUTH_GSSAPI_PREFIX", "/usr/local/mod_auth_gssapi") + .run("mkdir -p \"$MOD_AUTH_GSSAPI_PREFIX\"") + .workDir("$MOD_AUTH_GSSAPI_PREFIX") + .run("apt-get update; apt-get install -y krb5-user libkrb5-dev " + + " wget automake libtool pkg-config bison flex " + + " libapr1-dev libaprutil1-dev libssl-dev make;" + + " wget https://github.com/gssapi/mod_auth_gssapi/releases/download/v1.6.5/mod_auth_gssapi-1.6.5.tar.gz;" + + " mkdir src; cd src; tar xfvz ../mod_auth_gssapi-1.6.5.tar.gz") + .run("cd src/mod_auth_gssapi-1.6.5;" + + " autoreconf -fi; ./configure; make; make install") + .copy("krb5.conf", "/etc/krb5.conf") + .cmd("/bin/sh", "/usr/local/bin/start.sh") .build())) .withNetwork(network) .withNetworkAliases(WEB_SERVER) + .withFileSystemBind(keytabsHostPath.toString(), "/keytabs") .withLogConsumer(new Slf4jLogConsumer(LOG)) .withExposedPorts(HTTP_PORT, HTTPS_PORT); } @@ -116,4 +138,28 @@ public static GenericContainer squid(final Network network) { } + // This image builds on Ubuntu 24.04 and uses the included KDC + public static GenericContainer KDC(final Network network, final Path keytabsHostPath) { + return new GenericContainer<>(new ImageFromDockerfile() + .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf") + .withFileFromClasspath("start.sh", "docker/kdc/start.sh") + .withDockerfileFromBuilder(builder -> + builder + .from("ubuntu:noble") + .workDir("/workdir") + .volume("/keytabs") + .expose(KDC_PORT) + .copy("krb5.conf", "/etc/krb5.conf") + .copy("start.sh", ".") + .run("mkdir /var/log/kerberos && apt-get update" + + " && apt-get -y install krb5-kdc krb5-admin-server") + .cmd("/bin/sh", "start.sh") + .build())) + .withNetwork(network) + .withNetworkAliases(KDC_SERVER) + .withLogConsumer(new Slf4jLogConsumer(LOG)) + .withExposedPorts(KDC_PORT) + .withFileSystemBind(keytabsHostPath.toString(), "/keytabs"); + } + } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java index e6019d9952..511b6c0e0f 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java @@ -26,11 +26,16 @@ */ package org.apache.hc.client5.testing.compatibility.async; +import static org.junit.Assume.assumeNotNull; + import java.util.Queue; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; +import javax.security.auth.Subject; + import org.apache.hc.client5.http.ContextBuilder; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; @@ -42,7 +47,11 @@ import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.testing.Result; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; +import org.apache.hc.client5.testing.compatibility.spnego.UseJaasCredentials; import org.apache.hc.client5.testing.extension.async.HttpAsyncClientResource; +import org.apache.hc.client5.testing.util.SecurityUtils; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpStatus; @@ -51,6 +60,7 @@ import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.util.Timeout; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -63,27 +73,45 @@ public abstract class HttpAsyncClientCompatibilityTest { private final HttpHost target; @RegisterExtension private final HttpAsyncClientResource clientResource; + private final HttpAsyncClientResource spnegoClientResource; private final BasicCredentialsProvider credentialsProvider; + protected final Subject spnegoSubject; public HttpAsyncClientCompatibilityTest( final HttpVersionPolicy versionPolicy, final HttpHost target, final HttpHost proxy, final Credentials proxyCreds) throws Exception { + this(versionPolicy, target, proxy, proxyCreds, null); + } + + public HttpAsyncClientCompatibilityTest( + final HttpVersionPolicy versionPolicy, + final HttpHost target, + final HttpHost proxy, + final Credentials proxyCreds, + final Subject spnegoSubject) throws Exception { this.versionPolicy = versionPolicy; this.target = target; this.clientResource = new HttpAsyncClientResource(versionPolicy); + this.spnegoClientResource = new HttpAsyncClientResource(versionPolicy); this.clientResource.configure(builder -> builder.setProxy(proxy)); + this.spnegoClientResource.configure(builder -> builder.setProxy(proxy).setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()).setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); this.credentialsProvider = new BasicCredentialsProvider(); if (proxy != null && proxyCreds != null) { this.credentialsProvider.setCredentials(new AuthScope(proxy), proxyCreds); } + this.spnegoSubject = spnegoSubject; } CloseableHttpAsyncClient client() { return clientResource.client(); } + CloseableHttpAsyncClient spnegoClient() { + return spnegoClientResource.client(); + } + HttpClientContext context() { return ContextBuilder.create() .useCredentialsProvider(credentialsProvider) @@ -228,4 +256,52 @@ void test_auth_success() throws Exception { assertProtocolVersion(context); } + // This does not work. + // Looks like by the time the SPNEGO negotiations happens, we're in another thread, + // and Subject is no longer set. We could save the subject somewhere, or just document this. + @Disabled + @Test + void test_spnego_auth_success_implicit() throws Exception { + assumeNotNull(spnegoSubject); + addCredentials( + new AuthScope(target), + new UseJaasCredentials()); + final CloseableHttpAsyncClient client = spnegoClient(); + final HttpClientContext context = context(); + final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/private_spnego/big-secret.txt") + .build(); + + final Future future = SecurityUtils.callAs(spnegoSubject, new Callable>() { + @Override + public Future call() throws Exception { + return client.execute(httpGetSecret, context, null); + } + }); + + final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + assertProtocolVersion(context); + } + + @Test + void test_spnego_auth_success() throws Exception { + assumeNotNull(spnegoSubject); + addCredentials( + new AuthScope(target), + SpnegoTestUtil.createCredentials(spnegoSubject)); + final CloseableHttpAsyncClient client = spnegoClient(); + final HttpClientContext context = context(); + final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/private_spnego/big-secret.txt") + .build(); + + final Future future = client.execute(httpGetSecret, context, null); + + final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + assertProtocolVersion(context); + } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java index 901e655c4b..9813c8aa84 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java @@ -26,8 +26,12 @@ */ package org.apache.hc.client5.testing.compatibility.async; +import static org.junit.Assume.assumeNotNull; + import java.util.concurrent.Future; +import javax.security.auth.Subject; + import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; @@ -36,6 +40,7 @@ import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; import org.apache.hc.core5.http.HeaderElements; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; @@ -52,7 +57,16 @@ public HttpAsyncClientHttp1CompatibilityTest( final HttpHost target, final HttpHost proxy, final Credentials proxyCreds) throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds); + super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds, null); + this.target = target; + } + + public HttpAsyncClientHttp1CompatibilityTest( + final HttpHost target, + final HttpHost proxy, + final Credentials proxyCreds, + final Subject spnegoSubject) throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds, spnegoSubject); this.target = target; } @@ -75,4 +89,24 @@ void test_auth_success_no_keep_alive() throws Exception { assertProtocolVersion(context); } + @Test + void test_spnego_auth_success_no_keep_alive() throws Exception { + assumeNotNull(spnegoSubject); + addCredentials( + new AuthScope(target), + SpnegoTestUtil.createCredentials(spnegoSubject)); + final CloseableHttpAsyncClient client = spnegoClient(); + final HttpClientContext context = context(); + final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() + .setHttpHost(target) + .setPath("/private_spnego/big-secret.txt") + .addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE) + .build(); + + final Future future = client.execute(httpGetSecret, context, null); + + final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + assertProtocolVersion(context); + } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java new file mode 100644 index 0000000000..9206b8de57 --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/KeytabConfiguration.java @@ -0,0 +1,73 @@ +/* + * ==================================================================== + * 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.compatibility.spnego; + +import java.nio.file.Path; +import java.util.HashMap; + +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +public class KeytabConfiguration extends Configuration { + private static final String IBM_KRB5_LOGIN_MODULE = + "com.ibm.security.auth.module.Krb5LoginModule"; + private static final String SUN_KRB5_LOGIN_MODULE = + "com.sun.security.auth.module.Krb5LoginModule"; + + private static final String JAVA_VENDOR_NAME = System.getProperty("java.vendor"); + private static final boolean IS_IBM_JAVA = JAVA_VENDOR_NAME.contains("IBM"); + + private final String principal; + private final Path keytabFilePath; + + public KeytabConfiguration(final String principal, final Path keyTabFilePath) { + this.principal = principal; + this.keytabFilePath = keyTabFilePath; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final HashMap options = new HashMap<>(); + + if (IS_IBM_JAVA) { + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("useKeytab", "file://" + keytabFilePath.normalize().toString()); + return new AppConfigurationEntry[] { new AppConfigurationEntry( + IBM_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options) }; + } else { + options.put("principal", principal); + options.put("doNotPrompt", "true"); + options.put("useKeyTab", "true"); + options.put("keyTab", keytabFilePath.normalize().toString()); + return new AppConfigurationEntry[] { new AppConfigurationEntry( + SUN_KRB5_LOGIN_MODULE, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options) }; + } + } +} \ No newline at end of file diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java new file mode 100644 index 0000000000..b1c1efec77 --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java @@ -0,0 +1,49 @@ +/* + * ==================================================================== + * 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.compatibility.spnego; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; + +public 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; + } +} \ No newline at end of file diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java new file mode 100644 index 0000000000..ed4188a568 --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java @@ -0,0 +1,136 @@ +/* + * ==================================================================== + * 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.compatibility.spnego; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.KerberosCredentials; +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.impl.auth.MutualSpnegoSchemeFactory; +import org.apache.hc.client5.testing.compatibility.ContainerImages; +import org.apache.hc.client5.testing.util.SecurityUtils; +import org.apache.hc.core5.http.config.Registry; +import org.apache.hc.core5.http.config.RegistryBuilder; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; + + +public class SpnegoTestUtil { + + public static KerberosCredentials createCredentials(final Subject subject) { + return SecurityUtils.callAs(subject, new Callable() { + @Override + public KerberosCredentials call() throws Exception { + return new KerberosCredentials(GSSManager.getInstance().createCredential(GSSCredential.INITIATE_ONLY)); + } + }); + } + + public static Path createKeytabDir() { + try { + return Files.createTempDirectory("keytabs"); + } catch (final IOException e) { + return Paths.get("/tmp/keytabs"); + } + } + + public static Registry getSpnegoSchemeRegistry() { + return RegistryBuilder.create() + .register(StandardAuthScheme.SPNEGO, MutualSpnegoSchemeFactory.DEFAULT) + // register other schemes as needed + .build(); + } + + public static Subject loginFromKeytab(final String principal, final Path keytabFilePath) { + final Configuration kerberosConfig = new KeytabConfiguration(principal, keytabFilePath); + final Subject subject = new Subject(); + + final LoginContext lc; + try { + lc = new LoginContext("SPNEGOTest", subject, new CallbackHandler() { + @Override + public void handle(final Callback[] callbacks) + throws IOException, UnsupportedCallbackException { + throw new UnsupportedCallbackException(callbacks[0], + "Only keytab supported"); + } + }, kerberosConfig); + lc.login(); + return subject; + } catch (final LoginException e) { + throw new RuntimeException(e); + } + } + + /** + * Updates the krb5.conf file with the specified host and port, + * writes it to a tmp file, + * and sets the java.security.krb5.conf system property to point to it. + * + * @param KdcHostPort + * @return Path to the updated krb5.conf file + * @throws IOException + */ + public static Path prepareKrb5Conf(final String KdcHostPort) throws IOException { + // Copy krb5.conf to filesystem + final InputStream krb5 = SpnegoTestUtil.class.getResourceAsStream( + "/docker/kdc/krb5.conf"); + // replace KDC address + final String krb5In; + try (final BufferedReader reader = new BufferedReader( + new InputStreamReader(krb5, StandardCharsets.UTF_8))) { + krb5In = reader.lines() + .collect(Collectors.joining("\n")); + } + final String krb5Out = krb5In.replaceAll(ContainerImages.KDC_SERVER, KdcHostPort); + final Path tmpKrb5 = Files.createTempDirectory("test_krb_config_dir") + .resolve("krb5.conf"); + Files.write(tmpKrb5, krb5Out.getBytes(StandardCharsets.UTF_8)); + // Set the copied krb5.conf for java + System.setProperty("java.security.krb5.conf", tmpKrb5.toString()); + return tmpKrb5; + } + +} diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java new file mode 100644 index 0000000000..272d370d38 --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/UseJaasCredentials.java @@ -0,0 +1,45 @@ +/* + * ==================================================================== + * 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.compatibility.spnego; + +import java.security.Principal; + +import org.apache.hc.client5.http.auth.Credentials; + +public class UseJaasCredentials implements Credentials { + + @Override + public char[] getPassword() { + return null; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + +} \ No newline at end of file diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java index 6db3469524..c2ae1332e0 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java @@ -26,6 +26,11 @@ */ package org.apache.hc.client5.testing.compatibility.sync; + +import java.util.concurrent.Callable; + +import javax.security.auth.Subject; + import org.apache.hc.client5.http.ContextBuilder; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; @@ -36,7 +41,11 @@ import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy; +import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; +import org.apache.hc.client5.testing.compatibility.spnego.UseJaasCredentials; import org.apache.hc.client5.testing.extension.sync.HttpClientResource; +import org.apache.hc.client5.testing.util.SecurityUtils; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpHeaders; @@ -45,6 +54,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -53,13 +63,22 @@ public abstract class HttpClientCompatibilityTest { private final HttpHost target; @RegisterExtension private final HttpClientResource clientResource; + private final HttpClientResource spnegoClientResource; private final CredentialsStore credentialsProvider; + private final Subject spnegoSubject; public HttpClientCompatibilityTest(final HttpHost target, final HttpHost proxy, final Credentials proxyCreds) throws Exception { + this(target, proxy, proxyCreds, null); + } + + public HttpClientCompatibilityTest(final HttpHost target, final HttpHost proxy, final Credentials proxyCreds, final Subject spnegoSubject) throws Exception { this.target = target; this.clientResource = new HttpClientResource(); this.clientResource.configure(builder -> builder.setProxy(proxy)); + this.spnegoClientResource = new HttpClientResource(); + this.spnegoClientResource.configure(builder -> builder.setProxy(proxy).setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()).setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); this.credentialsProvider = new BasicCredentialsProvider(); + this.spnegoSubject = spnegoSubject; if (proxy != null && proxyCreds != null) { this.addCredentials(new AuthScope(proxy), proxyCreds); } @@ -69,6 +88,10 @@ CloseableHttpClient client() { return clientResource.client(); } + CloseableHttpClient spnegoClient() { + return spnegoClientResource.client(); + } + HttpClientContext context() { return ContextBuilder.create() .useCredentialsProvider(credentialsProvider) @@ -185,4 +208,39 @@ void test_correct_target_credentials_no_keep_alive() throws Exception { } } + @Test + void test_spnego_correct_target_credentials_implicit() throws Exception { + Assumptions.assumeFalse(spnegoSubject == null); + addCredentials(new AuthScope(target), new UseJaasCredentials()); + final CloseableHttpClient client = spnegoClient(); + final HttpClientContext context = context(); + + final ClassicHttpRequest request = new HttpGet("/private_spnego/big-secret.txt"); + try (ClassicHttpResponse response = + SecurityUtils.callAs(spnegoSubject, new Callable() { + @Override + public ClassicHttpResponse call() throws Exception { + return client.executeOpen(target, request, context); + } + });) { + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + EntityUtils.consume(response.getEntity()); + } + } + + @Test + void test_spnego_correct_target_credentials() throws Exception { + Assumptions.assumeFalse(spnegoSubject == null); + addCredentials( + new AuthScope(target), + SpnegoTestUtil.createCredentials(spnegoSubject)); + final CloseableHttpClient client = spnegoClient(); + final HttpClientContext context = context(); + + final ClassicHttpRequest request = new HttpGet("/private_spnego/big-secret.txt"); + try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { + Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); + EntityUtils.consume(response.getEntity()); + } + } } diff --git a/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf b/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf index f9931ea71b..cbb11f761a 100644 --- a/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf +++ b/httpclient5-testing/src/test/resources/docker/httpd/httpd.conf @@ -77,6 +77,7 @@ Listen 8080 # Example: # LoadModule foo_module modules/mod_foo.so # +LoadModule auth_gssapi_module modules/mod_auth_gssapi.so LoadModule mpm_event_module modules/mod_mpm_event.so #LoadModule mpm_prefork_module modules/mod_mpm_prefork.so #LoadModule mpm_worker_module modules/mod_mpm_worker.so @@ -340,7 +341,7 @@ DocumentRoot "/var/httpd/www" # logged here. If you *do* define an error logfile for a # container, that host's errors will be logged there and not here. # -ErrorLog /proc/self/fd/2 +ErrorLog /proc/1/fd/2 # # LogLevel: Control the number of messages logged to the error_log. @@ -369,7 +370,7 @@ LogLevel warn # define per- access logfiles, transactions will be # logged therein and *not* in this file. # - CustomLog /proc/self/fd/1 common + CustomLog /proc/1/fd/1 common # # If you prefer a logfile with access, agent, and referer information @@ -595,3 +596,15 @@ SSLRandomSeed connect builtin Require valid-user + + + + + AuthType GSSAPI + AuthName "GSSAPI Single Sign On Login" + GssapiCredStore keytab:/keytabs/HTTP.keytab + GssapiAcceptorName HTTP + Require valid-user + + + diff --git a/httpclient5-testing/src/test/resources/docker/httpd/start.sh b/httpclient5-testing/src/test/resources/docker/httpd/start.sh new file mode 100644 index 0000000000..8f1998804d --- /dev/null +++ b/httpclient5-testing/src/test/resources/docker/httpd/start.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# 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. +# ========================================================================== +# +chmod 777 /keytabs +/usr/local/bin/httpd-foreground diff --git a/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf b/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf new file mode 100644 index 0000000000..a9ce6acc50 --- /dev/null +++ b/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf @@ -0,0 +1,35 @@ +# 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. +# ========================================================================== +# +[libdefaults] + default_realm = EXAMPLE.ORG + forwardable = true + udp_preference_limit = 1 + + +[realms] + EXAMPLE.ORG = { + kdc = test-kdc + } + +[domain_realm] + .example.org = EXAMPLE.ORG + example.org = EXAMPLE.ORG + +[logging] + kdc = FILE:/var/log/kerberos/krb5kdc.log + admin_server = FILE:/var/log/kerberos/kadmin.log + default = FILE:/var/log/kerberos/krb5lib.log diff --git a/httpclient5-testing/src/test/resources/docker/kdc/start.sh b/httpclient5-testing/src/test/resources/docker/kdc/start.sh new file mode 100644 index 0000000000..7171cfee23 --- /dev/null +++ b/httpclient5-testing/src/test/resources/docker/kdc/start.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# 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. +# ========================================================================== +# +# The default image has no init +kdb5_util -P unsafe create -s +echo Kerberos DB created +krb5kdc +echo KDC started +useradd testclient +echo testclient:testclient | chpasswd +kadmin.local addprinc -pw HTTP HTTP/localhost@EXAMPLE.ORG +kadmin.local addprinc -pw testclient testclient@EXAMPLE.ORG +kadmin.local addprinc -pw testpwclient testpwclient@EXAMPLE.ORG +rm /keytabs/testclient.keytab +rm /keytabs/HTTP.keytab +kadmin.local ktadd -k /keytabs/testclient.keytab testclient@EXAMPLE.ORG +kadmin.local ktadd -k /keytabs/HTTP.keytab HTTP/localhost@EXAMPLE.ORG +chmod 666 /keytabs/testclient.keytab +chmod 666 /keytabs/HTTP.keytab +echo keytabs written +sleep 3600 + diff --git a/httpclient5-testing/src/test/resources/log4j2.xml b/httpclient5-testing/src/test/resources/log4j2.xml index dff8a53814..4c72fff550 100644 --- a/httpclient5-testing/src/test/resources/log4j2.xml +++ b/httpclient5-testing/src/test/resources/log4j2.xml @@ -15,15 +15,17 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + + + \ No newline at end of file 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 index 33021f5197..4241c068ec 100644 --- 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 @@ -128,9 +128,10 @@ public void processChallenge(final AuthChallenge authChallenge, @Override public void processChallenge( final HttpHost host, + final boolean challenged, final AuthChallenge authChallenge, - final HttpContext context, - final boolean challenged) throws AuthenticationException { + final HttpContext context + ) throws AuthenticationException { if (challengesLeft-- <= 0 ) { if (LOG.isDebugEnabled()) { @@ -165,6 +166,7 @@ public void processChallenge( LOG.debug("{} GSS init {}", exchangeId, gssHostname); } try { + setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context); queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); switch (state) { case UNINITIATED: @@ -298,14 +300,25 @@ public boolean isResponseReady( Args.notNull(host, "Auth host"); Args.notNull(credentialsProvider, "CredentialsProvider"); - final Credentials credentials = credentialsProvider.getCredentials( - new AuthScope(host, null, getName()), context); + setGssCredential(credentialsProvider, host, context); + return true; + } + + protected void setGssCredential(final CredentialsProvider credentialsProvider, + final HttpHost host, + final HttpContext context) { + if (this.gssCredential != null) { + return; + } + 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(); + this.gssCredential = + ((org.apache.hc.client5.http.auth.KerberosCredentials) credentials) + .getGSSCredential(); } else { this.gssCredential = null; } - return true; } @Override diff --git a/pom.xml b/pom.xml index 9862925f7f..1880903788 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,12 @@ 2.2.21 1.20.4 5.3 - javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer + 9.4.56.v20240826 + 2.1.0 + javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer,java.lang.invoke.MethodHandle + + ${project.build.directory} + @@ -192,6 +197,22 @@ junit-jupiter ${testcontainers.version} + + + org.apache.kerby + kerb-core + ${kerby.version} + + + org.apache.kerby + kerb-client + ${kerby.version} + + + org.apache.kerby + kerb-simplekdc + ${kerby.version} + @@ -270,6 +291,8 @@ com.github.siom79.japicmp japicmp-maven-plugin + + true ${project.groupId} @@ -408,6 +431,8 @@ + + true ${project.groupId} From 2d8f404ffd2053d8839e512871d08984e725e071 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Sat, 22 Feb 2025 20:18:47 +0100 Subject: [PATCH 04/10] Add SPNEGO tests for Squid --- .../ApacheHTTPDSquidCompatibilityIT.java | 321 ++++++++++++++---- .../compatibility/ContainerImages.java | 12 +- .../HttpAsyncClientCompatibilityTest.java | 126 +++---- ...HttpAsyncClientHttp1CompatibilityTest.java | 46 +-- .../spnego/SpnegoAuthenticationStrategy.java | 5 +- .../compatibility/spnego/SpnegoTestUtil.java | 31 +- .../sync/HttpClientCompatibilityTest.java | 112 +++--- .../src/test/resources/docker/httpd/start.sh | 19 -- .../src/test/resources/docker/kdc/start.sh | 4 +- .../test/resources/docker/squid/squid.conf | 8 +- .../test/resources/log4j2-debug.xml.template | 2 + .../src/test/resources/log4j2.xml | 6 +- 12 files changed, 393 insertions(+), 299 deletions(-) delete mode 100644 httpclient5-testing/src/test/resources/docker/httpd/start.sh diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java index b933f5ff28..0b052a07a4 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ApacheHTTPDSquidCompatibilityIT.java @@ -64,9 +64,9 @@ class ApacheHTTPDSquidCompatibilityIT { @Container static final GenericContainer KDC = ContainerImages.KDC(NETWORK, KEYTAB_DIR); @Container - static final GenericContainer HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK, KEYTAB_DIR); + static final GenericContainer HTTPD_CONTAINER = ContainerImages.apacheHttpD(NETWORK, KEYTAB_DIR, KDC); @Container - static final GenericContainer SQUID = ContainerImages.squid(NETWORK); + static final GenericContainer SQUID = ContainerImages.squid(NETWORK, KEYTAB_DIR, KDC); private static Path KRB5_CONF_PATH; private static Subject spnegoSubject; @@ -97,12 +97,18 @@ static HttpHost proxyContainerHost() { return new HttpHost(URIScheme.HTTP.id, SQUID.getHost(), SQUID.getMappedPort(ContainerImages.PROXY_PORT)); } - static HttpHost proxyPwProtectedContainerHost() { + static HttpHost proxyAuthenticatedContainerHost() { return new HttpHost(URIScheme.HTTP.id, SQUID.getHost(), SQUID.getMappedPort(ContainerImages.PROXY_PW_PROTECTED_PORT)); } @AfterAll static void cleanup() { + try { + // Let tail -F for squid logs catch up + Thread.sleep(5 * 1000); + } catch (final InterruptedException e) { + // Do nothring + } SQUID.close(); HTTPD_CONTAINER.close(); KDC.close(); @@ -122,181 +128,372 @@ static void cleanup() { } @Nested - @DisplayName("Classic client: HTTP/1.1, plain, direct connection") - class ClassicDirectHttp extends HttpClientCompatibilityTest { + @DisplayName("Classic client: HTTP/1.1, plain, password, direct connection") + class ClassicDirectHttpPw extends HttpClientCompatibilityTest { + + public ClassicDirectHttpPw() throws Exception { + super(targetContainerHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); + } - public ClassicDirectHttp() throws Exception { - super(targetContainerHost(), null, null, spnegoSubject); + } + + @Nested + @DisplayName("Classic client: HTTP/1.1, plain, SPNEGO, direct connection") + class ClassicDirectHttpSpnego extends HttpClientCompatibilityTest { + + public ClassicDirectHttpSpnego() throws Exception { + super(targetContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); } } @Nested - @DisplayName("Classic client: HTTP/1.1, plain, connection via proxy") + @DisplayName("Classic client: HTTP/1.1, plain, password, connection via proxy") class ClassicViaProxyHttp extends HttpClientCompatibilityTest { public ClassicViaProxyHttp() throws Exception { - super(targetInternalHost(), proxyContainerHost(), null); + super(targetInternalHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), + null); } } @Nested - @DisplayName("Classic client: HTTP/1.1, plain, connection via password protected proxy") - class ClassicViaPwProtectedProxyHttp extends HttpClientCompatibilityTest { - - public ClassicViaPwProtectedProxyHttp() throws Exception { - super(targetInternalHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + @DisplayName("Classic client: HTTP/1.1, TLS, password, direct connection") + class ClassicDirectHttpTlsPw extends HttpClientCompatibilityTest { + + public ClassicDirectHttpTlsPw() throws Exception { + super(targetContainerTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); } } @Nested - @DisplayName("Classic client: HTTP/1.1, TLS, direct connection") - class ClassicDirectHttpTls extends HttpClientCompatibilityTest { - - public ClassicDirectHttpTls() throws Exception { - super(targetContainerTlsHost(), null, null, spnegoSubject); + @DisplayName("Classic client: HTTP/1.1, TLS, SPNEGO, direct connection") + class ClassicDirectHttpTlsSpnego extends HttpClientCompatibilityTest { + + public ClassicDirectHttpTlsSpnego() throws Exception { + super(targetContainerTlsHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); } } @Nested - @DisplayName("Classic client: HTTP/1.1, TLS, connection via proxy (tunnel)") + @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via proxy (tunnel)") class ClassicViaProxyHttpTls extends HttpClientCompatibilityTest { public ClassicViaProxyHttpTls() throws Exception { - super(targetInternalTlsHost(), proxyContainerHost(), null); + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), + null); } } @Nested - @DisplayName("Classic client: HTTP/1.1, TLS, connection via password protected proxy (tunnel)") + @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via password protected proxy (tunnel)") class ClassicViaPwProtectedProxyHttpTls extends HttpClientCompatibilityTest { public ClassicViaPwProtectedProxyHttpTls() throws Exception { - super(targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + } + + } + + @Nested + @DisplayName("Classic client: HTTP/1.1, TLS, password, connection via SPNEGO protected proxy (tunnel)") + class ClassicViaSpnegoProtectedProxyHttpTls extends HttpClientCompatibilityTest { + + public ClassicViaSpnegoProtectedProxyHttpTls() throws Exception { + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject)); } } @Nested - @DisplayName("Async client: HTTP/1.1, plain, direct connection") - class AsyncDirectHttp1 extends HttpAsyncClientHttp1CompatibilityTest { + @DisplayName("Async client: HTTP/1.1, plain, password, direct connection") + class AsyncDirectHttp1Pw extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncDirectHttp1Pw() throws Exception { + super(targetContainerHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); + } + + } - public AsyncDirectHttp1() throws Exception { - super(targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); + @Nested + @DisplayName("Async client: HTTP/1.1, plain, SPNEGO, direct connection") + class AsyncDirectHttp1Spnego extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncDirectHttp1Spnego() throws Exception { + super(targetContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); } } @Nested - @DisplayName("Async client: HTTP/1.1, plain, connection via proxy") + @DisplayName("Async client: HTTP/1.1, plain, password, connection via proxy") class AsyncViaProxyHttp1 extends HttpAsyncClientHttp1CompatibilityTest { public AsyncViaProxyHttp1() throws Exception { - super(targetInternalHost(), proxyContainerHost(), null); + super(targetInternalHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), + null); } } @Nested - @DisplayName("Async client: HTTP/1.1, plain, connection via password protected proxy") - class AsyncViaPwProtectedProxyHttp1 extends HttpAsyncClientHttp1CompatibilityTest { + @DisplayName("Async client: HTTP/1.1, plain, password, connection via password protected proxy") + class AsyncViaPwProtectedProxyHttp1Pw extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncViaPwProtectedProxyHttp1Pw() throws Exception { + super(targetInternalHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + } - public AsyncViaPwProtectedProxyHttp1() throws Exception { - super(targetInternalHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + } + + @Nested + @DisplayName("Async client: HTTP/1.1, plain, passwsord, connection via SPNEGO protected proxy") + class AsyncViaPwProtectedProxyHttp1Spnego extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncViaPwProtectedProxyHttp1Spnego() throws Exception { + super(targetInternalHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject)); } } @Nested - @DisplayName("Async client: HTTP/1.1, TLS, direct connection") - class AsyncDirectHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest { + @DisplayName("Async client: HTTP/1.1, TLS, password, direct connection") + class AsyncDirectHttp1TlsPw extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncDirectHttp1TlsPw() throws Exception { + super(targetContainerTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); + } + + } - public AsyncDirectHttp1Tls() throws Exception { - super(targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); + @Nested + @DisplayName("Async client: HTTP/1.1, TLS, SPNEGO, direct connection") + class AsyncDirectHttp1TlsSpnego extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncDirectHttp1TlsSpnego() throws Exception { + super(targetContainerTlsHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); } } @Nested - @DisplayName("Async client: HTTP/1.1, TLS, connection via proxy (tunnel)") + @DisplayName("Async client: HTTP/1.1, TLS, password, connection via proxy (tunnel)") class AsyncViaProxyHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest { public AsyncViaProxyHttp1Tls() throws Exception { - super(targetInternalTlsHost(), proxyContainerHost(), null); + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), + null); } } @Nested - @DisplayName("Async client: HTTP/1.1, TLS, connection via password protected proxy (tunnel)") - class AsyncViaPwProtectedProxyHttp1Tls extends HttpAsyncClientHttp1CompatibilityTest { - - public AsyncViaPwProtectedProxyHttp1Tls() throws Exception { - super(targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + @DisplayName("Async client: HTTP/1.1, TLS, password, connection via password protected proxy (tunnel)") + class AsyncViaPwProtectedProxyHttp1TlsPw extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncViaPwProtectedProxyHttp1TlsPw() throws Exception { + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); } } @Nested - @DisplayName("Async client: HTTP/2, plain, direct connection") - class AsyncDirectHttp2 extends HttpAsyncClientCompatibilityTest { + @DisplayName("Async client: HTTP/1.1, TLS, password, connection via SPNEGO protected proxy (tunnel)") + class AsyncViaPwProtectedProxyHttp1TlsSpnego extends HttpAsyncClientHttp1CompatibilityTest { + + public AsyncViaPwProtectedProxyHttp1TlsSpnego() throws Exception { + super(targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject)); + } - public AsyncDirectHttp2() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); + } + + @Nested + @DisplayName("Async client: HTTP/2, plain, password, direct connection") + class AsyncDirectHttp2Pw extends HttpAsyncClientCompatibilityTest { + + public AsyncDirectHttp2Pw() throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); } } @Nested - @DisplayName("Async client: HTTP/2, TLS, direct connection") - class AsyncDirectHttp2Tls extends HttpAsyncClientCompatibilityTest { + @DisplayName("Async client: HTTP/2, plain, SPNEGO, direct connection") + class AsyncDirectHttp2Spnego extends HttpAsyncClientCompatibilityTest { + + public AsyncDirectHttp2Spnego() throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_2, + targetContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); + } + + } - public AsyncDirectHttp2Tls() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetContainerTlsHost(), null, null, ApacheHTTPDSquidCompatibilityIT.spnegoSubject); + @Nested + @DisplayName("Async client: HTTP/2, TLS, password, direct connection") + class AsyncDirectHttp2TlsPw extends HttpAsyncClientCompatibilityTest { + + public AsyncDirectHttp2TlsPw() throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_2, + targetContainerTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + null, + null); } } @Nested - @DisplayName("Async client: HTTP/2, TLS, connection via proxy (tunnel)") + @DisplayName("Async client: HTTP/2, TLS, SPNEGO, direct connection") + class AsyncDirectHttp2TlsSpnego extends HttpAsyncClientCompatibilityTest { + + public AsyncDirectHttp2TlsSpnego() throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_2, + targetContainerTlsHost(), + SpnegoTestUtil.createCredentials(spnegoSubject), + null, + null); + } + + } + @Nested + @DisplayName("Async client: HTTP/2, TLS, password, connection via proxy (tunnel)") class AsyncViaProxyHttp2Tls extends HttpAsyncClientCompatibilityTest { public AsyncViaProxyHttp2Tls() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetInternalTlsHost(), proxyContainerHost(), null); + super(HttpVersionPolicy.FORCE_HTTP_2, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), null); } } @Nested - @DisplayName("Async client: HTTP/2, TLS, connection via password protected proxy (tunnel)") + @DisplayName("Async client: HTTP/2, TLS, password, connection via password protected proxy (tunnel)") class AsyncViaPwProtectedProxyHttp2Tls extends HttpAsyncClientCompatibilityTest { public AsyncViaPwProtectedProxyHttp2Tls() throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_2, targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + super(HttpVersionPolicy.FORCE_HTTP_2, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + } + + } + + @Nested + @DisplayName("Async client: HTTP/2, TLS, password, connection via SPNEGO protected proxy (tunnel)") + class AsyncViaPwProtectedProxyHttp2TlsSpnego extends HttpAsyncClientCompatibilityTest { + + public AsyncViaPwProtectedProxyHttp2TlsSpnego() throws Exception { + super(HttpVersionPolicy.FORCE_HTTP_2, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject)); } } @Nested - @DisplayName("Async client: protocol negotiate, TLS, connection via proxy (tunnel)") + @DisplayName("Async client: protocol negotiate, TLS, password, connection via proxy (tunnel)") class AsyncViaProxyHttpNegotiateTls extends HttpAsyncClientCompatibilityTest { public AsyncViaProxyHttpNegotiateTls() throws Exception { - super(HttpVersionPolicy.NEGOTIATE, targetInternalTlsHost(), proxyContainerHost(), null); + super(HttpVersionPolicy.NEGOTIATE, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyContainerHost(), + null); } } @Nested - @DisplayName("Async client: protocol negotiate, TLS, connection via password protected proxy (tunnel)") + @DisplayName("Async client: protocol negotiate, TLS, password, connection via password protected proxy (tunnel)") class AsyncViaPwProtectedProxyHttpNegotiateTls extends HttpAsyncClientCompatibilityTest { public AsyncViaPwProtectedProxyHttpNegotiateTls() throws Exception { - super(HttpVersionPolicy.NEGOTIATE, targetInternalTlsHost(), proxyPwProtectedContainerHost(), new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + super(HttpVersionPolicy.NEGOTIATE, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + new UsernamePasswordCredentials("squid", "nopassword".toCharArray())); + } + + } + + @Nested + @DisplayName("Async client: protocol negotiate, TLS, password, connection via SPNEGO protected proxy (tunnel)") + class AsyncViaPwProtectedProxyHttpNegotiateTlsSpnego extends HttpAsyncClientCompatibilityTest { + + public AsyncViaPwProtectedProxyHttpNegotiateTlsSpnego() throws Exception { + super(HttpVersionPolicy.NEGOTIATE, + targetInternalTlsHost(), + new UsernamePasswordCredentials("testuser", "nopassword".toCharArray()), + proxyAuthenticatedContainerHost(), + SpnegoTestUtil.createCredentials(spnegoSubject)); } } @@ -366,7 +563,7 @@ public AsyncCachingHttp2Tls() throws Exception { class HttpClientProxy extends HttpClientProxyCompatibilityTest { public HttpClientProxy() throws Exception { - super(targetInternalTlsHost(), proxyPwProtectedContainerHost()); + super(targetInternalTlsHost(), proxyAuthenticatedContainerHost()); } } @@ -376,7 +573,7 @@ public HttpClientProxy() throws Exception { class AsyncClientProxy extends HttpAsyncClientProxyCompatibilityTest { public AsyncClientProxy() throws Exception { - super(targetInternalTlsHost(), proxyPwProtectedContainerHost()); + super(targetInternalTlsHost(), proxyAuthenticatedContainerHost()); } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java index 30633b05d6..8de58e9ae0 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/ContainerImages.java @@ -65,13 +65,12 @@ static byte[] randomData(final int max) { return builder.toByteArray(); } - public static GenericContainer apacheHttpD(final Network network, final Path keytabsHostPath) { + public static GenericContainer apacheHttpD(final Network network, final Path keytabsHostPath, final GenericContainer dependsOn) { return new GenericContainer<>(new ImageFromDockerfile() .withFileFromClasspath("server-cert.pem", "docker/server-cert.pem") .withFileFromClasspath("server-key.pem", "docker/server-key.pem") .withFileFromClasspath("httpd.conf", "docker/httpd/httpd.conf") .withFileFromClasspath("httpd-ssl.conf", "docker/httpd/httpd-ssl.conf") - .withFileFromClasspath("start.sh", "docker/httpd/start.sh") .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf") .withFileFromTransferable("111", Transferable.of(randomData(10240))) .withFileFromTransferable("222", Transferable.of(randomData(10240))) @@ -92,7 +91,6 @@ public static GenericContainer apacheHttpD(final Network network, final Path .copy("111", "${www_dir}/") .copy("222", "${www_dir}/") .copy("333", "${www_dir}/") - .copy("start.sh", "/usr/local/bin/") .run("mkdir -p ${private_dir}") .run("mkdir -p ${private_spnego_dir}") //# user: testuser; pwd: nopassword @@ -111,28 +109,32 @@ public static GenericContainer apacheHttpD(final Network network, final Path .run("cd src/mod_auth_gssapi-1.6.5;" + " autoreconf -fi; ./configure; make; make install") .copy("krb5.conf", "/etc/krb5.conf") - .cmd("/bin/sh", "/usr/local/bin/start.sh") .build())) .withNetwork(network) .withNetworkAliases(WEB_SERVER) .withFileSystemBind(keytabsHostPath.toString(), "/keytabs") + .dependsOn(dependsOn) .withLogConsumer(new Slf4jLogConsumer(LOG)) .withExposedPorts(HTTP_PORT, HTTPS_PORT); } - public static GenericContainer squid(final Network network) { + public static GenericContainer squid(final Network network, final Path keytabsHostPath, final GenericContainer dependsOn) { return new GenericContainer<>(new ImageFromDockerfile() .withFileFromClasspath("squid.conf", "docker/squid/squid.conf") + .withFileFromClasspath("krb5.conf", "docker/kdc/krb5.conf") .withDockerfileFromBuilder(builder -> builder .from("ubuntu/squid:5.2-22.04_beta") .env("conf_dir", "/etc/squid") .copy("squid.conf", "${conf_dir}/") + .copy("krb5.conf", "/etc/krb5.conf") //# user: squid; pwd: nopassword .run("echo \"squid:\\$apr1\\$.5saX63T\\$cMSoCJPqEfUw9br6zBdSO0\" > ${conf_dir}/htpasswd") .build())) .withNetwork(network) .withNetworkAliases(PROXY) + .dependsOn(dependsOn) + .withFileSystemBind(keytabsHostPath.toString(), "/keytabs") .withLogConsumer(new Slf4jLogConsumer(LOG)) .withExposedPorts(PROXY_PORT, PROXY_PW_PROTECTED_PORT); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java index 511b6c0e0f..1ca7d003a4 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java @@ -26,22 +26,18 @@ */ package org.apache.hc.client5.testing.compatibility.async; -import static org.junit.Assume.assumeNotNull; - import java.util.Queue; -import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; -import javax.security.auth.Subject; - import org.apache.hc.client5.http.ContextBuilder; import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.auth.KerberosCredentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; @@ -49,9 +45,7 @@ import org.apache.hc.client5.testing.Result; import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy; import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; -import org.apache.hc.client5.testing.compatibility.spnego.UseJaasCredentials; import org.apache.hc.client5.testing.extension.async.HttpAsyncClientResource; -import org.apache.hc.client5.testing.util.SecurityUtils; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpStatus; @@ -60,7 +54,6 @@ import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.hc.core5.util.Timeout; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -70,55 +63,60 @@ public abstract class HttpAsyncClientCompatibilityTest { static final Timeout LONG_TIMEOUT = Timeout.ofSeconds(30); private final HttpVersionPolicy versionPolicy; - private final HttpHost target; + protected final HttpHost target; @RegisterExtension private final HttpAsyncClientResource clientResource; - private final HttpAsyncClientResource spnegoClientResource; private final BasicCredentialsProvider credentialsProvider; - protected final Subject spnegoSubject; + protected final Credentials targetCreds; + protected String secretPath = "/private/big-secret.txt"; public HttpAsyncClientCompatibilityTest( final HttpVersionPolicy versionPolicy, final HttpHost target, + final Credentials targetCreds, final HttpHost proxy, final Credentials proxyCreds) throws Exception { - this(versionPolicy, target, proxy, proxyCreds, null); - } - - public HttpAsyncClientCompatibilityTest( - final HttpVersionPolicy versionPolicy, - final HttpHost target, - final HttpHost proxy, - final Credentials proxyCreds, - final Subject spnegoSubject) throws Exception { this.versionPolicy = versionPolicy; this.target = target; - this.clientResource = new HttpAsyncClientResource(versionPolicy); - this.spnegoClientResource = new HttpAsyncClientResource(versionPolicy); - this.clientResource.configure(builder -> builder.setProxy(proxy)); - this.spnegoClientResource.configure(builder -> builder.setProxy(proxy).setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()).setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + this.targetCreds = targetCreds; this.credentialsProvider = new BasicCredentialsProvider(); - if (proxy != null && proxyCreds != null) { - this.credentialsProvider.setCredentials(new AuthScope(proxy), proxyCreds); + this.clientResource = new HttpAsyncClientResource(versionPolicy); + if (targetCreds != null) { + //this.setCredentials(new AuthScope(target), targetCreds); + if (targetCreds instanceof KerberosCredentials) { + secretPath = "/private_spnego/big-secret.txt"; + this.clientResource.configure(builder -> builder + .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + } + } + if (proxy != null) { + this.clientResource.configure(builder -> builder.setProxy(proxy)); + if (proxyCreds != null) { + this.setCredentials(new AuthScope(proxy), proxyCreds); + if (proxyCreds instanceof KerberosCredentials) { + // We disable Mutual Auth, because Squid does not support it. + // There is no way to set separate scheme registry for target/proxy, + // but that's not a problem as SPNEGO cannot be proxied anyway. + this.clientResource.configure(builder -> + builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy()) + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistryNoMutual())); + } + } } - this.spnegoSubject = spnegoSubject; } CloseableHttpAsyncClient client() { return clientResource.client(); } - CloseableHttpAsyncClient spnegoClient() { - return spnegoClientResource.client(); - } - HttpClientContext context() { return ContextBuilder.create() .useCredentialsProvider(credentialsProvider) .build(); } - void addCredentials(final AuthScope authScope, final Credentials credentials) { + void setCredentials(final AuthScope authScope, final Credentials credentials) { credentialsProvider.setCredentials(authScope, credentials); } @@ -204,15 +202,15 @@ public void cancelled() { @Test void test_auth_failure_wrong_auth_scope() throws Exception { - addCredentials( + setCredentials( new AuthScope("http", "otherhost", -1, "Restricted Files", null), - new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); + targetCreds); final CloseableHttpAsyncClient client = client(); final HttpClientContext context = context(); final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() .setHttpHost(target) - .setPath("/private/big-secret.txt") + .setPath(secretPath) .build(); final Future future = client.execute(httpGetSecret, context, null); final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -222,7 +220,7 @@ void test_auth_failure_wrong_auth_scope() throws Exception { @Test void test_auth_failure_wrong_auth_credentials() throws Exception { - addCredentials( + setCredentials( new AuthScope(target), new UsernamePasswordCredentials("testuser", "wrong password".toCharArray())); final CloseableHttpAsyncClient client = client(); @@ -230,7 +228,7 @@ void test_auth_failure_wrong_auth_credentials() throws Exception { final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() .setHttpHost(target) - .setPath("/private/big-secret.txt") + .setPath(secretPath) .build(); final Future future = client.execute(httpGetSecret, context, null); final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -240,15 +238,15 @@ void test_auth_failure_wrong_auth_credentials() throws Exception { @Test void test_auth_success() throws Exception { - addCredentials( + setCredentials( new AuthScope(target), - new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); + targetCreds); final CloseableHttpAsyncClient client = client(); final HttpClientContext context = context(); final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() .setHttpHost(target) - .setPath("/private/big-secret.txt") + .setPath(secretPath) .build(); final Future future = client.execute(httpGetSecret, context, null); final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); @@ -256,52 +254,4 @@ void test_auth_success() throws Exception { assertProtocolVersion(context); } - // This does not work. - // Looks like by the time the SPNEGO negotiations happens, we're in another thread, - // and Subject is no longer set. We could save the subject somewhere, or just document this. - @Disabled - @Test - void test_spnego_auth_success_implicit() throws Exception { - assumeNotNull(spnegoSubject); - addCredentials( - new AuthScope(target), - new UseJaasCredentials()); - final CloseableHttpAsyncClient client = spnegoClient(); - final HttpClientContext context = context(); - final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() - .setHttpHost(target) - .setPath("/private_spnego/big-secret.txt") - .build(); - - final Future future = SecurityUtils.callAs(spnegoSubject, new Callable>() { - @Override - public Future call() throws Exception { - return client.execute(httpGetSecret, context, null); - } - }); - - final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); - assertProtocolVersion(context); - } - - @Test - void test_spnego_auth_success() throws Exception { - assumeNotNull(spnegoSubject); - addCredentials( - new AuthScope(target), - SpnegoTestUtil.createCredentials(spnegoSubject)); - final CloseableHttpAsyncClient client = spnegoClient(); - final HttpClientContext context = context(); - final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() - .setHttpHost(target) - .setPath("/private_spnego/big-secret.txt") - .build(); - - final Future future = client.execute(httpGetSecret, context, null); - - final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); - assertProtocolVersion(context); - } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java index 9813c8aa84..503d915eb5 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientHttp1CompatibilityTest.java @@ -26,21 +26,15 @@ */ package org.apache.hc.client5.testing.compatibility.async; -import static org.junit.Assume.assumeNotNull; - import java.util.concurrent.Future; -import javax.security.auth.Subject; - import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; -import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.protocol.HttpClientContext; -import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; import org.apache.hc.core5.http.HeaderElements; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; @@ -51,36 +45,26 @@ public abstract class HttpAsyncClientHttp1CompatibilityTest extends HttpAsyncClientCompatibilityTest { - private final HttpHost target; public HttpAsyncClientHttp1CompatibilityTest( final HttpHost target, + final Credentials targetCreds, final HttpHost proxy, final Credentials proxyCreds) throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds, null); - this.target = target; - } - - public HttpAsyncClientHttp1CompatibilityTest( - final HttpHost target, - final HttpHost proxy, - final Credentials proxyCreds, - final Subject spnegoSubject) throws Exception { - super(HttpVersionPolicy.FORCE_HTTP_1, target, proxy, proxyCreds, spnegoSubject); - this.target = target; + super(HttpVersionPolicy.FORCE_HTTP_1, target, targetCreds, proxy, proxyCreds); } @Test void test_auth_success_no_keep_alive() throws Exception { - addCredentials( + setCredentials( new AuthScope(target), - new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); + targetCreds); final CloseableHttpAsyncClient client = client(); final HttpClientContext context = context(); final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() .setHttpHost(target) - .setPath("/private/big-secret.txt") + .setPath(secretPath) .addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE) .build(); final Future future = client.execute(httpGetSecret, context, null); @@ -89,24 +73,4 @@ void test_auth_success_no_keep_alive() throws Exception { assertProtocolVersion(context); } - @Test - void test_spnego_auth_success_no_keep_alive() throws Exception { - assumeNotNull(spnegoSubject); - addCredentials( - new AuthScope(target), - SpnegoTestUtil.createCredentials(spnegoSubject)); - final CloseableHttpAsyncClient client = spnegoClient(); - final HttpClientContext context = context(); - final SimpleHttpRequest httpGetSecret = SimpleRequestBuilder.get() - .setHttpHost(target) - .setPath("/private_spnego/big-secret.txt") - .addHeader(HttpHeaders.CONNECTION, HeaderElements.CLOSE) - .build(); - - final Future future = client.execute(httpGetSecret, context, null); - - final SimpleHttpResponse response = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit()); - Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); - assertProtocolVersion(context); - } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java index b1c1efec77..4c48f3ea9e 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoAuthenticationStrategy.java @@ -37,10 +37,11 @@ public class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy private static final List SPNEGO_SCHEME_PRIORITY = Collections.unmodifiableList( - Arrays.asList(StandardAuthScheme.SPNEGO, + Arrays.asList( StandardAuthScheme.BEARER, StandardAuthScheme.DIGEST, - StandardAuthScheme.BASIC)); + StandardAuthScheme.BASIC, + StandardAuthScheme.SPNEGO)); @Override protected final List getSchemePriority() { diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java index ed4188a568..3680981653 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java @@ -34,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; import java.util.concurrent.Callable; import java.util.stream.Collectors; @@ -45,9 +46,14 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; +import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthSchemeFactory; import org.apache.hc.client5.http.auth.KerberosCredentials; +import org.apache.hc.client5.http.auth.MutualKerberosConfig; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; +import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory; +import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory; import org.apache.hc.client5.http.impl.auth.MutualSpnegoSchemeFactory; import org.apache.hc.client5.testing.compatibility.ContainerImages; import org.apache.hc.client5.testing.util.SecurityUtils; @@ -59,18 +65,25 @@ public class SpnegoTestUtil { + static private final MutualKerberosConfig NO_MUTUAL_KERBEROS_CONFIG = + MutualKerberosConfig.custom().setRequestMutualAuth(false).build(); + static private final MutualSpnegoSchemeFactory NO_MUTUAL_SCHEME_FACTORY = + new MutualSpnegoSchemeFactory(NO_MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); + public static KerberosCredentials createCredentials(final Subject subject) { return SecurityUtils.callAs(subject, new Callable() { @Override public KerberosCredentials call() throws Exception { - return new KerberosCredentials(GSSManager.getInstance().createCredential(GSSCredential.INITIATE_ONLY)); + return new KerberosCredentials( + GSSManager.getInstance().createCredential(GSSCredential.INITIATE_ONLY)); } }); } public static Path createKeytabDir() { try { - return Files.createTempDirectory("keytabs"); + return Files.createTempDirectory("keytabs", + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("r-xr-xr-x"))); } catch (final IOException e) { return Paths.get("/tmp/keytabs"); } @@ -78,11 +91,25 @@ public static Path createKeytabDir() { public static Registry getSpnegoSchemeRegistry() { return RegistryBuilder.create() + .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE) + .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) + .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) .register(StandardAuthScheme.SPNEGO, MutualSpnegoSchemeFactory.DEFAULT) // register other schemes as needed .build(); } + //Squid does not support mutual auth + public static Registry getSpnegoSchemeRegistryNoMutual() { + return RegistryBuilder.create() + .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE) + .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) + .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) + .register(StandardAuthScheme.SPNEGO, NO_MUTUAL_SCHEME_FACTORY) + // register other schemes as needed + .build(); + } + public static Subject loginFromKeytab(final String principal, final Path keytabFilePath) { final Configuration kerberosConfig = new KeytabConfiguration(principal, keytabFilePath); final Subject subject = new Subject(); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java index c2ae1332e0..306ff10107 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java @@ -27,14 +27,11 @@ package org.apache.hc.client5.testing.compatibility.sync; -import java.util.concurrent.Callable; - -import javax.security.auth.Subject; - import org.apache.hc.client5.http.ContextBuilder; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsStore; +import org.apache.hc.client5.http.auth.KerberosCredentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpOptions; @@ -43,9 +40,7 @@ import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.testing.compatibility.spnego.SpnegoAuthenticationStrategy; import org.apache.hc.client5.testing.compatibility.spnego.SpnegoTestUtil; -import org.apache.hc.client5.testing.compatibility.spnego.UseJaasCredentials; import org.apache.hc.client5.testing.extension.sync.HttpClientResource; -import org.apache.hc.client5.testing.util.SecurityUtils; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpHeaders; @@ -54,7 +49,6 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -63,24 +57,37 @@ public abstract class HttpClientCompatibilityTest { private final HttpHost target; @RegisterExtension private final HttpClientResource clientResource; - private final HttpClientResource spnegoClientResource; private final CredentialsStore credentialsProvider; - private final Subject spnegoSubject; - - public HttpClientCompatibilityTest(final HttpHost target, final HttpHost proxy, final Credentials proxyCreds) throws Exception { - this(target, proxy, proxyCreds, null); - } + private final Credentials targetCreds; + private String secretPath = "/private/big-secret.txt"; - public HttpClientCompatibilityTest(final HttpHost target, final HttpHost proxy, final Credentials proxyCreds, final Subject spnegoSubject) throws Exception { + public HttpClientCompatibilityTest(final HttpHost target, final Credentials targetCreds, final HttpHost proxy, final Credentials proxyCreds) throws Exception { this.target = target; - this.clientResource = new HttpClientResource(); - this.clientResource.configure(builder -> builder.setProxy(proxy)); - this.spnegoClientResource = new HttpClientResource(); - this.spnegoClientResource.configure(builder -> builder.setProxy(proxy).setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()).setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + this.targetCreds = targetCreds; this.credentialsProvider = new BasicCredentialsProvider(); - this.spnegoSubject = spnegoSubject; - if (proxy != null && proxyCreds != null) { - this.addCredentials(new AuthScope(proxy), proxyCreds); + this.clientResource = new HttpClientResource(); + if (targetCreds != null) { + //this.setCredentials(new AuthScope(target), targetCreds); + if (targetCreds instanceof KerberosCredentials) { + secretPath = "/private_spnego/big-secret.txt"; + this.clientResource.configure(builder -> builder + .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + } + } + if (proxy != null) { + this.clientResource.configure(builder -> builder.setProxy(proxy)); + if (proxyCreds != null) { + this.setCredentials(new AuthScope(proxy), proxyCreds); + if (proxyCreds instanceof KerberosCredentials) { + // We disable Mutual Auth, because Squid does not support it. + // There is no way to set separate scheme registry for target/proxy, + // but that's not a problem as SPNEGO cannot be proxied anyway. + this.clientResource.configure(builder -> + builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy()) + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistryNoMutual())); + } + } } } @@ -88,17 +95,13 @@ CloseableHttpClient client() { return clientResource.client(); } - CloseableHttpClient spnegoClient() { - return spnegoClientResource.client(); - } - HttpClientContext context() { return ContextBuilder.create() .useCredentialsProvider(credentialsProvider) .build(); } - void addCredentials(final AuthScope authScope, final Credentials credentials) { + void setCredentials(final AuthScope authScope, final Credentials credentials) { credentialsProvider.setCredentials(authScope, credentials); } @@ -146,14 +149,14 @@ void test_get_connection_close() throws Exception { @Test void test_wrong_target_auth_scope() throws Exception { - addCredentials( + setCredentials( new AuthScope("http", "otherhost", -1, "Restricted Files", null), new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); final CloseableHttpClient client = client(); final HttpClientContext context = context(); - final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt"); + final ClassicHttpRequest request = new HttpGet(secretPath); try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode()); EntityUtils.consume(response.getEntity()); @@ -162,14 +165,14 @@ void test_wrong_target_auth_scope() throws Exception { @Test void test_wrong_target_credentials() throws Exception { - addCredentials( + setCredentials( new AuthScope(target), new UsernamePasswordCredentials("testuser", "wrong password".toCharArray())); final CloseableHttpClient client = client(); final HttpClientContext context = context(); - final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt"); + final ClassicHttpRequest request = new HttpGet(secretPath); try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode()); EntityUtils.consume(response.getEntity()); @@ -178,13 +181,12 @@ void test_wrong_target_credentials() throws Exception { @Test void test_correct_target_credentials() throws Exception { - addCredentials( - new AuthScope(target), - new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); + setCredentials( + new AuthScope(target), targetCreds); final CloseableHttpClient client = client(); final HttpClientContext context = context(); - final ClassicHttpRequest request = new HttpGet("/private/big-secret.txt"); + final ClassicHttpRequest request = new HttpGet(secretPath); try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); EntityUtils.consume(response.getEntity()); @@ -193,13 +195,12 @@ void test_correct_target_credentials() throws Exception { @Test void test_correct_target_credentials_no_keep_alive() throws Exception { - addCredentials( - new AuthScope(target), - new UsernamePasswordCredentials("testuser", "nopassword".toCharArray())); + setCredentials( + new AuthScope(target), targetCreds); final CloseableHttpClient client = client(); final HttpClientContext context = context(); - final ClassicHttpRequest request = ClassicRequestBuilder.get("/private/big-secret.txt") + final ClassicHttpRequest request = ClassicRequestBuilder.get(secretPath) .addHeader(HttpHeaders.CONNECTION, "close") .build(); try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { @@ -208,39 +209,4 @@ void test_correct_target_credentials_no_keep_alive() throws Exception { } } - @Test - void test_spnego_correct_target_credentials_implicit() throws Exception { - Assumptions.assumeFalse(spnegoSubject == null); - addCredentials(new AuthScope(target), new UseJaasCredentials()); - final CloseableHttpClient client = spnegoClient(); - final HttpClientContext context = context(); - - final ClassicHttpRequest request = new HttpGet("/private_spnego/big-secret.txt"); - try (ClassicHttpResponse response = - SecurityUtils.callAs(spnegoSubject, new Callable() { - @Override - public ClassicHttpResponse call() throws Exception { - return client.executeOpen(target, request, context); - } - });) { - Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); - EntityUtils.consume(response.getEntity()); - } - } - - @Test - void test_spnego_correct_target_credentials() throws Exception { - Assumptions.assumeFalse(spnegoSubject == null); - addCredentials( - new AuthScope(target), - SpnegoTestUtil.createCredentials(spnegoSubject)); - final CloseableHttpClient client = spnegoClient(); - final HttpClientContext context = context(); - - final ClassicHttpRequest request = new HttpGet("/private_spnego/big-secret.txt"); - try (ClassicHttpResponse response = client.executeOpen(target, request, context)) { - Assertions.assertEquals(HttpStatus.SC_OK, response.getCode()); - EntityUtils.consume(response.getEntity()); - } - } } diff --git a/httpclient5-testing/src/test/resources/docker/httpd/start.sh b/httpclient5-testing/src/test/resources/docker/httpd/start.sh deleted file mode 100644 index 8f1998804d..0000000000 --- a/httpclient5-testing/src/test/resources/docker/httpd/start.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -# 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. -# ========================================================================== -# -chmod 777 /keytabs -/usr/local/bin/httpd-foreground diff --git a/httpclient5-testing/src/test/resources/docker/kdc/start.sh b/httpclient5-testing/src/test/resources/docker/kdc/start.sh index 7171cfee23..3156d67ff8 100644 --- a/httpclient5-testing/src/test/resources/docker/kdc/start.sh +++ b/httpclient5-testing/src/test/resources/docker/kdc/start.sh @@ -25,8 +25,8 @@ echo testclient:testclient | chpasswd kadmin.local addprinc -pw HTTP HTTP/localhost@EXAMPLE.ORG kadmin.local addprinc -pw testclient testclient@EXAMPLE.ORG kadmin.local addprinc -pw testpwclient testpwclient@EXAMPLE.ORG -rm /keytabs/testclient.keytab -rm /keytabs/HTTP.keytab +rm -f /keytabs/testclient.keytab +rm -f /keytabs/HTTP.keytab kadmin.local ktadd -k /keytabs/testclient.keytab testclient@EXAMPLE.ORG kadmin.local ktadd -k /keytabs/HTTP.keytab HTTP/localhost@EXAMPLE.ORG chmod 666 /keytabs/testclient.keytab diff --git a/httpclient5-testing/src/test/resources/docker/squid/squid.conf b/httpclient5-testing/src/test/resources/docker/squid/squid.conf index 0f476df6c3..f1fdb56b67 100644 --- a/httpclient5-testing/src/test/resources/docker/squid/squid.conf +++ b/httpclient5-testing/src/test/resources/docker/squid/squid.conf @@ -17,12 +17,18 @@ http_port 8888 http_port 8889 +debug_options ALL,1 + coredump_dir /var/spool/squid auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/htpasswd auth_param basic children 5 auth_param basic realm test-proxy +auth_param negotiate program /usr/lib/squid/negotiate_kerberos_auth -k /keytabs/HTTP.keytab -s HTTP/localhost -d -i +auth_param negotiate children 5 +auth_param negotiate keep_alive on + acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) @@ -71,4 +77,4 @@ refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims refresh_pattern \/InRelease$ 0 0% 0 refresh-ims refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims -refresh_pattern . 0 20% 4320 \ No newline at end of file +refresh_pattern . 0 20% 4320 diff --git a/httpclient5-testing/src/test/resources/log4j2-debug.xml.template b/httpclient5-testing/src/test/resources/log4j2-debug.xml.template index 3386294619..8d540913a7 100644 --- a/httpclient5-testing/src/test/resources/log4j2-debug.xml.template +++ b/httpclient5-testing/src/test/resources/log4j2-debug.xml.template @@ -25,6 +25,8 @@ + + diff --git a/httpclient5-testing/src/test/resources/log4j2.xml b/httpclient5-testing/src/test/resources/log4j2.xml index 4c72fff550..dff8a53814 100644 --- a/httpclient5-testing/src/test/resources/log4j2.xml +++ b/httpclient5-testing/src/test/resources/log4j2.xml @@ -15,17 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. --> - + - + - - \ No newline at end of file From 82c9603f9a787edea4a29deb82cccf5c8282b2c4 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Thu, 27 Feb 2025 14:38:42 +0100 Subject: [PATCH 05/10] Rename and move around classes --- .../compatibility/spnego/SpnegoTestUtil.java | 14 ++++---- ...pnegoScheme.java => TestSpnegoScheme.java} | 32 +++++++++---------- .../hc/client5/http/auth/KerberosConfig.java | 7 ++-- .../http/auth/KerberosCredentials.java | 4 +-- .../client5/http/auth/StandardAuthScheme.java | 2 +- .../GssConfig.java} | 25 ++++++++------- .../client5/http/impl/auth/GGSSchemeBase.java | 6 ++-- .../http/impl/auth/KerberosScheme.java | 5 +-- .../http/impl/auth/KerberosSchemeFactory.java | 6 ++-- .../client5/http/impl/auth/SPNegoScheme.java | 6 ++-- .../http/impl/auth/SPNegoSchemeFactory.java | 6 ++-- .../GssSchemeBase.java} | 23 ++++++------- .../SpnegoScheme.java} | 19 +++++------ .../SpnegoSchemeFactory.java} | 16 +++++----- 14 files changed, 88 insertions(+), 83 deletions(-) rename httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/{TestMutualSpnegoScheme.java => TestSpnegoScheme.java} (95%) rename httpclient5/src/main/java/org/apache/hc/client5/http/auth/{MutualKerberosConfig.java => gss/GssConfig.java} (88%) rename httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/{MutualGssSchemeBase.java => gss/GssSchemeBase.java} (95%) rename httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/{MutualSpnegoScheme.java => gss/SpnegoScheme.java} (84%) rename httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/{MutualSpnegoSchemeFactory.java => gss/SpnegoSchemeFactory.java} (76%) diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java index 3680981653..707572ee9d 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java @@ -49,12 +49,12 @@ import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthSchemeFactory; import org.apache.hc.client5.http.auth.KerberosCredentials; -import org.apache.hc.client5.http.auth.MutualKerberosConfig; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.gss.GssConfig; import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory; import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory; -import org.apache.hc.client5.http.impl.auth.MutualSpnegoSchemeFactory; +import org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory; import org.apache.hc.client5.testing.compatibility.ContainerImages; import org.apache.hc.client5.testing.util.SecurityUtils; import org.apache.hc.core5.http.config.Registry; @@ -65,10 +65,10 @@ public class SpnegoTestUtil { - static private final MutualKerberosConfig NO_MUTUAL_KERBEROS_CONFIG = - MutualKerberosConfig.custom().setRequestMutualAuth(false).build(); - static private final MutualSpnegoSchemeFactory NO_MUTUAL_SCHEME_FACTORY = - new MutualSpnegoSchemeFactory(NO_MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); + static private final GssConfig NO_MUTUAL_KERBEROS_CONFIG = + GssConfig.custom().setRequestMutualAuth(false).build(); + static private final SpnegoSchemeFactory NO_MUTUAL_SCHEME_FACTORY = + new SpnegoSchemeFactory(NO_MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); public static KerberosCredentials createCredentials(final Subject subject) { return SecurityUtils.callAs(subject, new Callable() { @@ -94,7 +94,7 @@ public static Registry getSpnegoSchemeRegistry() { .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE) .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) - .register(StandardAuthScheme.SPNEGO, MutualSpnegoSchemeFactory.DEFAULT) + .register(StandardAuthScheme.SPNEGO, SpnegoSchemeFactory.DEFAULT) // register other schemes as needed .build(); } 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/TestSpnegoScheme.java similarity index 95% rename from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestMutualSpnegoScheme.java rename to httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSpnegoScheme.java index c32b66a4e9..b4e973fc58 100644 --- 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/TestSpnegoScheme.java @@ -43,12 +43,12 @@ 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.MutualKerberosConfig; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.gss.GssConfig; 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.impl.auth.gss.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; @@ -77,11 +77,11 @@ import org.mockito.Mockito; /** - * Tests for {@link SPNegoScheme}. + * Tests for {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme}. */ -public class TestMutualSpnegoScheme extends AbstractIntegrationTestBase { +public class TestSpnegoScheme extends AbstractIntegrationTestBase { - protected TestMutualSpnegoScheme() { + protected TestSpnegoScheme() { super(URIScheme.HTTP, ClientProtocolLevel.STANDARD); } @@ -103,7 +103,7 @@ protected TestMutualSpnegoScheme() { 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 MutualKerberosConfig MUTUAL_KERBEROS_CONFIG = MutualKerberosConfig.DEFAULT; + static GssConfig MUTUAL_KERBEROS_CONFIG = GssConfig.DEFAULT; private static class SpnegoAuthenticationStrategy extends DefaultAuthenticationStrategy { @@ -146,13 +146,13 @@ public void handle( /** * This service implements a normal mutualAuth flow */ - private static class SPNEGOMutualService implements HttpRequestHandler { + private static class SpnegoService implements HttpRequestHandler { int callCount = 1; final boolean sendMutualToken; final byte[] encodedMutualAuthToken; - SPNEGOMutualService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken) { + SpnegoService (final boolean sendMutualToken, final byte[] encodedMutualAuthToken) { this.sendMutualToken = sendMutualToken; this.encodedMutualAuthToken = encodedMutualAuthToken; } @@ -190,14 +190,14 @@ public void handle( * Kerberos configuration. * */ - private static class NegotiateSchemeWithMockGssManager extends MutualSpnegoScheme { + 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(MutualKerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + super(GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); Mockito.when(context.initSecContext( ArgumentMatchers.any(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .thenReturn("12345678".getBytes()); @@ -217,7 +217,7 @@ protected GSSManager getManager() { } - private static class MutualNegotiateSchemeWithMockGssManager extends MutualSpnegoScheme { + private static class MutualNegotiateSchemeWithMockGssManager extends SpnegoScheme { final GSSManager manager = Mockito.mock(GSSManager.class); final GSSName name = Mockito.mock(GSSName.class); @@ -354,7 +354,7 @@ void testNoTokenGeneratedError() throws Exception { @Test void testMutualSuccess() throws Exception { configureServer(t -> { - t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); }); final HttpHost target = startServer(); @@ -388,7 +388,7 @@ void testMutualSuccess() throws Exception { @Test void testMutualFailureNoToken() throws Exception { configureServer(t -> { - t.register("*", new SPNEGOMutualService(false, null)); + t.register("*", new SpnegoService(false, null)); }); final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false); @@ -430,7 +430,7 @@ void testMutualFailureNoToken() throws Exception { @Test void testMutualFailureEstablishedStatusFalse() throws Exception { configureServer(t -> { - t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); }); final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(false, false); @@ -471,7 +471,7 @@ void testMutualFailureEstablishedStatusFalse() throws Exception { @Test void testMutualFailureMutualStatusFalse() throws Exception { configureServer(t -> { - t.register("*", new SPNEGOMutualService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + t.register("*", new SpnegoService(true, GOOD_MUTUAL_AUTH_TOKEN_B64_BYTES)); }); final MutualNegotiateSchemeWithMockGssManager mockAuthScheme = new MutualNegotiateSchemeWithMockGssManager(true, false); @@ -512,7 +512,7 @@ void testMutualFailureMutualStatusFalse() throws Exception { @Test void testMutualFailureBadToken() throws Exception { configureServer(t -> { - t.register("*", new SPNEGOMutualService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES)); + t.register("*", new SpnegoService(true, BAD_MUTUAL_AUTH_TOKEN_B64_BYTES)); }); // We except that the initSecContent throws an exception, so the status is irrelevant 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 ca94094e3b..3bd517a723 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 @@ -37,9 +37,10 @@ * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer * supported. - * Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS instead. - * @see org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme - * @see MutualKerberosConfig + * Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or Bearer + * authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme + * @see org.apache.hc.client5.http.auth.gss.GssConfig */ @Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) 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 c30384423b..ae2cd4e52d 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 @@ -39,10 +39,10 @@ * @since 4.4 * * - * Optionally used both by {@link org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme} + * Optionally used both by {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme} * and the old deprecated GGS based experimental authentication schemes. * - * @see org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme */ @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 a17e839803..ea3fff625a 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 @@ -67,7 +67,7 @@ private StandardAuthScheme() { /** * SPNEGO authentication scheme as defined in RFC 4559 and RFC 4178. * - * Use {@link org.apache.hc.client5.http.impl.auth.MutualSpnegoScheme} instead of the old + * Use {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme} instead of the old * deprecated {@link org.apache.hc.client5.http.impl.auth.SPNegoScheme} */ public static final String SPNEGO = "Negotiate"; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java similarity index 88% rename from httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java index 460a5ff3ad..ab67cd2960 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/MutualKerberosConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java @@ -25,13 +25,14 @@ * */ -package org.apache.hc.client5.http.auth; +package org.apache.hc.client5.http.auth.gss; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; /** - * Immutable class encapsulating Kerberos configuration options for MutualSpnegoScheme. + * Immutable class encapsulating Kerberos configuration options for the new mutual auth capable + * SpnegoScheme. * * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and * primitive booleans. @@ -43,10 +44,10 @@ * */ @Contract(threading = ThreadingBehavior.IMMUTABLE) -public class MutualKerberosConfig implements Cloneable { +public class GssConfig implements Cloneable { - public static final MutualKerberosConfig DEFAULT = new Builder().build(); + public static final GssConfig DEFAULT = new Builder().build(); private final boolean addPort; private final boolean useCanonicalHostname; @@ -56,11 +57,11 @@ public class MutualKerberosConfig implements Cloneable { /** * Intended for CDI compatibility */ - protected MutualKerberosConfig() { + protected GssConfig() { this(false, false, true, false); } - MutualKerberosConfig( + GssConfig( final boolean stripPort, final boolean useCanonicalHostname, final boolean requestMutualAuth, @@ -89,8 +90,8 @@ public boolean isRequestMutualAuth() { } @Override - protected MutualKerberosConfig clone() throws CloneNotSupportedException { - return (MutualKerberosConfig) super.clone(); + protected GssConfig clone() throws CloneNotSupportedException { + return (GssConfig) super.clone(); } @Override @@ -105,11 +106,11 @@ public String toString() { return builder.toString(); } - public static MutualKerberosConfig.Builder custom() { + public static GssConfig.Builder custom() { return new Builder(); } - public static MutualKerberosConfig.Builder copy(final MutualKerberosConfig config) { + public static GssConfig.Builder copy(final GssConfig config) { return new Builder() .setAddPort(config.isAddPort()) .setUseCanonicalHostname(config.isUseCanonicalHostname()) @@ -148,8 +149,8 @@ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { return this; } - public MutualKerberosConfig build() { - return new MutualKerberosConfig( + public GssConfig build() { + return new GssConfig( addPort, useCanonicalHostname, requestMutualAuth, 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 f5859c9c3d..b22269cc4e 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 @@ -61,9 +61,9 @@ * @since 4.2 * * @deprecated Do not use. The GGS based experimental authentication schemes are no longer - * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS - * instead. - * @see MutualSpnegoScheme + * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or + * Bearer authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme * @see BasicScheme * @see BearerScheme */ 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 b32eb9176f..deb8316179 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 @@ -42,9 +42,10 @@ * @since 4.2 * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer - * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS + * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or + * Bearer authentication with TLS * instead. - * @see MutualSpnegoScheme + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme * @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 74efdccf64..161f12629c 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 @@ -46,9 +46,9 @@ * @since 4.2 * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer - * supported. Use MutualSpnegoSchemeFactory, or consider using Basic or Bearer authentication - * with TLS instead. - * @see MutualSpnegoSchemeFactory + * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory, or consider using + * Basic or Bearer authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory * @see BasicSchemeFactory * @see BearerSchemeFactory */ 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 69cf0c86d4..2516588dc3 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 @@ -43,9 +43,9 @@ * @since 4.2 * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer - * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS - * instead. - * @see MutualSpnegoScheme + * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or + * Bearer authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme * @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 7050800e17..d45d9dfe1a 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 @@ -46,9 +46,9 @@ * @since 4.2 * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer - * supported. Use MutualSpnegoScheme, or consider using Basic or Bearer authentication with TLS - * instead. - * @see MutualSpnegoSchemeFactory + * supported. Use org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory, or consider using + * Basic or Bearer authentication with TLS instead. + * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoSchemeFactory * @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/gss/GssSchemeBase.java similarity index 95% rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualGssSchemeBase.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index 4241c068ec..57afd57117 100644 --- 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/gss/GssSchemeBase.java @@ -24,7 +24,7 @@ * . * */ -package org.apache.hc.client5.http.impl.auth; +package org.apache.hc.client5.http.impl.auth.gss; import java.net.UnknownHostException; import java.security.Principal; @@ -39,7 +39,7 @@ 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.MutualKerberosConfig; +import org.apache.hc.client5.http.auth.gss.GssConfig; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.core5.http.HttpHost; @@ -59,13 +59,14 @@ * Common behaviour for the new mutual authentication capable {@code GSS} based authentication * schemes. * - * This class is derived from the old {@link GGSSchemeBase} class, which was deprecated in 5.3. + * This class is derived from the old {@link org.apache.hc.client5.http.impl.auth.GGSSchemeBase} + * class, which was deprecated in 5.3. * * @since 5.5 * * @see GGSSchemeBase */ -public abstract class MutualGssSchemeBase implements AuthScheme { +public abstract class GssSchemeBase implements AuthScheme { enum State { UNINITIATED, @@ -75,14 +76,14 @@ enum State { FAILED, } - private static final Logger LOG = LoggerFactory.getLogger(MutualGssSchemeBase.class); + private static final Logger LOG = LoggerFactory.getLogger(GssSchemeBase.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 MutualKerberosConfig config; + private final GssConfig config; private final DnsResolver dnsResolver; private final boolean mutualAuth; private int challengesLeft = MAX_GSS_CHALLENGES; @@ -94,20 +95,20 @@ enum State { private String challenge; private byte[] queuedToken = new byte[0]; - MutualGssSchemeBase(final MutualKerberosConfig config, final DnsResolver dnsResolver) { + GssSchemeBase(final GssConfig config, final DnsResolver dnsResolver) { super(); - this.config = config != null ? config : MutualKerberosConfig.DEFAULT; + this.config = config != null ? config : GssConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; this.mutualAuth = config.isRequestMutualAuth(); this.state = State.UNINITIATED; } - MutualGssSchemeBase(final MutualKerberosConfig config) { + GssSchemeBase(final GssConfig config) { this(config, SystemDefaultDnsResolver.INSTANCE); } - MutualGssSchemeBase() { - this(MutualKerberosConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + GssSchemeBase() { + this(GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); } @Override 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/gss/SpnegoScheme.java similarity index 84% rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoScheme.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java index 4702714c8f..1dd5b951e8 100644 --- 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/gss/SpnegoScheme.java @@ -24,11 +24,12 @@ * . * */ -package org.apache.hc.client5.http.impl.auth; +package org.apache.hc.client5.http.impl.auth.gss; 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.client5.http.impl.auth.SPNegoScheme; import org.ietf.jgss.GSSException; import org.ietf.jgss.Oid; @@ -62,35 +63,35 @@ * } * } * - * AuthenticationStrategy mutualStrategy = new SpnegoAuthenticationStrategy(); + * AuthenticationStrategy spnegoStrategy = new SpnegoAuthenticationStrategy(); * - * AuthSchemeFactory mutualFactory = new MutualSpnegoSchemeFactory(); + * AuthSchemeFactory spnegoFactory = new SpnegoSchemeFactory(); * Registry mutualSchemeRegistry = RegistryBuilder.create() - * .register(StandardAuthScheme.SPNEGO, mutualFactory) + * .register(StandardAuthScheme.SPNEGO, spnegoFactory) * //register other schemes as needed * .build(); * * CloseableHttpClient mutualClient = HttpClientBuilder.create() - * .setTargetAuthenticationStrategy(mutualStrategy); - * .setDefaultAuthSchemeRegistry(mutualSchemeRegistry); + * .setTargetAuthenticationStrategy(spnegoStrategy); + * .setDefaultAuthSchemeRegistry(spnegoSchemeRegistry); * .build(); * } * * * @since 5.5 */ -public class MutualSpnegoScheme extends MutualGssSchemeBase { +public class SpnegoScheme extends GssSchemeBase { 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.MutualKerberosConfig config, final DnsResolver dnsResolver) { + public SpnegoScheme(final org.apache.hc.client5.http.auth.gss.GssConfig config, final DnsResolver dnsResolver) { super(config, dnsResolver); } - public MutualSpnegoScheme() { + public SpnegoScheme() { super(); } 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/gss/SpnegoSchemeFactory.java similarity index 76% rename from httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/MutualSpnegoSchemeFactory.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java index 4711ffee49..b0b96814f1 100644 --- 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/gss/SpnegoSchemeFactory.java @@ -24,7 +24,7 @@ * . * */ -package org.apache.hc.client5.http.impl.auth; +package org.apache.hc.client5.http.impl.auth.gss; import org.apache.hc.client5.http.DnsResolver; import org.apache.hc.client5.http.SystemDefaultDnsResolver; @@ -37,9 +37,9 @@ /** * {@link AuthSchemeFactory} implementation that creates and initialises - * {@link MutualSpnegoScheme} instances. + * {@link SpnegoScheme} instances. *

- * This replaces the old deprecated {@link SPNegoSchemeFactory} + * This replaces the old deprecated {@link org.apache.hc.client5.http.impl.auth.SPNegoSchemeFactory} *

* * @since 5.5 @@ -48,21 +48,21 @@ */ @Contract(threading = ThreadingBehavior.STATELESS) @Experimental -public class MutualSpnegoSchemeFactory implements AuthSchemeFactory { +public class SpnegoSchemeFactory implements AuthSchemeFactory { /** * Singleton instance for the default configuration. */ - public static final MutualSpnegoSchemeFactory DEFAULT = new MutualSpnegoSchemeFactory(org.apache.hc.client5.http.auth.MutualKerberosConfig.DEFAULT, + public static final SpnegoSchemeFactory DEFAULT = new SpnegoSchemeFactory(org.apache.hc.client5.http.auth.gss.GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); - private final org.apache.hc.client5.http.auth.MutualKerberosConfig config; + private final org.apache.hc.client5.http.auth.gss.GssConfig config; private final DnsResolver dnsResolver; /** * @since 5.5 */ - public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.MutualKerberosConfig config, final DnsResolver dnsResolver) { + public SpnegoSchemeFactory(final org.apache.hc.client5.http.auth.gss.GssConfig config, final DnsResolver dnsResolver) { super(); this.config = config; this.dnsResolver = dnsResolver; @@ -70,7 +70,7 @@ public MutualSpnegoSchemeFactory(final org.apache.hc.client5.http.auth.MutualKer @Override public AuthScheme create(final HttpContext context) { - return new MutualSpnegoScheme(this.config, this.dnsResolver); + return new SpnegoScheme(this.config, this.dnsResolver); } } From bbdb403fb745ae65530c5b70faa8de099c1a18ea Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Thu, 27 Feb 2025 15:22:55 +0100 Subject: [PATCH 06/10] renames KerberosCredentials --- .../HttpAsyncClientCompatibilityTest.java | 6 +- .../compatibility/spnego/SpnegoTestUtil.java | 10 +-- .../sync/HttpClientCompatibilityTest.java | 6 +- .../http/auth/KerberosCredentials.java | 45 ++++------- .../client5/http/auth/gss/GssCredentials.java | 74 +++++++++++++++++++ .../client5/http/impl/auth/GGSSchemeBase.java | 4 +- .../http/impl/auth/gss/GssSchemeBase.java | 4 +- 7 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java index 1ca7d003a4..60ec55cd7d 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java @@ -37,8 +37,8 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; -import org.apache.hc.client5.http.auth.KerberosCredentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.auth.gss.GssCredentials; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -83,7 +83,7 @@ public HttpAsyncClientCompatibilityTest( this.clientResource = new HttpAsyncClientResource(versionPolicy); if (targetCreds != null) { //this.setCredentials(new AuthScope(target), targetCreds); - if (targetCreds instanceof KerberosCredentials) { + if (targetCreds instanceof GssCredentials) { secretPath = "/private_spnego/big-secret.txt"; this.clientResource.configure(builder -> builder .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) @@ -94,7 +94,7 @@ public HttpAsyncClientCompatibilityTest( this.clientResource.configure(builder -> builder.setProxy(proxy)); if (proxyCreds != null) { this.setCredentials(new AuthScope(proxy), proxyCreds); - if (proxyCreds instanceof KerberosCredentials) { + if (proxyCreds instanceof GssCredentials) { // We disable Mutual Auth, because Squid does not support it. // There is no way to set separate scheme registry for target/proxy, // but that's not a problem as SPNEGO cannot be proxied anyway. diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java index 707572ee9d..300da27d9a 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java @@ -48,9 +48,9 @@ import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthSchemeFactory; -import org.apache.hc.client5.http.auth.KerberosCredentials; import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.auth.gss.GssConfig; +import org.apache.hc.client5.http.auth.gss.GssCredentials; import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory; import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory; @@ -70,11 +70,11 @@ public class SpnegoTestUtil { static private final SpnegoSchemeFactory NO_MUTUAL_SCHEME_FACTORY = new SpnegoSchemeFactory(NO_MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); - public static KerberosCredentials createCredentials(final Subject subject) { - return SecurityUtils.callAs(subject, new Callable() { + public static GssCredentials createCredentials(final Subject subject) { + return SecurityUtils.callAs(subject, new Callable() { @Override - public KerberosCredentials call() throws Exception { - return new KerberosCredentials( + public GssCredentials call() throws Exception { + return new GssCredentials( GSSManager.getInstance().createCredential(GSSCredential.INITIATE_ONLY)); } }); diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java index 306ff10107..fb2b3d8b21 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java @@ -31,8 +31,8 @@ import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsStore; -import org.apache.hc.client5.http.auth.KerberosCredentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.auth.gss.GssCredentials; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpOptions; import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; @@ -68,7 +68,7 @@ public HttpClientCompatibilityTest(final HttpHost target, final Credentials targ this.clientResource = new HttpClientResource(); if (targetCreds != null) { //this.setCredentials(new AuthScope(target), targetCreds); - if (targetCreds instanceof KerberosCredentials) { + if (targetCreds instanceof GssCredentials) { secretPath = "/private_spnego/big-secret.txt"; this.clientResource.configure(builder -> builder .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) @@ -79,7 +79,7 @@ public HttpClientCompatibilityTest(final HttpHost target, final Credentials targ this.clientResource.configure(builder -> builder.setProxy(proxy)); if (proxyCreds != null) { this.setCredentials(new AuthScope(proxy), proxyCreds); - if (proxyCreds instanceof KerberosCredentials) { + if (proxyCreds instanceof GssCredentials) { // We disable Mutual Auth, because Squid does not support it. // There is no way to set separate scheme registry for target/proxy, // but that's not a problem as SPNEGO cannot be proxied anyway. 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 ae2cd4e52d..35db43f9c3 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 @@ -26,9 +26,7 @@ */ package org.apache.hc.client5.http.auth; -import java.io.Serializable; -import java.security.Principal; - +import org.apache.hc.client5.http.auth.gss.GssCredentials; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.ietf.jgss.GSSCredential; @@ -38,41 +36,24 @@ * * @since 4.4 * + * The original KerberosCredentials class has been renamed to + * org.apache.hc.client5.http.auth.gss.GssCredentials for the new Mutual capable SPNEGO Scheme. + * This is an identical child class with the original name to maintain backwards compatibility. * - * Optionally used both by {@link org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme} - * and the old deprecated GGS based experimental authentication schemes. - * + * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer + * supported. + * Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or Bearer + * authentication with TLS instead. * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme + * @see org.apache.hc.client5.http.auth.gss.GssConfig + * @see org.apache.hc.client5.http.auth.gss.GssCredentials */ +@Deprecated @Contract(threading = ThreadingBehavior.IMMUTABLE) -public class KerberosCredentials implements Credentials, Serializable { - - private static final long serialVersionUID = 487421613855550713L; +public class KerberosCredentials extends GssCredentials { - /** GSSCredential */ - private final GSSCredential gssCredential; - - /** - * Constructor with GSSCredential argument - * - * @param gssCredential - */ public KerberosCredentials(final GSSCredential gssCredential) { - this.gssCredential = gssCredential; - } - - public GSSCredential getGSSCredential() { - return gssCredential; - } - - @Override - public Principal getUserPrincipal() { - return null; - } - - @Override - public char[] getPassword() { - return null; + super(gssCredential); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java new file mode 100644 index 0000000000..dc48e4e672 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java @@ -0,0 +1,74 @@ +/* + * ==================================================================== + * 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.gss; + +import java.io.Serializable; +import java.security.Principal; + +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.ietf.jgss.GSSCredential; + +/** + * Kerberos specific {@link Credentials} representation based on {@link GSSCredential}. + * + * @since 5.5 + * + */ +@Contract(threading = ThreadingBehavior.IMMUTABLE) +public class GssCredentials implements Credentials, Serializable { + + private static final long serialVersionUID = 487421613855550713L; + + /** GSSCredential */ + private final GSSCredential gssCredential; + + /** + * Constructor with GSSCredential argument + * + * @param gssCredential + */ + public GssCredentials(final GSSCredential gssCredential) { + this.gssCredential = gssCredential; + } + + public GSSCredential getGSSCredential() { + return gssCredential; + } + + @Override + public Principal getUserPrincipal() { + return null; + } + + @Override + public char[] getPassword() { + return null; + } + +} 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 b22269cc4e..700a961ffa 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 @@ -186,8 +186,8 @@ public boolean isResponseReady( 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(); + if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) { + this.gssCredential = ((org.apache.hc.client5.http.auth.gss.GssCredentials) credentials).getGSSCredential(); } else { this.gssCredential = null; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index 57afd57117..cba91ff93e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java @@ -313,9 +313,9 @@ protected void setGssCredential(final CredentialsProvider credentialsProvider, } final Credentials credentials = credentialsProvider.getCredentials(new AuthScope(host, null, getName()), context); - if (credentials instanceof org.apache.hc.client5.http.auth.KerberosCredentials) { + if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) { this.gssCredential = - ((org.apache.hc.client5.http.auth.KerberosCredentials) credentials) + ((org.apache.hc.client5.http.auth.gss.GssCredentials) credentials) .getGSSCredential(); } else { this.gssCredential = null; From dd87c1a7fb805fb0df64147c843bb5d17e32af93 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Thu, 27 Feb 2025 18:23:33 +0100 Subject: [PATCH 07/10] fixing the samller issues based on the review --- .../apache/hc/client5/http/auth/KerberosConfig.java | 2 -- .../hc/client5/http/auth/KerberosCredentials.java | 3 +-- .../apache/hc/client5/http/auth/gss/GssConfig.java | 8 ++++---- .../hc/client5/http/impl/auth/gss/GssSchemeBase.java | 2 +- .../hc/client5/http/impl/auth/gss/SpnegoScheme.java | 12 ++++++++++-- 5 files changed, 16 insertions(+), 11 deletions(-) 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 3bd517a723..6b405bbe5d 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 @@ -37,8 +37,6 @@ * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer * supported. - * Use org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme, or consider using Basic or Bearer - * authentication with TLS instead. * @see org.apache.hc.client5.http.impl.auth.gss.SpnegoScheme * @see org.apache.hc.client5.http.auth.gss.GssConfig */ 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 35db43f9c3..beb5fe7ed1 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,8 +37,7 @@ * @since 4.4 * * The original KerberosCredentials class has been renamed to - * org.apache.hc.client5.http.auth.gss.GssCredentials for the new Mutual capable SPNEGO Scheme. - * This is an identical child class with the original name to maintain backwards compatibility. + * org.apache.hc.client5.http.auth.gss.GssCredentials. * * @deprecated Do not use. The old GGS based experimental authentication schemes are no longer * supported. diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java index ab67cd2960..8f17d0feca 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java @@ -31,13 +31,13 @@ import org.apache.hc.core5.annotation.ThreadingBehavior; /** - * Immutable class encapsulating Kerberos configuration options for the new mutual auth capable + * Immutable class encapsulating GSS configuration options for the new mutual auth capable * SpnegoScheme. * * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and * primitive booleans. * - * Compared to {@link KerberosConfig} stripPort has been changed toAddPort, and the default is now + * Compared to {@link KerberosConfig} stripPort has been changed to addPort, and the default is now * false (same effect). The default for useCanonicalHostname has been changed to false from true. * * @since 5.5 @@ -62,12 +62,12 @@ protected GssConfig() { } GssConfig( - final boolean stripPort, + final boolean addPort, final boolean useCanonicalHostname, final boolean requestMutualAuth, final boolean requestDelegCreds) { super(); - this.addPort = stripPort; + this.addPort = addPort; this.useCanonicalHostname = useCanonicalHostname; this.requestMutualAuth = requestMutualAuth; this.requestDelegCreds = requestDelegCreds; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index cba91ff93e..1a005d760e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java @@ -171,7 +171,7 @@ public void processChallenge( queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); switch (state) { case UNINITIATED: - if (challenge != NO_TOKEN) { + if (challengeToken != null) { if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java index 1dd5b951e8..1b5ff557c5 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java @@ -82,7 +82,15 @@ */ public class SpnegoScheme extends GssSchemeBase { - private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + private static final String SPNEGO_OID_STRING = "1.3.6.1.5.5.2"; + private static final Oid SPNEGO_OID; + static { + try { + SPNEGO_OID = new Oid(SPNEGO_OID_STRING); + } catch (final GSSException e) { + throw new IllegalStateException("Failed to create OID for SPNEGO mechanism", e); + } + } /** * @since 5.0 @@ -102,7 +110,7 @@ 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); + return generateGSSToken(input, SPNEGO_OID, gssServiceName, gssHostname); } @Override From 41c5203f91e5b94468e01b9205c9f3d3643ec621 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Fri, 28 Feb 2025 10:24:16 +0100 Subject: [PATCH 08/10] rewrite GSS established logic, and add new option for compatibility for non-compliant servers --- .../HttpAsyncClientCompatibilityTest.java | 4 +- .../compatibility/spnego/SpnegoTestUtil.java | 13 +--- .../sync/HttpClientCompatibilityTest.java | 4 +- .../testing/sync/TestSpnegoScheme.java | 2 +- .../hc/client5/http/auth/gss/GssConfig.java | 30 ++++++-- .../http/impl/auth/AuthenticationHandler.java | 3 +- .../http/impl/auth/gss/GssSchemeBase.java | 69 ++++++++++++------- .../impl/auth/gss/SpnegoSchemeFactory.java | 3 + 8 files changed, 82 insertions(+), 46 deletions(-) diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java index 60ec55cd7d..49ff8a45f2 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/async/HttpAsyncClientCompatibilityTest.java @@ -87,7 +87,7 @@ public HttpAsyncClientCompatibilityTest( secretPath = "/private_spnego/big-secret.txt"; this.clientResource.configure(builder -> builder .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) - .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getDefaultSpnegoSchemeRegistry())); } } if (proxy != null) { @@ -100,7 +100,7 @@ public HttpAsyncClientCompatibilityTest( // but that's not a problem as SPNEGO cannot be proxied anyway. this.clientResource.configure(builder -> builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy()) - .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistryNoMutual())); + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getLegacySpnegoSchemeRegistry())); } } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java index 300da27d9a..492a9f10f0 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/spnego/SpnegoTestUtil.java @@ -46,10 +46,8 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; -import org.apache.hc.client5.http.SystemDefaultDnsResolver; import org.apache.hc.client5.http.auth.AuthSchemeFactory; import org.apache.hc.client5.http.auth.StandardAuthScheme; -import org.apache.hc.client5.http.auth.gss.GssConfig; import org.apache.hc.client5.http.auth.gss.GssCredentials; import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory; import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory; @@ -65,11 +63,6 @@ public class SpnegoTestUtil { - static private final GssConfig NO_MUTUAL_KERBEROS_CONFIG = - GssConfig.custom().setRequestMutualAuth(false).build(); - static private final SpnegoSchemeFactory NO_MUTUAL_SCHEME_FACTORY = - new SpnegoSchemeFactory(NO_MUTUAL_KERBEROS_CONFIG, SystemDefaultDnsResolver.INSTANCE); - public static GssCredentials createCredentials(final Subject subject) { return SecurityUtils.callAs(subject, new Callable() { @Override @@ -89,7 +82,7 @@ public static Path createKeytabDir() { } } - public static Registry getSpnegoSchemeRegistry() { + public static Registry getDefaultSpnegoSchemeRegistry() { return RegistryBuilder.create() .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE) .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) @@ -100,12 +93,12 @@ public static Registry getSpnegoSchemeRegistry() { } //Squid does not support mutual auth - public static Registry getSpnegoSchemeRegistryNoMutual() { + public static Registry getLegacySpnegoSchemeRegistry() { return RegistryBuilder.create() .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE) .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE) .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) - .register(StandardAuthScheme.SPNEGO, NO_MUTUAL_SCHEME_FACTORY) + .register(StandardAuthScheme.SPNEGO, SpnegoSchemeFactory.LEGACY) // register other schemes as needed .build(); } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java index fb2b3d8b21..c26cfae920 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/compatibility/sync/HttpClientCompatibilityTest.java @@ -72,7 +72,7 @@ public HttpClientCompatibilityTest(final HttpHost target, final Credentials targ secretPath = "/private_spnego/big-secret.txt"; this.clientResource.configure(builder -> builder .setTargetAuthenticationStrategy(new SpnegoAuthenticationStrategy()) - .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistry())); + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getDefaultSpnegoSchemeRegistry())); } } if (proxy != null) { @@ -85,7 +85,7 @@ public HttpClientCompatibilityTest(final HttpHost target, final Credentials targ // but that's not a problem as SPNEGO cannot be proxied anyway. this.clientResource.configure(builder -> builder.setProxyAuthenticationStrategy(new SpnegoAuthenticationStrategy()) - .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getSpnegoSchemeRegistryNoMutual())); + .setDefaultAuthSchemeRegistry(SpnegoTestUtil.getLegacySpnegoSchemeRegistry())); } } } 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 b4e973fc58..905b2b34e2 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 @@ -420,7 +420,7 @@ void testMutualFailureNoToken() throws Exception { Assertions.assertTrue(e.getCause() instanceof AuthenticationException); } - Mockito.verify(mockAuthScheme.context, Mockito.atLeastOnce()).isEstablished(); + 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/gss/GssConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java index 8f17d0feca..a96e8a3b9e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java @@ -48,29 +48,34 @@ public class GssConfig implements Cloneable { public static final GssConfig DEFAULT = new Builder().build(); + public static final GssConfig LEGACY = + new Builder().setIgnoreMissingToken(true).setRequestMutualAuth(false).build(); private final boolean addPort; private final boolean useCanonicalHostname; private final boolean requestMutualAuth; private final boolean requestDelegCreds; + private final boolean ignoreMissingToken; /** * Intended for CDI compatibility */ protected GssConfig() { - this(false, false, true, false); + this(false, false, true, false, false); } GssConfig( final boolean addPort, final boolean useCanonicalHostname, final boolean requestMutualAuth, - final boolean requestDelegCreds) { + final boolean requestDelegCreds, + final boolean ignoreMissingToken) { super(); this.addPort = addPort; this.useCanonicalHostname = useCanonicalHostname; this.requestMutualAuth = requestMutualAuth; this.requestDelegCreds = requestDelegCreds; + this.ignoreMissingToken = ignoreMissingToken; } public boolean isAddPort() { @@ -89,6 +94,10 @@ public boolean isRequestMutualAuth() { return requestMutualAuth; } + public boolean isIgnoreMissingToken() { + return ignoreMissingToken; + } + @Override protected GssConfig clone() throws CloneNotSupportedException { return (GssConfig) super.clone(); @@ -102,6 +111,7 @@ public String toString() { builder.append(", useCanonicalHostname=").append(useCanonicalHostname); builder.append(", requestDelegCreds=").append(requestDelegCreds); builder.append(", requestMutualAuth=").append(requestMutualAuth); + builder.append(", ignoreMissingToken=").append(ignoreMissingToken); builder.append("]"); return builder.toString(); } @@ -115,7 +125,8 @@ public static GssConfig.Builder copy(final GssConfig config) { .setAddPort(config.isAddPort()) .setUseCanonicalHostname(config.isUseCanonicalHostname()) .setRequestDelegCreds(config.isRequestDelegCreds()) - .setRequestMutualAuth(config.isRequestMutualAuth()); + .setRequestMutualAuth(config.isRequestMutualAuth()) + .setIgnoreMissingToken(config.isIgnoreMissingToken()); } public static class Builder { @@ -124,6 +135,8 @@ public static class Builder { private boolean useCanonicalHostname = false; private boolean requestMutualAuth = true; private boolean requestDelegCreds = false; + private boolean ignoreMissingToken = false; + Builder() { super(); @@ -149,12 +162,21 @@ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { return this; } + public Builder setIgnoreMissingToken(final boolean ignoreMissingToken) { + this.ignoreMissingToken = ignoreMissingToken; + return this; + } + public GssConfig build() { + if (requestMutualAuth && ignoreMissingToken) { + throw new IllegalArgumentException("If requestMutualAuth is set then ignoreMissingToken must not be set"); + } return new GssConfig( addPort, useCanonicalHostname, requestMutualAuth, - requestDelegCreds + requestDelegCreds, + ignoreMissingToken ); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java index 8d38ec8e75..55f72f659f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java @@ -121,8 +121,7 @@ public boolean isChallenged( } /** - * Determines whether the given response represents an authentication challenge, without - * changing the {@link AuthExchange} state. + * Determines whether the response is 401/407 response depending to the challengeType * * @param challengeType the challenge type (target or proxy). * @param response the response message head. diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index 1a005d760e..7d2eceecf8 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java @@ -77,7 +77,6 @@ enum State { } private static final Logger LOG = LoggerFactory.getLogger(GssSchemeBase.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. @@ -86,6 +85,7 @@ enum State { private final GssConfig config; private final DnsResolver dnsResolver; private final boolean mutualAuth; + private final boolean ignoreMissingToken; private int challengesLeft = MAX_GSS_CHALLENGES; /** Authentication process state */ @@ -100,6 +100,7 @@ enum State { this.config = config != null ? config : GssConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; this.mutualAuth = config.isRequestMutualAuth(); + this.ignoreMissingToken = config.isIgnoreMissingToken(); this.state = State.UNINITIATED; } @@ -167,10 +168,10 @@ public void processChallenge( LOG.debug("{} GSS init {}", exchangeId, gssHostname); } try { - setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context); - queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); switch (state) { case UNINITIATED: + setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context); + queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); if (challengeToken != null) { if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); @@ -183,35 +184,54 @@ public void processChallenge( 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 (challengeToken == null) { + if (!challenged && ignoreMissingToken) { + // Got a 200 without a challenge. Old non RFC compliant server. if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = - HttpClientContext.cast(context); + final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSSContext is not established ", exchangeId); + LOG.debug("{} GSS Context is not established, but continuing because GssConfig.ignoreMissingToken is true.", 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()) { + state = State.SUCCEEDED; + break; + } else { if (LOG.isDebugEnabled()) { - final HttpClientContext clientContext = - HttpClientContext.cast(context); + final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} requireMutualAuth is set but GSSAUthContext does not have" - + " mutualAuthState set", exchangeId); + LOG.debug("{} Did not receive required challenge and GssConfig.ignoreMissingToken is false.", + exchangeId); } state = State.FAILED; throw new AuthenticationException( - "requireMutualAuth is set but GSSContext mutualAuthState is not set"); - } else { - state = State.SUCCEEDED; + "Did not receive required challenge and GssConfig.ignoreMissingToken is false."); + } + } + queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); + if (challenged) { + state = State.TOKEN_READY; + } else 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 (mutualAuth && !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: @@ -289,7 +309,7 @@ public boolean isChallengeComplete() { @Override public boolean isChallengeExpected() { - return state == State.TOKEN_SENT && mutualAuth; + return state == State.TOKEN_SENT; } @Override @@ -301,7 +321,6 @@ public boolean isResponseReady( Args.notNull(host, "Auth host"); Args.notNull(credentialsProvider, "CredentialsProvider"); - setGssCredential(credentialsProvider, host, context); return true; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java index b0b96814f1..1247d11afc 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoSchemeFactory.java @@ -56,6 +56,9 @@ public class SpnegoSchemeFactory implements AuthSchemeFactory { public static final SpnegoSchemeFactory DEFAULT = new SpnegoSchemeFactory(org.apache.hc.client5.http.auth.gss.GssConfig.DEFAULT, SystemDefaultDnsResolver.INSTANCE); + public static final SpnegoSchemeFactory LEGACY = new SpnegoSchemeFactory(org.apache.hc.client5.http.auth.gss.GssConfig.LEGACY, + SystemDefaultDnsResolver.INSTANCE); + private final org.apache.hc.client5.http.auth.gss.GssConfig config; private final DnsResolver dnsResolver; From 7318bf3e543aeaefd984cc68315d15a662e14973 Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Fri, 28 Feb 2025 14:39:09 +0100 Subject: [PATCH 09/10] Add separate flags for requesting and checking mutual auth status. Update some debug/exception messages --- .../hc/client5/http/auth/gss/GssConfig.java | 27 ++++++++++-- .../http/impl/auth/gss/GssSchemeBase.java | 41 +++++++++++-------- 2 files changed, 48 insertions(+), 20 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java index a96e8a3b9e..04d5bba08d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java @@ -49,11 +49,12 @@ public class GssConfig implements Cloneable { public static final GssConfig DEFAULT = new Builder().build(); public static final GssConfig LEGACY = - new Builder().setIgnoreMissingToken(true).setRequestMutualAuth(false).build(); + new Builder().setIgnoreMissingToken(true).setRequireMutualAuth(false).build(); private final boolean addPort; private final boolean useCanonicalHostname; private final boolean requestMutualAuth; + private final boolean requireMutualAuth; private final boolean requestDelegCreds; private final boolean ignoreMissingToken; @@ -61,19 +62,21 @@ public class GssConfig implements Cloneable { * Intended for CDI compatibility */ protected GssConfig() { - this(false, false, true, false, false); + this(false, false, true, true, false, false); } GssConfig( final boolean addPort, final boolean useCanonicalHostname, final boolean requestMutualAuth, + final boolean requireMutualAuth, final boolean requestDelegCreds, final boolean ignoreMissingToken) { super(); this.addPort = addPort; this.useCanonicalHostname = useCanonicalHostname; this.requestMutualAuth = requestMutualAuth; + this.requireMutualAuth = requireMutualAuth; this.requestDelegCreds = requestDelegCreds; this.ignoreMissingToken = ignoreMissingToken; } @@ -94,6 +97,10 @@ public boolean isRequestMutualAuth() { return requestMutualAuth; } + public boolean isRequireMutualAuth() { + return requireMutualAuth; + } + public boolean isIgnoreMissingToken() { return ignoreMissingToken; } @@ -111,6 +118,7 @@ public String toString() { builder.append(", useCanonicalHostname=").append(useCanonicalHostname); builder.append(", requestDelegCreds=").append(requestDelegCreds); builder.append(", requestMutualAuth=").append(requestMutualAuth); + builder.append(", requireMutualAuth=").append(requireMutualAuth); builder.append(", ignoreMissingToken=").append(ignoreMissingToken); builder.append("]"); return builder.toString(); @@ -125,6 +133,7 @@ public static GssConfig.Builder copy(final GssConfig config) { .setAddPort(config.isAddPort()) .setUseCanonicalHostname(config.isUseCanonicalHostname()) .setRequestDelegCreds(config.isRequestDelegCreds()) + .setRequireMutualAuth(config.isRequireMutualAuth()) .setRequestMutualAuth(config.isRequestMutualAuth()) .setIgnoreMissingToken(config.isIgnoreMissingToken()); } @@ -134,6 +143,7 @@ public static class Builder { private boolean addPort = false; private boolean useCanonicalHostname = false; private boolean requestMutualAuth = true; + private boolean requireMutualAuth = true; private boolean requestDelegCreds = false; private boolean ignoreMissingToken = false; @@ -157,6 +167,11 @@ public Builder setRequestMutualAuth(final boolean requestMutualAuth) { return this; } + public Builder setRequireMutualAuth(final boolean requireMutualAuth) { + this.requireMutualAuth = requireMutualAuth; + return this; + } + public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { this.requestDelegCreds = requuestDelegCreds; return this; @@ -168,13 +183,17 @@ public Builder setIgnoreMissingToken(final boolean ignoreMissingToken) { } public GssConfig build() { - if (requestMutualAuth && ignoreMissingToken) { - throw new IllegalArgumentException("If requestMutualAuth is set then ignoreMissingToken must not be set"); + if (requireMutualAuth && ignoreMissingToken) { + throw new IllegalArgumentException("If requireMutualAuth is set then ignoreMissingToken must not be set"); + } + if (requireMutualAuth && !requestMutualAuth) { + throw new IllegalArgumentException("If requireMutualAuth is set then requestMutualAuth must also be set"); } return new GssConfig( addPort, useCanonicalHostname, requestMutualAuth, + requireMutualAuth, requestDelegCreds, ignoreMissingToken ); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index 7d2eceecf8..0a3450bf43 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java @@ -84,7 +84,7 @@ enum State { private static final int MAX_GSS_CHALLENGES = 3; private final GssConfig config; private final DnsResolver dnsResolver; - private final boolean mutualAuth; + private final boolean requireMutualAuth; private final boolean ignoreMissingToken; private int challengesLeft = MAX_GSS_CHALLENGES; @@ -99,7 +99,7 @@ enum State { super(); this.config = config != null ? config : GssConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; - this.mutualAuth = config.isRequestMutualAuth(); + this.requireMutualAuth = config.isRequireMutualAuth(); this.ignoreMissingToken = config.isIgnoreMissingToken(); this.state = State.UNINITIATED; } @@ -186,7 +186,7 @@ public void processChallenge( case TOKEN_SENT: if (challengeToken == null) { if (!challenged && ignoreMissingToken) { - // Got a 200 without a challenge. Old non RFC compliant server. + // Got a Non 401/407 code without a challenge. Old non RFC compliant server. if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); @@ -213,23 +213,32 @@ public void processChallenge( if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSSContext is not established ", exchangeId); + 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 (mutualAuth && !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); + "GSSContext is not established."); + } else if (!gssContext.getMutualAuthState()) { + if (requireMutualAuth) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} requireMutualAuth is true but GSSContext mutualAuthState is false", + exchangeId); + } + state = State.FAILED; + throw new AuthenticationException( + "requireMutualAuth is true but GSSContext mutualAuthState is false"); + } else { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} GSSContext MutualAuthState is false, but continuing because GssConfig.requireMutualAuth is false.", + exchangeId); + } + state = State.FAILED; } - state = State.FAILED; - throw new AuthenticationException( - "requireMutualAuth is set but GSSContext mutualAuthState is not set"); } else { state = State.SUCCEEDED; } @@ -289,7 +298,7 @@ protected GSSContext createGSSContext( final GSSCredential gssCredential) throws GSSException { final GSSContext gssContext = manager.createContext(peerName.canonicalize(oid), oid, gssCredential, GSSContext.DEFAULT_LIFETIME); - gssContext.requestMutualAuth(mutualAuth); + gssContext.requestMutualAuth(config.isRequestMutualAuth()); gssContext.requestCredDeleg(config.isRequestDelegCreds()); return gssContext; } From 0bf4eca441399c9e42256776f1a27961baba25fa Mon Sep 17 00:00:00 2001 From: Istvan Toth Date: Sat, 8 Mar 2025 09:51:41 +0100 Subject: [PATCH 10/10] apply discussed fixes set isConnectionBased() to false fix logic in needsAuthentication() --- .../testing/sync/TestSpnegoScheme.java | 47 +++---------- .../hc/client5/http/auth/gss/GssConfig.java | 30 ++++---- .../client5/http/auth/gss/GssCredentials.java | 1 + .../http/impl/async/AsyncProtocolExec.java | 11 ++- .../http/impl/auth/AuthenticationHandler.java | 2 +- .../http/impl/auth/gss/GssSchemeBase.java | 68 ++++++++++++------- .../http/impl/auth/gss/SpnegoScheme.java | 6 +- .../http/impl/classic/ProtocolExec.java | 11 ++- 8 files changed, 85 insertions(+), 91 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 905b2b34e2..4473881af4 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 @@ -291,6 +291,7 @@ public AuthScheme create(final HttpContext context) { * Tests that the client will stop connecting to the server if * the server still keep asking for a valid ticket. */ + //@Disabled @Test void testDontTryToAuthenticateEndlessly() throws Exception { configureServer(t -> { @@ -310,42 +311,16 @@ void testDontTryToAuthenticateEndlessly() throws Exception { 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; - }); - + try { + client().execute(target, httpget, response -> { + EntityUtils.consume(response.getEntity()); + Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode()); + return null; + }); + Assertions.fail(); + } catch (final IllegalStateException e) { + // Expected + } } /** diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java index 04d5bba08d..9850a59ee2 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java @@ -32,7 +32,7 @@ /** * Immutable class encapsulating GSS configuration options for the new mutual auth capable - * SpnegoScheme. + * for the new {@link SpnegoScheme}. * * Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and * primitive booleans. @@ -49,14 +49,14 @@ public class GssConfig implements Cloneable { public static final GssConfig DEFAULT = new Builder().build(); public static final GssConfig LEGACY = - new Builder().setIgnoreMissingToken(true).setRequireMutualAuth(false).build(); + new Builder().setIgnoreIncompleteSecurityContext(true).setRequireMutualAuth(false).build(); private final boolean addPort; private final boolean useCanonicalHostname; private final boolean requestMutualAuth; private final boolean requireMutualAuth; private final boolean requestDelegCreds; - private final boolean ignoreMissingToken; + private final boolean ignoreIncompleteSecurityContext; /** * Intended for CDI compatibility @@ -71,14 +71,14 @@ protected GssConfig() { final boolean requestMutualAuth, final boolean requireMutualAuth, final boolean requestDelegCreds, - final boolean ignoreMissingToken) { + final boolean ignoreIncompleteSecurityContext) { super(); this.addPort = addPort; this.useCanonicalHostname = useCanonicalHostname; this.requestMutualAuth = requestMutualAuth; this.requireMutualAuth = requireMutualAuth; this.requestDelegCreds = requestDelegCreds; - this.ignoreMissingToken = ignoreMissingToken; + this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext; } public boolean isAddPort() { @@ -101,8 +101,8 @@ public boolean isRequireMutualAuth() { return requireMutualAuth; } - public boolean isIgnoreMissingToken() { - return ignoreMissingToken; + public boolean isIgnoreIncompleteSecurityContext() { + return ignoreIncompleteSecurityContext; } @Override @@ -119,7 +119,7 @@ public String toString() { builder.append(", requestDelegCreds=").append(requestDelegCreds); builder.append(", requestMutualAuth=").append(requestMutualAuth); builder.append(", requireMutualAuth=").append(requireMutualAuth); - builder.append(", ignoreMissingToken=").append(ignoreMissingToken); + builder.append(", ignoreIncompleteSecurityContext=").append(ignoreIncompleteSecurityContext); builder.append("]"); return builder.toString(); } @@ -135,7 +135,7 @@ public static GssConfig.Builder copy(final GssConfig config) { .setRequestDelegCreds(config.isRequestDelegCreds()) .setRequireMutualAuth(config.isRequireMutualAuth()) .setRequestMutualAuth(config.isRequestMutualAuth()) - .setIgnoreMissingToken(config.isIgnoreMissingToken()); + .setIgnoreIncompleteSecurityContext(config.isIgnoreIncompleteSecurityContext()); } public static class Builder { @@ -145,7 +145,7 @@ public static class Builder { private boolean requestMutualAuth = true; private boolean requireMutualAuth = true; private boolean requestDelegCreds = false; - private boolean ignoreMissingToken = false; + private boolean ignoreIncompleteSecurityContext = false; Builder() { @@ -177,14 +177,14 @@ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) { return this; } - public Builder setIgnoreMissingToken(final boolean ignoreMissingToken) { - this.ignoreMissingToken = ignoreMissingToken; + public Builder setIgnoreIncompleteSecurityContext(final boolean ignoreIncompleteSecurityContext) { + this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext; return this; } public GssConfig build() { - if (requireMutualAuth && ignoreMissingToken) { - throw new IllegalArgumentException("If requireMutualAuth is set then ignoreMissingToken must not be set"); + if (requireMutualAuth && ignoreIncompleteSecurityContext) { + throw new IllegalArgumentException("If requireMutualAuth is set then ignoreIncompleteSecurityContext must not be set"); } if (requireMutualAuth && !requestMutualAuth) { throw new IllegalArgumentException("If requireMutualAuth is set then requestMutualAuth must also be set"); @@ -195,7 +195,7 @@ public GssConfig build() { requestMutualAuth, requireMutualAuth, requestDelegCreds, - ignoreMissingToken + ignoreIncompleteSecurityContext ); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java index dc48e4e672..6270444d36 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java @@ -63,6 +63,7 @@ public GSSCredential getGSSCredential() { @Override public Principal getUserPrincipal() { + // TODO Can we extract this somehow ? return null; } 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 0d8c504627..1b35f8cb34 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 @@ -334,26 +334,25 @@ private boolean needAuthentication( } } + boolean targetNeedsAuth = false; + boolean proxyNeedsAuth = false; if (targetAuthRequested || targetMutualAuthRequired) { - final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response, + targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); } - - return updated; } if (proxyAuthRequested || proxyMutualAuthRequired) { - final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, + proxyNeedsAuth = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } - - return updated; } + return targetNeedsAuth || proxyNeedsAuth; } return false; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java index 55f72f659f..b02d718346 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java @@ -363,7 +363,7 @@ public boolean handleResponse( /** * Generates a response to the authentication challenge based on the actual {@link AuthExchange} state - * and adds it to the given {@link HttpRequest} message . + * and adds it to the given {@link HttpRequest} message. * * @param host the hostname of the opposite endpoint. * @param challengeType the challenge type (target or proxy). diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java index 0a3450bf43..cee4cf42e5 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java @@ -77,15 +77,16 @@ enum State { } private static final Logger LOG = LoggerFactory.getLogger(GssSchemeBase.class); - private static final String KERBEROS_SCHEME = "HTTP"; + private static final String PEER_SERVICE_NAME = "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. + // TODO In the future this might need to be configurable with the upcoming IAKerb support private static final int MAX_GSS_CHALLENGES = 3; private final GssConfig config; private final DnsResolver dnsResolver; private final boolean requireMutualAuth; - private final boolean ignoreMissingToken; + private final boolean ignoreIncompleteSecurityContext; private int challengesLeft = MAX_GSS_CHALLENGES; /** Authentication process state */ @@ -100,10 +101,24 @@ enum State { this.config = config != null ? config : GssConfig.DEFAULT; this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE; this.requireMutualAuth = config.isRequireMutualAuth(); - this.ignoreMissingToken = config.isIgnoreMissingToken(); + this.ignoreIncompleteSecurityContext = config.isIgnoreIncompleteSecurityContext(); this.state = State.UNINITIATED; } + private void dispose() { + // remove sensitive information from memory + // cleaning up the credential is the caller's job + try { + if (gssContext != null) { + gssContext.dispose(); + } + } catch (final Exception e) { + if (LOG.isWarnEnabled()) { + LOG.warn("Exception caught while calling gssContext.dispose()", e); + } + } + } + GssSchemeBase(final GssConfig config) { this(config, SystemDefaultDnsResolver.INSTANCE); } @@ -136,17 +151,16 @@ public void processChallenge( ) throws AuthenticationException { if (challengesLeft-- <= 0 ) { - if (LOG.isDebugEnabled()) { + if (LOG.isWarnEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId); + LOG.warn("{} 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 byte[] challengeToken = (authChallenge == null) ? null : Base64.decodeBase64(authChallenge.getValue()); final String gssHostname; String hostname = host.getHostName(); @@ -154,6 +168,11 @@ public void processChallenge( try { hostname = dnsResolver.resolveCanonicalHostname(host.getHostName()); } catch (final UnknownHostException ignore) { + if (LOG.isWarnEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.warn("{} Could not canonicalize hostname {}, using as is.", exchangeId, host.getHostName()); + } } } if (config.isAddPort()) { @@ -171,26 +190,26 @@ public void processChallenge( switch (state) { case UNINITIATED: setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context); - queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); - if (challengeToken != null) { + if (challengeToken == null) { + queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname); + state = State.TOKEN_READY; + } else { 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.FAILED; } - state = State.TOKEN_READY; break; case TOKEN_SENT: if (challengeToken == null) { - if (!challenged && ignoreMissingToken) { + if (!challenged && ignoreIncompleteSecurityContext) { // Got a Non 401/407 code without a challenge. Old non RFC compliant server. - if (LOG.isDebugEnabled()) { + if (LOG.isWarnEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} GSS Context is not established, but continuing because GssConfig.ignoreMissingToken is true.", exchangeId); + LOG.warn("{} GSS Context is not established, but continuing because GssConfig.ignoreIncompleteSecurityContext is true.", exchangeId); } state = State.SUCCEEDED; break; @@ -198,15 +217,15 @@ public void processChallenge( if (LOG.isDebugEnabled()) { final HttpClientContext clientContext = HttpClientContext.cast(context); final String exchangeId = clientContext.getExchangeId(); - LOG.debug("{} Did not receive required challenge and GssConfig.ignoreMissingToken is false.", + LOG.debug("{} Did not receive required challenge.", exchangeId); } state = State.FAILED; throw new AuthenticationException( - "Did not receive required challenge and GssConfig.ignoreMissingToken is false."); + "Did not receive required challenge."); } } - queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname); + queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname); if (challenged) { state = State.TOKEN_READY; } else if (!gssContext.isEstablished()) { @@ -237,16 +256,16 @@ public void processChallenge( LOG.debug("{} GSSContext MutualAuthState is false, but continuing because GssConfig.requireMutualAuth is false.", exchangeId); } - state = State.FAILED; + state = State.SUCCEEDED; } } else { state = State.SUCCEEDED; } break; default: + final State prevState = state; state = State.FAILED; - throw new IllegalStateException("Illegal state: " + state); - + throw new IllegalStateException("Illegal state: " + prevState); } } catch (final GSSException gsse) { state = State.FAILED; @@ -264,6 +283,10 @@ public void processChallenge( } // other error throw new AuthenticationException(gsse.getMessage(), gsse); + } finally { + if ((state == State.FAILED || state == State.SUCCEEDED) && gssContext != null) { + dispose(); + } } } @@ -336,9 +359,6 @@ public boolean isResponseReady( protected void setGssCredential(final CredentialsProvider credentialsProvider, final HttpHost host, final HttpContext context) { - if (this.gssCredential != null) { - return; - } final Credentials credentials = credentialsProvider.getCredentials(new AuthScope(host, null, getName()), context); if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java index 1b5ff557c5..d6808e9649 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/SpnegoScheme.java @@ -37,8 +37,8 @@ * 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} + * This is the new mutual authentication capable Scheme which replaces the old deprecated + * {@link SPNegoScheme} *

* *

@@ -115,7 +115,7 @@ protected byte[] generateToken(final byte[] input, final String gssServiceName, @Override public boolean isConnectionBased() { - return true; + return false; } } 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 fe3d35281f..b927dbc798 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 @@ -295,26 +295,25 @@ private boolean needAuthentication( } } + boolean targetNeedsAuth = false; + boolean proxyNeedsAuth = false; if (targetAuthRequested || targetMutualAuthRequired) { - final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response, + targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); if (authCacheKeeper != null) { authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); } - - return updated; } if (proxyAuthRequested || proxyMutualAuthRequired) { - final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, + proxyNeedsAuth = authenticator.handleResponse(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); } - - return updated; } + return targetNeedsAuth || proxyNeedsAuth; } return false; }