Skip to content

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 5 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 3 times, most recently from a6efd89 to 96aa2ba Compare July 1, 2025 08:57
@raccoonback raccoonback force-pushed the issue-3079 branch 2 times, most recently from 8fcac3f to a77c0a5 Compare July 5, 2025 03:43
@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 :)

@wendigo
Copy link

wendigo commented Jul 23, 2025

I can provide some guidance around APIs and configuration. Not every kerberos-enabled client uses JAAS, therefore the direct Subject/SPNEGO token support should be provided

@raccoonback
Copy link
Contributor Author

@wendigo
Hello!
Thank you for the great point. 😀

I was thinking of allowing users to implement the SpnegoAuthenticator interface, similar to JaasAuthenticator, to support custom authentication logic if needed.

If I understood you correctly, you're suggesting that we should provide a way for users to directly supply a Subject, as in the example below:

public class DirectSubjectAuthenticator implements SpnegoAuthenticator {

    // ...
    private Subject subject;

    @Override
    public Subject login() throws LoginException {
        return subject;
    }

    // ...
}

Would you be able to share a more concrete example or use case?
It would help refine the design in the right direction.

@wendigo
Copy link

wendigo commented Jul 24, 2025

Sure @raccoonback.

I'd like to use reactor-netty in the trino CLI/JDBC/client libraries.

We support delegated/constrained/unconstrained kerberos authentication. Relevant code is here:

https://github.com/trinodb/trino/tree/master/client/trino-client/src/main/java/io/trino/client/auth/kerberos

This is how we add it to the okhttp: https://github.com/trinodb/trino/blob/master/client/trino-client/src/main/java/io/trino/client/auth/kerberos/SpnegoHandler.java

Configurability is important as we expose configuration that allows the user to pass remote service name, service principal name, whether to canonicalize hostname: https://github.com/trinodb/trino/blob/master/client/trino-client/src/main/java/io/trino/client/auth/kerberos/SpnegoHandler.java#L50C5-L54C48

@raccoonback
Copy link
Contributor Author

@violetagg
I think supporting not only JAAS-based authentication but also allowing the user to provide a GSSCredential directly could improve configurability and flexibility.
This would be especially useful in environments where JAAS is not preferred or where credentials need to be managed programmatically.
What do you think about this direction?

cc. @wendigo

@violetagg
Copy link
Member

violetagg commented Jul 28, 2025 via email

@raccoonback
Copy link
Contributor Author

@wendigo
I've added GSSCredential-based SPNEGO authentication and support for service name and canonical hostname configuration.
Thank you for the suggestion.

@raccoonback raccoonback force-pushed the issue-3079 branch 2 times, most recently from 685924c to b082661 Compare July 30, 2025 23:39
@raccoonback
Copy link
Contributor Author

@violetagg
Hello.
I would appreciate it if you could review this PR.
Thanks!

@violetagg
Copy link
Member

@violetagg Hello. I would appreciate it if you could review this PR. Thanks!

I'm just returning fro vacation, will check it in the next days or so

@raccoonback
Copy link
Contributor Author

@violetagg
Hello.
I would appreciate it if you could review this PR.
Thanks!

@violetagg
Copy link
Member

I will check this one ... just need to finalise some other tasks.

@violetagg
Copy link
Member

@raccoonback We discussed to introduce this feature for 1.3.0 version but it is based on 1.2.x branch. Which version are we targeting?

@raccoonback
Copy link
Contributor Author

@violetagg
I think I got confused earlier.
As you mentioned, I’ll make the changes based on version 1.3.x.
ref. #3079 (comment)

@raccoonback
Copy link
Contributor Author

@violetagg
I couldn’t find the 1.3.x branch — would it be okay if I open the PR based on the main branch instead?

@violetagg
Copy link
Member

@violetagg I couldn’t find the 1.3.x branch — would it be okay if I open the PR based on the main branch instead?

yes main is the branch that is for 1.3.x version

@raccoonback raccoonback changed the base branch from 1.2.x to main October 29, 2025 14:15
@raccoonback
Copy link
Contributor Author

@violetagg
I’ve changed the target branch to 1.3.x.
Please review PR.

@violetagg
Copy link
Member

After following the discussion here and having in mind the Reactor Netty API, I think that the solution should be a bit more generic. Also IMO, Reactor Netty is responsible to provide the infrastructure while the user - to provide the concrete implementation (for example how the token will be generated, cached etc).
My proposal is the following (similar to how follow redirect is implemented)

HttpClient.httpAuthentication(
			BiPredicate<HttpClientRequest, HttpClientResponse> authenticationPredicate,
			Consumer<HttpClientRequest> authenticationConsumer)

And the usage will be

HttpClient.create()
			.httpAuthentication(
					(request, response) ->
						return response.status().code() == 401 &&
								response.responseHeaders().contains("WWW-Authenticate", "Negotiate", true)
					,
					request -> {
							String token = getOrGenerateToken(request.resourceUrl());
							request.header("Authorization", "Negotiate " + token);
					}
			);

@violetagg
Copy link
Member

The API is a bit rough, if we have a delayed token generation we may need the authenticationConsumer to return Mono that we can execute before sending the request. What do you think?

@raccoonback
Copy link
Contributor Author

@violetagg
Thank you for the review. 😀
I have a few questions.

  1. Regarding the token issuance and re-authentication behavior you mentioned — should we let users define only those parts?
    Or do you also think users should define the handling of components like GssCredentialAuthenticator?

  2. Currently, the authentication header value is stored and reused for subsequent HTTP requests.
    Do you also expect this kind of handling to be managed through user-defined logic?

@raccoonback
Copy link
Contributor Author

@violetagg
I initially considered this as a feature addition specifically for SPNEGO authentication...
However, are you suggesting adding only the authentication flow itself, without including the detailed SPNEGO implementation or dependencies ~~?

@violetagg
Copy link
Member

@violetagg Thank you for the review. 😀 I have a few questions.

  1. Regarding the token issuance and re-authentication behavior you mentioned — should we let users define only those parts?
    Or do you also think users should define the handling of components like GssCredentialAuthenticator?
  2. Currently, the authentication header value is stored and reused for subsequent HTTP requests.
    Do you also expect this kind of handling to be managed through user-defined logic?

I would like the user to be responsible for the token generation (thus our API will work not only for SPNEGO) and also the decision how the token will be used/reused, what kind of cache that will be used, what will be the expiration time etc., it is not Reactor Netty decision but user's.

@violetagg
Copy link
Member

violetagg commented Nov 3, 2025

@violetagg I initially considered this as a feature addition specifically for SPNEGO authentication... However, are you suggesting adding only the authentication flow itself, without including the detailed SPNEGO implementation or dependencies ~~?

yes, I understand
however I think that if we try to provide a generic API, later it will be able to be used not only with SPNEGO

We can show case in the examples various http authentications

@raccoonback
Copy link
Contributor Author

@violetagg
I’ll update it according to the direction you suggested.

Comment on lines +460 to +470
HttpClientOperations operations = connection.as(HttpClientOperations.class);
if (operations != null && handler.authenticationPredicate != null) {
if (handler.authenticationPredicate.test(operations, operations)) {
if (log.isDebugEnabled()) {
log.debug(format(operations.channel(), "Authentication predicate matched, triggering retry"));
}
sink.error(new HttpClientAuthenticationException());
return;
}
}

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 propagates an exception to trigger a retry, when the authentication requirements are met.

Copy link
Member

Choose a reason for hiding this comment

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

It is better to use the mechanism that we use for follow redirect. reactor.netty.http.client.HttpClientOperations#onInboundNext -> see how "redirecting" is used. The idea is that once we decide to retry we will need to take care of this connection, which means we drain the incoming messages, no message goes to the end user etc (we don't know whether the server will send only the headers or also some message). Basically we need to return to the connection pool a clean connection. Also because we don't send any message to the end user we can switch to auto-read = 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.

@violetagg I'll check it.

Comment on lines +571 to +581
if (authenticator != null && authenticationAttempted) {
return authenticator.apply(ch, ch.address())
.then(
Mono.defer(
() -> Mono.from(requestWithBodyInternal(ch))
)
);
}

return requestWithBodyInternal(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.

The authenticator is executed first and then the HTTP request is attempted, when a retry is triggered due to HTTP authentication.

Copy link
Member

Choose a reason for hiding this comment

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

Isn't it too early here to provide the HttpClientOperations? HttpClientOperations is still empty and we need to configure it which happens in the new method requestWithBodyInternal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense.
I will update it so that the authenticator is applied after the configuration.
Configuration → apply authenticator → request.

Comment on lines +762 to +766
if (throwable instanceof HttpClientAuthenticationException) {
// Set flag to trigger authenticator on retry
authenticationAttempted = true;
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.

The HTTP request is retried, when the HTTP authentication requirements are met.

…ecific design

Following the review feedback, this change updates the proposed SPNEGO-specific
implementation into a more generic authentication flow that can support various
HTTP authentication mechanisms.

Signed-off-by: raccoonback <[email protected]>
* Generates an authentication token for the given remote address.
* In a real application, this would retrieve or generate a valid token.
*/
static String generateAuthToken(SocketAddress remoteAddress) {

Check notice

Code scanning / CodeQL

Useless parameter Note documentation

The parameter 'remoteAddress' is never used.
@raccoonback raccoonback changed the title Support SPNEGO Authentication in HttpClient Support HTTP Authentication in HttpClient Nov 17, 2025
@raccoonback
Copy link
Contributor Author

@violetagg
I’ve improved it in the guide you suggested.
Please review it!

Copy link
Member

@violetagg violetagg left a comment

Choose a reason for hiding this comment

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

Thanks! This is the direction. I have some comments related to the implementation.

* @return a new {@link HttpClient}
* @since 1.3.0
*/
public final HttpClient httpAuthentication(
Copy link
Member

Choose a reason for hiding this comment

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

Let's have two APIs (similar to what we have already)

  • httpAuthentication - this will provide just a BiConsumer
  • httpAuthenticationWhen - this will use the current signature

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@violetagg
Since the decision to apply the authenticator is made using a BiPredicate<HttpClientRequest, HttpClientResponse> predicate, I think that httpAuthentication requires both a BiPredicate and a BiConsumer.
What do you think?

Comment on lines +460 to +470
HttpClientOperations operations = connection.as(HttpClientOperations.class);
if (operations != null && handler.authenticationPredicate != null) {
if (handler.authenticationPredicate.test(operations, operations)) {
if (log.isDebugEnabled()) {
log.debug(format(operations.channel(), "Authentication predicate matched, triggering retry"));
}
sink.error(new HttpClientAuthenticationException());
return;
}
}

Copy link
Member

Choose a reason for hiding this comment

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

It is better to use the mechanism that we use for follow redirect. reactor.netty.http.client.HttpClientOperations#onInboundNext -> see how "redirecting" is used. The idea is that once we decide to retry we will need to take care of this connection, which means we drain the incoming messages, no message goes to the end user etc (we don't know whether the server will send only the headers or also some message). Basically we need to return to the connection pool a clean connection. Also because we don't send any message to the end user we can switch to auto-read = true

Comment on lines +571 to +581
if (authenticator != null && authenticationAttempted) {
return authenticator.apply(ch, ch.address())
.then(
Mono.defer(
() -> Mono.from(requestWithBodyInternal(ch))
)
);
}

return requestWithBodyInternal(ch);
}
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it too early here to provide the HttpClientOperations? HttpClientOperations is still empty and we need to configure it which happens in the new method requestWithBodyInternal.

* @author Oliver Ko
* @since 1.3.0
*/
final class HttpClientAuthenticationException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about omitting the stack trace similar to RedirectClientException?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@violetagg
Okay, I will omit the stack trace!
I have one question: is there a particular reason for omitting the stack trace?

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.

Support For Spnego Auth scheme support for netty HttpClient similar to Apache's

3 participants