Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4ddffb0
Support SPNEGO (Kerberos) authentication
raccoonback Jun 23, 2025
6b09c79
Reuse SPNEGO token until expiry and reset on expiration
raccoonback Jun 26, 2025
5a8ff02
Ensure GSSContext is disposed after initializing security context
raccoonback Jul 4, 2025
956b1f8
Add SpnegoAuthenticationException for better error handling in SPNEGO…
raccoonback Jul 4, 2025
82dd4f6
Improve SPNEGO authentication retry mechanism
raccoonback Jul 4, 2025
d1ef964
Support GSSCredential-based SPNEGO authentication
raccoonback Jul 30, 2025
5e664fa
Generalize the httpAuthentication infrastructure and remove SPNEGO-sp…
raccoonback Nov 17, 2025
ee921a0
Update omitting the stack trace
raccoonback Nov 19, 2025
e35a028
Apply authenticator after HTTP request preparation
raccoonback Nov 20, 2025
0e05222
Ensure response messages are drained before authentication retry
raccoonback Nov 20, 2025
1865e7c
Extend authentication handling to WebSocket operations
raccoonback Nov 20, 2025
138d7d2
Add httpAuthentication API and rename existing to httpAuthenticationWhen
raccoonback Nov 20, 2025
721be5c
Add test for HTTP authentication state propagation
raccoonback Nov 20, 2025
dead58f
Update documentation and examples for httpAuthentication API changes
raccoonback Nov 20, 2025
723ac4b
Fix broken test
raccoonback Nov 20, 2025
d2c310c
Fix checkstyle
raccoonback Nov 20, 2025
b65d57d
Fix test warning log
raccoonback Nov 21, 2025
18a038c
Update httpAuthentication API to accept custom retry predicate
raccoonback Nov 22, 2025
9c3d6b9
notAuthenticated -> authenticationNotRequired
raccoonback Nov 24, 2025
0a9ac82
Update javadoc about http authentication
raccoonback Nov 24, 2025
efb21af
Apply generic in http authentication predication
raccoonback Nov 24, 2025
7eb0cb5
Add configurable maximum retry attempts for HTTP authentication
raccoonback Nov 25, 2025
5febca9
Extract authentication retry configuration to method
raccoonback Nov 26, 2025
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
125 changes: 125 additions & 0 deletions docs/modules/ROOT/pages/http-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -777,3 +777,128 @@ 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-authentication]]
== HTTP Authentication
Reactor Netty `HttpClient` provides a flexible HTTP authentication framework that allows you to implement
custom authentication mechanisms such as SPNEGO/Negotiate, OAuth, Bearer tokens, or any other HTTP-based authentication scheme.

The framework provides two APIs for HTTP authentication:

* {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthentication-java.util.function.BiPredicate-java.util.function.BiConsumer-[`httpAuthentication(BiPredicate, BiConsumer)`] -
For authentication where credentials can be computed immediately without delay. The predicate determines when to retry with authentication. Defaults to 1 maximum retry attempt.
* {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthentication-java.util.function.BiPredicate-java.util.function.BiConsumer-int-[`httpAuthentication(BiPredicate, BiConsumer, int maxRetries)`] -
Same as above but allows configuring the maximum count of retry attempts.
* {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthenticationWhen-java.util.function.BiPredicate-java.util.function.BiFunction-[`httpAuthenticationWhen(BiPredicate, BiFunction)`] -
For authentication where credentials need to be fetched from external sources or require delayed computation. Returns a `Mono<Void>` to support deferred credential retrieval. Defaults to 1 maximum retry attempt.
* {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthenticationWhen-java.util.function.BiPredicate-java.util.function.BiFunction-int-[`httpAuthenticationWhen(BiPredicate, BiFunction, int maxRetries)`] -
Same as above but allows configuring the maximum count of retry attempts.

This approach gives you complete control over the authentication flow while Reactor Netty handles the retry mechanism.

=== How It Works

The typical HTTP authentication flow works as follows:

. The client sends an HTTP request to a protected resource.
. The server responds with an authentication challenge (e.g., `401 Unauthorized` with a `WWW-Authenticate` header).
. The authenticator function is invoked to add authentication credentials to the request.
. The request is retried with the authentication credentials.
. If authentication is successful, the server returns the requested resource.

=== Immediate Authentication with httpAuthentication

For authentication scenarios where credentials can be computed immediately without needing to fetch them from external sources,
use {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthentication-java.util.function.BiPredicate-java.util.function.BiConsumer-[`httpAuthentication(BiPredicate, BiConsumer)`].
This method requires both a predicate to determine when to retry and a consumer to add authentication headers.

==== Token-Based Authentication Example

The following example demonstrates how to implement Bearer token authentication with configurable retry count:

{examples-link}/authentication/token/Application.java
[%unbreakable]
----
include::{examples-dir}/authentication/token/Application.java[lines=18..51]
----
<1> The predicate checks for `401 Unauthorized` responses to trigger authentication retry.
<2> The authenticator adds the `Authorization` header with a Bearer token.
<3> Configures the maximum count of authentication retry attempts to 3.

==== Basic Authentication Example

{examples-link}/authentication/basic/Application.java
[%unbreakable]
----
include::{examples-dir}/authentication/basic/Application.java[lines=18..44]
----
<1> The predicate checks for `401 Unauthorized` responses to trigger authentication retry.
<2> The authenticator adds Basic authentication credentials to the `Authorization` header.
<3> Uses the default maximum retry count of 1.

=== Custom Authentication with httpAuthenticationWhen

When you need custom retry conditions (e.g., checking specific headers or status codes other than 401),
use the {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthenticationWhen-java.util.function.BiPredicate-java.util.function.BiFunction-[`httpAuthenticationWhen(BiPredicate, BiFunction)`] method.

==== SPNEGO/Negotiate Authentication Example

For SPNEGO (Kerberos) authentication, you can implement a custom authenticator using Java's GSS-API:

{examples-link}/authentication/spnego/Application.java
[%unbreakable]
----
include::{examples-dir}/authentication/spnego/Application.java[lines=18..70]
----
<1> Custom predicate checks for `401 Unauthorized` with `WWW-Authenticate: Negotiate` header.
<2> The authenticator generates a SPNEGO token using GSS-API and adds it to the `Authorization` header.
<3> Configures the maximum count of authentication retry attempts to 2.

NOTE: For SPNEGO authentication, you need to configure Kerberos settings (e.g., `krb5.conf`) and JAAS configuration
(e.g., `jaas.conf`) appropriately. Set the system properties `java.security.krb5.conf` and `java.security.auth.login.config`
to point to your configuration files.

=== Custom Authentication Scenarios

The authentication framework is flexible enough to support various authentication scenarios:

==== OAuth 2.0 Authentication
[source,java]
----
HttpClient client = HttpClient.create()
.httpAuthenticationWhen(
(req, res) -> res.status().code() == 401, // <1>
(req, addr) -> {
return fetchOAuthToken() // <2>
.doOnNext(token ->
req.header(HttpHeaderNames.AUTHORIZATION, "Bearer " + token))
.then();
}
);
----
<1> Retry authentication on `401 Unauthorized` responses.
<2> Asynchronously fetch an OAuth token and add it to the request.

==== Proxy Authentication
[source,java]
----
HttpClient client = HttpClient.create()
.httpAuthenticationWhen(
(req, res) -> res.status().code() == 407, // <1>
(req, addr) -> {
String proxyCredentials = generateProxyCredentials();
req.header("Proxy-Authorization", "Bearer " + proxyCredentials);
return Mono.empty();
}
);
----
<1> Custom predicate checks for `407 Proxy Authentication Required` status code.

=== Important Notes

* The authenticator function is invoked only when the predicate returns `true` (indicating authentication is needed).
* For `httpAuthentication`, use a `BiConsumer` when credentials can be computed immediately without delay. No `Mono` is needed as headers are added directly.
* For `httpAuthenticationWhen`, use a `BiFunction` that returns `Mono<Void>` when credentials need to be fetched from external sources or require delayed computation.
* The authenticator receives the request and remote address, allowing you to customize authentication based on the target server.
* By default, authentication is retried once per request. You can configure the maximum count of retry attempts using the `maxRetries` parameter. If authentication fails after all retry attempts are exhausted, the error is propagated to the caller.
* For security reasons, ensure that sensitive credentials are not logged or exposed in error messages.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.authentication.basic;

import io.netty.handler.codec.http.HttpHeaderNames;
import reactor.netty.http.client.HttpClient;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Application {

public static void main(String[] args) {
HttpClient client =
HttpClient.create()
.httpAuthentication(
(req, res) -> res.status().code() == 401, // <1>
(req, addr) -> { // <2>
String credentials = "username:password";
String encodedCredentials = Base64.getEncoder()
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
req.header(HttpHeaderNames.AUTHORIZATION, "Basic " + encodedCredentials);
} // <3>
);

client.get()
.uri("https://example.com/")
.response()
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.authentication.spnego;

import io.netty.handler.codec.http.HttpHeaderNames;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

import java.net.InetSocketAddress;
import java.util.Base64;

public class Application {

public static void main(String[] args) {
HttpClient client =
HttpClient.create()
.httpAuthenticationWhen(
(req, res) -> res.status().code() == 401 && // <1>
res.responseHeaders().contains("WWW-Authenticate", "Negotiate", true),
(req, addr) -> { // <2>
try {
GSSManager manager = GSSManager.getInstance();
String hostName = ((InetSocketAddress) addr).getHostString();
String serviceName = "HTTP@" + hostName;
GSSName serverName = manager.createName(serviceName, GSSName.NT_HOSTBASED_SERVICE);

Oid krb5Mechanism = new Oid("1.2.840.113554.1.2.2");
GSSContext context = manager.createContext(
serverName, krb5Mechanism, null, GSSContext.DEFAULT_LIFETIME);

byte[] token = context.initSecContext(new byte[0], 0, 0);
String encodedToken = Base64.getEncoder().encodeToString(token);

req.header(HttpHeaderNames.AUTHORIZATION, "Negotiate " + encodedToken);

context.dispose();
}
catch (GSSException e) {
return Mono.error(new RuntimeException(
"Failed to generate SPNEGO token", e));
}
return Mono.empty();
},
2 // <3>
);

client.get()
.uri("https://example.com/")
.response()
.block();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.authentication.token;

import io.netty.handler.codec.http.HttpHeaderNames;
import reactor.netty.http.client.HttpClient;

import java.net.SocketAddress;

public class Application {

public static void main(String[] args) {
HttpClient client =
HttpClient.create()
.httpAuthentication(
(req, res) -> res.status().code() == 401, // <1>
(req, addr) -> { // <2>
String token = generateAuthToken(addr);
req.header(HttpHeaderNames.AUTHORIZATION, "Bearer " + token);
},
3 // <3>
);

client.get()
.uri("https://example.com/")
.response()
.block();
}

/**
* 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.
// In a real application, implement token generation/retrieval logic
return "sample-token-123";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ else if (msg instanceof HttpResponse) {

setNettyResponse(response);

if (notRedirected(response)) {
if (notRedirected(response) && authenticationNotRequired()) {
try {
HttpResponseStatus status = response.status();
if (!HttpResponseStatus.OK.equals(status)) {
Expand All @@ -131,9 +131,12 @@ else if (msg instanceof HttpResponse) {
}
}
else {
// Deliberately suppress "NullAway"
// redirecting != null in this case
listener().onUncaughtException(this, redirecting);
if (redirecting != null) {
listener().onUncaughtException(this, redirecting);
}
else if (authenticating != null) {
listener().onUncaughtException(this, authenticating);
}
}
}
else {
Expand Down
Loading
Loading