Skip to content
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
114 changes: 114 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,117 @@ 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.BiFunction-[`httpAuthentication(BiFunction)`] -
Automatically retries requests when the server returns `401 Unauthorized`.
* {javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthenticationWhen-java.util.function.BiPredicate-java.util.function.BiFunction-[`httpAuthenticationWhen(BiPredicate, BiFunction)`] -
Allows custom retry conditions based on request and response.

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.

=== Simple Authentication with httpAuthentication

For most authentication scenarios where you want to retry on `401 Unauthorized` responses, use the simpler
{javadoc}/reactor/netty/http/client/HttpClient.html#httpAuthentication-java.util.function.BiFunction-[`httpAuthentication(BiFunction)`] method.

==== Token-Based Authentication Example

The following example demonstrates how to implement Bearer token authentication:

{examples-link}/authentication/token/Application.java
[%unbreakable]
----
include::{examples-dir}/authentication/token/Application.java[lines=18..51]
----
<1> Automatically retries on `401 Unauthorized` responses.
<2> The authenticator adds the `Authorization` header with a Bearer token.

==== Basic Authentication Example

{examples-link}/authentication/basic/Application.java
[%unbreakable]
----
include::{examples-dir}/authentication/basic/Application.java[lines=18..45]
----
<1> Automatically retries on `401 Unauthorized` responses.
<2> The authenticator adds Basic authentication credentials to the `Authorization` header.

=== 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..69]
----
<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.

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()
.httpAuthentication(
(req, addr) -> {
return fetchOAuthToken() // <1>
.doOnNext(token ->
req.header(HttpHeaderNames.AUTHORIZATION, "Bearer " + token))
.then();
}
);
----
<1> 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 authentication is needed (on `401` for `httpAuthentication`, or when the predicate returns `true` for `httpAuthenticationWhen`).
* The authenticator receives the request and remote address, allowing you to customize authentication based on the target server.
* The authenticator returns a `Mono<Void>` which allows for asynchronous credential retrieval.
* Authentication is retried only once per request. If authentication fails after retry, 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,45 @@
/*
* 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.core.publisher.Mono;
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(// <1>
(req, addr) -> { // <2>
String credentials = "username:password";
String encodedCredentials = Base64.getEncoder()
.encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
req.header(HttpHeaderNames.AUTHORIZATION, "Basic " + encodedCredentials);
return Mono.empty();
}
);

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

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.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

import java.net.SocketAddress;

public class Application {

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

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) && notAuthenticated()) {
try {
HttpResponseStatus status = response.status();
if (!HttpResponseStatus.OK.equals(status)) {
Expand All @@ -131,9 +131,16 @@ else if (msg instanceof HttpResponse) {
}
}
else {
// Deliberately suppress "NullAway"
// redirecting != null in this case
listener().onUncaughtException(this, redirecting);
if (redirecting != null) {
// Deliberately suppress "NullAway"
// redirecting != null in this case
listener().onUncaughtException(this, redirecting);
}
else if (authenticating != null) {
// Deliberately suppress "NullAway"
// authenticating != null in this case
listener().onUncaughtException(this, authenticating);
}
}
}
else {
Expand Down
Loading
Loading