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.

* 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?

Copy link
Member

Choose a reason for hiding this comment

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

yes

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.

@raccoonback
Copy link
Contributor Author

@violetagg
Should this feature also be considered in Http2WebsocketClientOperations and WebsocketClientOperations?
Since redirecting is already handled in those Operations as well, I wanted to check.

@violetagg
Copy link
Member

@violetagg Should this feature also be considered in Http2WebsocketClientOperations and WebsocketClientOperations? Since redirecting is already handled in those Operations as well, I wanted to check.

yes

…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]>
Moved authenticator to occur after the HTTP request is
prepared, ensuring configuration is completed before authentication
logic runs.

Signed-off-by: raccoonback <[email protected]>
Move authentication predicate evaluation from RESPONSE_RECEIVED state to response processing, ensuring all response messages are consumed before retrying with authentication.

Signed-off-by: raccoonback <[email protected]>
Introduce httpAuthentication(BiFunction) for automatic 401 retry, and rename the predicate-based method to httpAuthenticationWhen for clarity when custom retry conditions are needed.

Signed-off-by: raccoonback <[email protected]>
Signed-off-by: raccoonback <[email protected]>
Comment on lines 1699 to 1731
/**
* Configure HTTP authentication that retries on 401 Unauthorized responses.
* <p>
* This method provides a generic authentication framework that allows users to implement
* their own authentication mechanisms (e.g., Negotiate/SPNEGO, OAuth, Bearer tokens, custom schemes).
* The framework automatically retries requests when a 401 Unauthorized response is received.
* </p>
*
* <p>Example - Token-based Authentication:</p>
* <pre>
* {@code
* HttpClient client = HttpClient.create()
* .httpAuthentication(
* // Add authentication header before request
* (req, addr) -> {
* String token = generateAuthToken(addr);
* req.header(HttpHeaderNames.AUTHORIZATION, "Bearer " + token);
* return Mono.empty();
* }
* );
* }
* </pre>
*
* @param authenticator applies authentication to the request, receives the request and remote address,
* returns a Mono that completes when authentication is applied
* @return a new {@link HttpClient}
* @since 1.3.0
* @see #httpAuthenticationWhen(BiPredicate, BiFunction)
*/
public final HttpClient httpAuthentication(
BiFunction<? super HttpClientRequest, ? super SocketAddress, ? extends Mono<Void>> authenticator) {
return httpAuthenticationWhen(HttpClientConfig.AUTHENTICATION_PREDICATE, authenticator);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

support HTTP authentication that retries on 401 Unauthorized responses.

Copy link
Member

Choose a reason for hiding this comment

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

I needed to be more concrete - I was thinking about these below. The idea is that if you don't need to delay the token generation (i.e. asking some other component/service for a token) then you can immediately calculate it and add it to the headers thus no need of Mono otherwise you use the variant with Mono. Does this make sense?

	public final HttpClient httpAuthentication(
			BiPredicate<HttpClientRequest, HttpClientResponse> predicate,
			BiConsumer<? super HttpClientRequest, ? super SocketAddress> authenticator) {
	}

	public final HttpClient httpAuthenticationWhen(
			BiPredicate<HttpClientRequest, HttpClientResponse> predicate,
			BiFunction<? super HttpClientRequest, ? super SocketAddress, ? extends Mono<Void>> authenticator) {
	}

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
Feedback has been applied.
Please review it again. 🙏

Signed-off-by: raccoonback <[email protected]>
@raccoonback
Copy link
Contributor Author

@violetagg
I’ve applied all the review comments.
Please take another look. 🙂

Signed-off-by: raccoonback <[email protected]>
This change updates the httpAuthentication() method to require both a retry predicate and an authenticator, allowing users to customize when authentication retry should occur.

Signed-off-by: raccoonback <[email protected]>
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