Skip to content

Support SPNEGO Authentication in HttpClient #3813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 1.2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions docs/modules/ROOT/pages/http-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -742,3 +742,144 @@ To customize the default settings, you can configure `HttpClient` as follows:
include::{examples-dir}/resolver/Application.java[lines=18..39]
----
<1> The timeout of each DNS query performed by this resolver will be 500ms.

[[http-client-spnego]]
== SPNEGO Authentication
Reactor Netty HttpClient supports SPNEGO (Kerberos) authentication, which is widely used in enterprise environments.
SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) provides secure authentication over HTTP using Kerberos.

==== How It Works
SPNEGO authentication follows this HTTP authentication flow:

. The client sends an HTTP request to a protected resource.
. The server responds with `401 Unauthorized` and a `WWW-Authenticate: Negotiate` header.
. The client generates a SPNEGO token based on its Kerberos ticket, and resends the request with an `Authorization: Negotiate <base64-encoded-token>` header.
. The server validates the token and, if authentication is successful, returns 200 OK.

If further negotiation is required, the server may return another 401 with additional data in the `WWW-Authenticate` header.

==== JAAS-based Authenticator
{examples-link}/spnego/jaas/Application.java
----
include::{examples-dir}/spnego/jaas/Application.java[lines=18..45]
----
<1> Configures the `jaas.conf`. A JAAS configuration file in Java for integrating with authentication backends such as Kerberos.
<2> Configures the `krb5.conf`. krb5.conf is a Kerberos client configuration file used to define how the client locates and communicates with the Kerberos Key Distribution Center (KDC) for authentication.
<3> Configures the SPNEGO jaas.conf. A JVM system property that enables detailed debug logging for Kerberos operations in Java.
<4> `JaasAuthenticator` performs Kerberos authentication using a JAAS configuration (jaas.conf).
<5> `SpnegoAuthProvider.Builder` supports the following configuration methods. Please refer to <<spnegoauthprovider-config>>.
<6> `SpnegoAuthProvider` generates a SPNEGO token from the Kerberos ticket. It automatically adds the `Authorization: Negotiate ...` header to HTTP requests. If the server responds with `401 Unauthorized` and includes `WWW-Authenticate: Negotiate`, the client will automatically reauthenticate and retry the request once.

===== Example JAAS Configuration
Specify the path to your JAAS configuration file using the `java.security.auth.login.config` system property.

.`jaas.conf`
[jaas,conf]
----
KerberosLogin {
com.sun.security.auth.module.Krb5LoginModule required
client=true
useKeyTab=true
keyTab="/path/to/test.keytab"
principal="[email protected]"
doNotPrompt=true
debug=true;
};
----

===== Example Kerberos Configuration
Specify Kerberos realm and KDC information using the `java.security.krb5.conf` system property.

.`krb5.conf`
[krb5,conf]
----
[libdefaults]
default_realm = EXAMPLE.COM
[realms]
EXAMPLE.COM = {
kdc = kdc.example.com
}
[domain_realms]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
----

===== Configuration Example
[jvm option]
----
-Djava.security.auth.login.config=/path/to/login.conf
-Djava.security.krb5.conf=/path/to/krb5.conf
----

==== GSSCredential-based Authenticator
For scenarios where you already have a `GSSCredential` available or want to avoid JAAS configuration, you can use `GssCredentialAuthenticator`:

{examples-link}/spnego/gsscredential/Application.java
----
include::{examples-dir}/spnego/gsscredential/Application.java[lines=18..46]
----
<1> Obtain the `GSSCredential` through other means.
<2> Configure the GSSCredential-based authenticator for SPNEGO authentication.

This approach is useful when:
- You want to reuse existing credentials
- You need more control over credential management
- JAAS configuration is not available or preferred

==== Custom Authenticator Implementation
For advanced scenarios where the provided authenticators don't meet your specific requirements, you can implement the `SpnegoAuthenticator` interface directly:

----
import org.ietf.jgss.GSSContext;
import reactor.netty.http.client.SpnegoAuthenticator;
import reactor.netty.http.client.SpnegoAuthProvider;

public class CustomSpnegoAuthenticator implements SpnegoAuthenticator {

@Override
public GSSContext createContext(String serviceName, String remoteHost) throws Exception {
// Your custom authentication logic here
// This method should return a configured GSSContext
// for the specified service and remote host
// serviceName: e.g., "HTTP", "LDAP"
// remoteHost: target server hostname

return null; // Replace with actual GSSContext creation logic
}
}

// Usage with advanced configuration
HttpClient client = HttpClient.create()
.spnego(
SpnegoAuthProvider.builder(new CustomSpnegoAuthenticator())
.serviceName("HTTP") // Custom service name
.unauthorizedStatusCode(401) // Custom status code
.resolveCanonicalHostname(true) // Use canonical hostname
.build()
);
----

This approach is useful when you need:
- Custom credential acquisition logic
- Integration with third-party authentication systems
- Special handling for token caching or refresh
- Environment-specific authentication flows

[[spnegoauthprovider-config]]
==== SpnegoAuthProvider Configuration Options
The `SpnegoAuthProvider.Builder` supports the following configuration Options:

[width="100%",options="header"]
|=======
| Method | Default | Description | Example
| `serviceName(String)` | "HTTP" | Service name for constructing service principal names (serviceName/hostname) | "HTTP", "LDAP"
| `unauthorizedStatusCode(int)` | 401 | HTTP status code that triggers authentication retry | 401, 407
| `resolveCanonicalHostname(boolean)` | false | Whether to use canonical hostname resolution via reverse DNS lookup | true for FQDN requirements
|=======

==== Notes
- SPNEGO authentication is fully supported on Java 1.6 and above.
- If authentication fails, check the server logs and client exception messages, and verify your Kerberos environment settings (realm, KDC, ticket, etc.).
- `JaasAuthenticator` performs authentication through JAAS login configuration.
- `GssCredentialAuthenticator` uses pre-existing `GSSCredential` objects, bypassing JAAS configuration.
- For custom scenarios, implement the `SpnegoAuthenticator` interface to provide your own authentication logic.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed 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
*
* https://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.
*/
package reactor.netty.examples.documentation.http.client.spnego.gsscredential;

import org.ietf.jgss.GSSCredential;
import reactor.netty.http.client.GssCredentialAuthenticator;
import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.SpnegoAuthProvider;

public class Application {

public static void main(String[] args) {
// Assuming you have obtained a GSSCredential from elsewhere
GSSCredential credential = obtainGSSCredential(); // <1>

HttpClient client = HttpClient.create()
.spnego(
SpnegoAuthProvider.builder(new GssCredentialAuthenticator(credential)) // <2>
.build()
);

client.get()
.uri("http://protected.example.com/")
.responseSingle((res, content) -> content.asString())
.block();
}

private static GSSCredential obtainGSSCredential() {
// Implement your logic to obtain a GSSCredential
// This could involve using a Kerberos library or other means
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2025 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed 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
*
* https://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.
*/
package reactor.netty.examples.documentation.http.client.spnego.jaas;

import reactor.netty.http.client.HttpClient;
import reactor.netty.http.client.JaasAuthenticator;
import reactor.netty.http.client.SpnegoAuthProvider;
import reactor.netty.http.client.SpnegoAuthenticator;

public class Application {

public static void main(String[] args) {
System.setProperty("java.security.auth.login.config", "/path/to/jaas.conf"); // <1>
System.setProperty("java.security.krb5.conf", "/path/to/krb5.conf"); // <2>
System.setProperty("sun.security.krb5.debug", "true"); // <3>

SpnegoAuthenticator authenticator = new JaasAuthenticator("KerberosLogin"); // <4>
SpnegoAuthProvider provider = SpnegoAuthProvider.builder(authenticator)
.serviceName("HTTP")
.unauthorizedStatusCode(401)
.resolveCanonicalHostname(false)
.build();
HttpClient client = HttpClient.create()
.spnego(
provider // <5>
); // <6>

client.get()
.uri("http://protected.example.com/")
.responseSingle((res, content) -> content.asString())
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2025 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed 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
*
* https://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.
*/
package reactor.netty.http.client;

import java.util.Objects;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

/**
* A GSSCredential-based Authenticator implementation for use with SPNEGO providers.
* <p>
* This authenticator uses a pre-existing GSSCredential to create a GSSContext,
* bypassing the need for JAAS login configuration. This is useful when you already
* have obtained Kerberos credentials through other means or want more direct control
* over the authentication process.
* </p>
* <p>
* The GSSCredential should contain valid Kerberos credentials that can be used
* for SPNEGO authentication. The credential's lifetime and validity are managed
* externally to this authenticator.
* </p>
*
* <p>Example usage:</p>
* <pre>
* GSSCredential credential = // ... obtain credential
* GssCredentialAuthenticator authenticator = new GssCredentialAuthenticator(credential);
* SpnegoAuthProvider provider = SpnegoAuthProvider.builder(authenticator).build();
* </pre>
*
* @author raccoonback
* @since 1.3.0
*/
public class GssCredentialAuthenticator implements SpnegoAuthenticator {

private final GSSCredential credential;

/**
* Creates a new GssCredentialAuthenticator with the given GSSCredential.
*
* @param credential the GSSCredential to use for authentication
*/
public GssCredentialAuthenticator(GSSCredential credential) {
Objects.requireNonNull(credential, "GSSCredential cannot be null");
this.credential = credential;
}

/**
* Creates a GSSContext for the specified service and remote host using the provided GSSCredential.
* <p>
* This method uses the pre-existing GSSCredential to create a GSSContext for SPNEGO
* authentication. The service principal name is constructed as serviceName/remoteHost.
* </p>
*
* @param serviceName the service name (e.g., "HTTP", "FTP")
* @param remoteHost the remote host to authenticate with
* @return the created GSSContext configured for SPNEGO authentication
* @throws Exception if context creation fails
*/
@Override
public GSSContext createContext(String serviceName, String remoteHost) throws Exception {
GSSManager manager = GSSManager.getInstance();
GSSName serverName = manager.createName(serviceName + "/" + remoteHost, GSSName.NT_HOSTBASED_SERVICE);
GSSContext context = manager.createContext(
serverName,
new Oid("1.3.6.1.5.5.2"),
credential,
GSSContext.DEFAULT_LIFETIME
);
context.requestMutualAuth(true);
return context;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,20 @@ public final HttpClient wiretap(boolean enable) {
return super.wiretap(enable);
}

/**
* Configure SPNEGO authentication for the HTTP client.
*
* @param spnegoAuthProvider the SPNEGO authentication provider
* @return a new {@link HttpClient}
* @since 1.3.0
*/
public final HttpClient spnego(SpnegoAuthProvider spnegoAuthProvider) {
Objects.requireNonNull(spnegoAuthProvider, "spnegoAuthProvider");
HttpClient dup = duplicate();
dup.configuration().spnegoAuthProvider = spnegoAuthProvider;
return dup;
}

static boolean isCompressing(HttpHeaders h) {
return h.contains(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP, true)
|| h.contains(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.BR, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ public WebsocketClientSpec websocketClientSpec() {
String uriStr;
Function<String, String> uriTagValue;
WebsocketClientSpec websocketClientSpec;
SpnegoAuthProvider spnegoAuthProvider;

HttpClientConfig(HttpConnectionProvider connectionProvider, Map<ChannelOption<?>, ?> options,
Supplier<? extends SocketAddress> remoteAddress) {
Expand Down Expand Up @@ -430,6 +431,7 @@ public WebsocketClientSpec websocketClientSpec() {
this.uriStr = parent.uriStr;
this.uriTagValue = parent.uriTagValue;
this.websocketClientSpec = parent.websocketClientSpec;
this.spnegoAuthProvider = parent.spnegoAuthProvider;
}

@Override
Expand Down
Loading