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..c4183eb52a
--- /dev/null
+++ b/httpclient5-testing/src/test/resources/docker/httpd/start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+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..7785e58ea1
--- /dev/null
+++ b/httpclient5-testing/src/test/resources/docker/kdc/krb5.conf
@@ -0,0 +1,19 @@
+[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..cb09f23020
--- /dev/null
+++ b/httpclient5-testing/src/test/resources/docker/kdc/start.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# 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..6fa04715f3 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
@@ -165,6 +165,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 +299,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 9cc5cda643..23300ee6b5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -76,7 +76,12 @@
2.2.21
1.20.2
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}