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 5 commits into
base: 1.2.x
Choose a base branch
from

Conversation

raccoonback
Copy link
Contributor

@raccoonback raccoonback commented Jun 23, 2025

Motivation

This PR adds support for SPNEGO (Kerberos) authentication to HttpClient, addressing #3079.
SPNEGO is widely used for HTTP authentication in enterprise environments, particularly those based on Kerberos.

Changes

SpnegoAuthProvider

Provides SPNEGO authentication by generating a Kerberos-based token and attaching it to the Authorization header of outgoing HTTP requests.

JaasAuthenticator

Provides a pluggable way to perform JAAS-based Kerberos login, making it easy to integrate with various authentication backends.

HttpClient.spnego(...) API

Adds a new API to configure SPNEGO authentication for HttpClient instances.

HttpClient client = HttpClient.create()
    .spnego(SpnegoAuthProvider.create(new JaasAuthenticator("KerberosLogin")));

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

jaas.conf

A JAAS(Java Authentication and Authorization Service) configuration file in Java for integrating with authentication backends such as Kerberos.

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

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.

[libdefaults]
    default_realm = EXAMPLE.COM
[realms]
    EXAMPLE.COM = {
        kdc = kdc.example.com
    }
[domain_realms]
    .example.com = EXAMPLE.COM
    example.com = EXAMPLE.COM

How It Works

  • When a server responds with 401 Unauthorized and a WWW-Authenticate: Negotiate header,
    the client automatically generates a SPNEGO token using the Kerberos ticket and resends the request with the appropriate Authorization header.
  • The implementation is based on Java's GSS-API and is compatible with standard Kerberos environments.

Environment Configuration

Requires proper JAAS (jaas.conf) and Kerberos (krb5.conf) configuration.
See the updated documentation for example configuration files and JVM options.

Additional Notes

  • The SpnegoAuthProvider allows for easy extension and testing by supporting custom authenticators and GSSManager injection.
  • The feature is fully compatible with Java 1.6+ and works on both Unix and Windows environments.

@raccoonback
Copy link
Contributor Author

I tested Kerberos authentication using the krb5 available at https://formulae.brew.sh/formula/krb5.

@raccoonback raccoonback force-pushed the issue-3079 branch 3 times, most recently from a656f1d to 8dd00a1 Compare June 23, 2025 13:08
@raccoonback raccoonback force-pushed the issue-3079 branch 2 times, most recently from 19ccf13 to 090e1c2 Compare June 26, 2025 06:49
@raccoonback raccoonback changed the title Support SPNEGO (Kerberos) Authentication in HttpClient Support SPNEGO Authentication in HttpClient Jun 27, 2025
@raccoonback raccoonback force-pushed the issue-3079 branch 2 times, most recently from 5ce5819 to a6efd89 Compare June 30, 2025 00:04
Comment on lines +31 to +56
public class JaasAuthenticator implements SpnegoAuthenticator {

private final String contextName;

/**
* Creates a new JaasAuthenticator with the given context name.
*
* @param contextName the JAAS login context name
*/
public JaasAuthenticator(String contextName) {
this.contextName = contextName;
}

/**
* Performs a JAAS login using the configured context name and returns the authenticated Subject.
*
* @return the authenticated JAAS Subject
* @throws LoginException if login fails
*/
@Override
public Subject login() throws LoginException {
LoginContext context = new LoginContext(contextName);
context.login();
return context.getSubject();
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It logs in using the specified JAAS login context name and returns the authenticated Subject object.

Comment on lines 449 to 457
HttpClientOperations operations = connection.as(HttpClientOperations.class);
if (operations != null && handler.spnegoAuthProvider != null) {
int statusCode = operations.status().code();
HttpHeaders headers = operations.responseHeaders();
if (handler.spnegoAuthProvider.isUnauthorized(statusCode, headers)) {
handler.spnegoAuthProvider.invalidateCache();
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a SPNEGO authentication expiration response is received via the HTTP response, the authentication header value is cleared.

Comment on lines +555 to +567
if (spnegoAuthProvider != null) {
return spnegoAuthProvider.apply(ch, ch.address())
.then(
Mono.defer(
() -> Mono.from(requestWithBodyInternal(ch))
)
);
}

return requestWithBodyInternal(ch);
}

private Publisher<Void> requestWithBodyInternal(HttpClientOperations ch) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If SPNEGO authentication is configured through the HttpClient, authentication is attempted before sending the request.

Comment on lines 113 to 116
if (verifiedAuthHeader != null) {
request.header(HttpHeaderNames.AUTHORIZATION, verifiedAuthHeader);
return Mono.empty();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a valid authentication token already exists, it is reused.

Comment on lines 118 to 142
return Mono.fromCallable(() -> {
try {
return Subject.doAs(
authenticator.login(),
(PrivilegedAction<byte[]>) () -> {
try {
byte[] token = generateSpnegoToken(address.getHostName());
String authHeader = SPNEGO_HEADER + " " + Base64.getEncoder().encodeToString(token);

verifiedAuthHeader = authHeader;
request.header(HttpHeaderNames.AUTHORIZATION, authHeader);
return token;
}
catch (GSSException e) {
throw new RuntimeException("Failed to generate SPNEGO token", e);
}
}
);
}
catch (LoginException e) {
throw new RuntimeException("Failed to login with SPNEGO", e);
}
})
.subscribeOn(boundedElastic())
.then();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After obtaining Kerberos credentials via JAAS login, a SPNEGO token for the target server is generated using the GSS-API, and the Authorization: Negotiate <token> header is added to the request.

@@ -446,6 +447,16 @@ public Context currentContext() {
@Override
public void onStateChange(Connection connection, State newState) {
if (newState == HttpClientState.RESPONSE_RECEIVED) {
HttpClientOperations operations = connection.as(HttpClientOperations.class);
if (operations != null && handler.spnegoAuthProvider != null) {
if (shouldRetryWithSpnego(operations)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-authenticates with SPNEGO if necessary.

Comment on lines +785 to +787
if (throwable instanceof SpnegoRetryException) {
return true;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configures it to retry even if a SpnegoRetryException occurs.

@raccoonback
Copy link
Contributor Author

@violetagg
Hello!
Please check this PR when you have a chance. 😃

@wendigo
Copy link

wendigo commented Jul 23, 2025

This is so great! Looking forward to get this in :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants