org.openjdk.jmh
jmh-core
diff --git a/src/docs/asciidoc/advanced-topics.adoc b/src/docs/asciidoc/advanced-topics.adoc
index 0c338657b9..ba2254dc83 100644
--- a/src/docs/asciidoc/advanced-topics.adoc
+++ b/src/docs/asciidoc/advanced-topics.adoc
@@ -86,6 +86,31 @@ A defined set of values shared across the messages is a good candidate: geograph
Cardinality of filter values can be from a few to a few thousands.
Extreme cardinality (a couple or dozens of thousands) can make filtering less efficient.
+=== OAuth 2 Support
+
+The client can authenticate against an OAuth 2 server like https://github.com/cloudfoundry/uaa[UAA].
+It uses the https://tools.ietf.org/html/rfc6749#section-4.4[OAuth 2 Client Credentials flow].
+The https://www.rabbitmq.com/docs/oauth2[OAuth 2 plugin] must be enabled on the server side and configured to use the same OAuth 2 server as the client.
+
+How to retrieve the OAuth 2 token is configured at the environment level:
+
+.Configuring OAuth 2 token retrieval
+[source,java,indent=0]
+--------
+include::{test-examples}/EnvironmentUsage.java[tag=oauth2]
+--------
+<1> Access the OAuth 2 configuration
+<2> Set the token endpoint URI
+<3> Authenticate the client application
+<4> Set the grant type
+<5> Set optional parameters (depends on the OAuth 2 server)
+<6> Set the SSL context (e.g. to verify and trust the identity of the OAuth 2 server)
+
+The environment retrieves tokens and uses them to create stream connections.
+It also takes care of refreshing the tokens before they expire and of re-authenticating existing connections so the broker does not close them when their token expires.
+
+The environment uses the same token for all the connections it maintains.
+
=== Using Native `epoll`
The stream Java client uses the https://netty.io/[Netty] network framework and its Java NIO transport implementation by default.
diff --git a/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java b/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
index 4c8a5239f5..6eb1506f8b 100644
--- a/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
+++ b/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
@@ -31,6 +31,7 @@
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
+import javax.net.ssl.SSLContext;
/**
* API to configure and create an {@link Environment}.
@@ -517,4 +518,90 @@ interface NettyConfiguration {
*/
EnvironmentBuilder environmentBuilder();
}
+
+ /**
+ * OAuth 2 settings.
+ *
+ * @return OAuth 2 settings
+ * @see OAuth2Configuration
+ * @since 1.3.0
+ */
+ OAuth2Configuration oauth2();
+
+ /**
+ * Configuration to retrieve a token using the OAuth 2 Client Credentials flow.
+ *
+ * @since 1.3.0
+ */
+ interface OAuth2Configuration {
+
+ /**
+ * Set the URI to access to get the token.
+ *
+ * TLS is supported by providing a HTTPS
URI and setting a {@link
+ * javax.net.ssl.SSLContext}. See {@link #tls()} for more information. Applications in
+ * production should always use HTTPS to retrieve tokens.
+ *
+ * @param uri access URI
+ * @return OAuth 2 configuration
+ * @see #sslContext(javax.net.ssl.SSLContext)
+ */
+ OAuth2Configuration tokenEndpointUri(String uri);
+
+ /**
+ * Set the OAuth 2 client ID
+ *
+ *
The client ID usually identifies the application that requests a token.
+ *
+ * @param clientId client ID
+ * @return OAuth 2 configuration
+ */
+ OAuth2Configuration clientId(String clientId);
+
+ /**
+ * Set the secret (password) to use to get a token.
+ *
+ * @param clientSecret client secret
+ * @return OAuth 2 configuration
+ */
+ OAuth2Configuration clientSecret(String clientSecret);
+
+ /**
+ * Set the grant type to use when requesting the token.
+ *
+ *
The default is client_credentials
, but some OAuth 2 servers can use
+ * non-standard grant types to request tokens with extra-information.
+ *
+ * @param grantType grant type
+ * @return OAuth 2 configuration
+ */
+ OAuth2Configuration grantType(String grantType);
+
+ /**
+ * Set a parameter to pass in the request.
+ *
+ *
The OAuth 2 server may require extra parameters to narrow down the identity of the user.
+ *
+ * @param name name of the parameter
+ * @param value value of the parameter
+ * @return OAuth 2 configuration
+ */
+ OAuth2Configuration parameter(String name, String value);
+
+ /**
+ * {@link javax.net.ssl.SSLContext} for HTTPS requests.
+ *
+ * @param sslContext the SSL context
+ * @return OAuth 2 configuration
+ */
+ OAuth2Configuration sslContext(SSLContext sslContext);
+
+ /**
+ * Go back to the environment builder
+ *
+ * @return the environment builder
+ */
+ EnvironmentBuilder environmentBuilder();
+ }
}
diff --git a/src/main/java/com/rabbitmq/stream/impl/Client.java b/src/main/java/com/rabbitmq/stream/impl/Client.java
index 892e7553fb..56ac077aaf 100644
--- a/src/main/java/com/rabbitmq/stream/impl/Client.java
+++ b/src/main/java/com/rabbitmq/stream/impl/Client.java
@@ -48,6 +48,8 @@
import com.rabbitmq.stream.impl.ServerFrameHandler.FrameHandlerInfo;
import com.rabbitmq.stream.metrics.MetricsCollector;
import com.rabbitmq.stream.metrics.NoOpMetricsCollector;
+import com.rabbitmq.stream.oauth2.CredentialsManager;
+import com.rabbitmq.stream.oauth2.CredentialsManager.Registration;
import com.rabbitmq.stream.sasl.CredentialsProvider;
import com.rabbitmq.stream.sasl.DefaultSaslConfiguration;
import com.rabbitmq.stream.sasl.DefaultUsernamePasswordCredentialsProvider;
@@ -115,6 +117,7 @@
*/
public class Client implements AutoCloseable {
+ private static final AtomicLong ID_SEQUENCE = new AtomicLong(0);
private static final Charset CHARSET = StandardCharsets.UTF_8;
public static final int DEFAULT_PORT = 5552;
public static final int DEFAULT_TLS_PORT = 5551;
@@ -170,7 +173,6 @@ public long applyAsLong(Object value) {
};
private final AtomicInteger correlationSequence = new AtomicInteger(0);
private final SaslConfiguration saslConfiguration;
- private final CredentialsProvider credentialsProvider;
private final Runnable nettyClosing;
private final int maxFrameSize;
private final boolean frameSizeCapped;
@@ -190,6 +192,7 @@ public long applyAsLong(Object value) {
private final Runnable streamStatsCommandVersionsCheck;
private final boolean filteringSupported;
private final Runnable superStreamManagementCommandVersionsCheck;
+ private final Registration credentialsRegistration;
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
public Client() {
@@ -206,7 +209,6 @@ public Client(ClientParameters parameters) {
this.creditNotification = parameters.creditNotification;
this.codec = parameters.codec == null ? Codecs.DEFAULT : parameters.codec;
this.saslConfiguration = parameters.saslConfiguration;
- this.credentialsProvider = parameters.credentialsProvider;
this.chunkChecksum = parameters.chunkChecksum;
this.metricsCollector = parameters.metricsCollector;
this.metadataListener = parameters.metadataListener;
@@ -381,8 +383,36 @@ public void initChannel(SocketChannel ch) {
debug(() -> "starting SASL handshake");
this.saslMechanisms = getSaslMechanisms();
debug(() -> "SASL mechanisms supported by server ({})", this.saslMechanisms);
+
+ CredentialsProvider credentialsProvider = parameters.credentialsProvider;
+ CredentialsManager credentialsManager = parameters.credentialsManager;
+ CredentialsManager.AuthenticationCallback authCallback, renewCallback;
+ String regName =
+ clientConnectionName.isBlank()
+ ? String.valueOf(ID_SEQUENCE.getAndIncrement())
+ : clientConnectionName + "-" + ID_SEQUENCE.getAndIncrement();
+ if (credentialsManager == null) {
+ this.credentialsRegistration = CredentialsManagerFactory.get();
+ authCallback = (u, p) -> this.authenticate(credentialsProvider);
+ } else {
+ renewCallback =
+ authCallback =
+ (u, p) -> {
+ if (u == null && p == null) {
+ // no username/password provided by the credentials manager
+ // using the credentials manager
+ this.authenticate(credentialsProvider);
+ } else {
+ // the credentials manager provides username/password (e.g. after token
+ // retrieval)
+ // we use them with a one-time credentials provider
+ this.authenticate(new DefaultUsernamePasswordCredentialsProvider(u, p));
+ }
+ };
+ this.credentialsRegistration = credentialsManager.register(regName, renewCallback);
+ }
debug(() -> "starting authentication");
- authenticate(this.credentialsProvider);
+ this.credentialsRegistration.connect(authCallback);
debug(() -> "authenticated");
this.tuneState.await(Duration.ofSeconds(10));
this.maxFrameSize = this.tuneState.getMaxFrameSize();
@@ -1454,6 +1484,9 @@ void closingSequence(ShutdownContext.ShutdownReason reason) {
if (reason != null) {
this.shutdownListenerCallback.accept(reason);
}
+ if (this.credentialsRegistration != null) {
+ this.credentialsRegistration.close();
+ }
this.nettyClosing.run();
this.failOutstandingRequests();
if (this.closeDispatchingExecutorService != null) {
@@ -2378,6 +2411,10 @@ String label() {
}
}
+ static ClientParameters cp() {
+ return new ClientParameters();
+ }
+
public static class ClientParameters {
private final Map clientProperties = new ConcurrentHashMap<>();
@@ -2410,6 +2447,7 @@ public static class ClientParameters {
private SaslConfiguration saslConfiguration = DefaultSaslConfiguration.PLAIN;
private CredentialsProvider credentialsProvider =
new DefaultUsernamePasswordCredentialsProvider(DEFAULT_USERNAME, "guest");
+ private CredentialsManager credentialsManager;
private ChunkChecksum chunkChecksum = JdkChunkChecksum.CRC32_SINGLETON;
private MetricsCollector metricsCollector = NoOpMetricsCollector.SINGLETON;
private SslContext sslContext;
@@ -2492,6 +2530,11 @@ public ClientParameters credentialsProvider(CredentialsProvider credentialsProvi
return this;
}
+ public ClientParameters credentialsManager(CredentialsManager credentialsManager) {
+ this.credentialsManager = credentialsManager;
+ return this;
+ }
+
public ClientParameters username(String username) {
if (this.credentialsProvider instanceof UsernamePasswordCredentialsProvider) {
this.credentialsProvider =
diff --git a/src/main/java/com/rabbitmq/stream/impl/ConsumersCoordinator.java b/src/main/java/com/rabbitmq/stream/impl/ConsumersCoordinator.java
index 05a9ae00c1..fcff1116f3 100644
--- a/src/main/java/com/rabbitmq/stream/impl/ConsumersCoordinator.java
+++ b/src/main/java/com/rabbitmq/stream/impl/ConsumersCoordinator.java
@@ -506,6 +506,18 @@ SubscriptionState state() {
return this.state.get();
}
+ private void markConsuming() {
+ if (this.consumer != null) {
+ this.consumer.consuming();
+ }
+ }
+
+ private void markNotConsuming() {
+ if (this.consumer != null) {
+ this.consumer.notConsuming();
+ }
+ }
+
String label() {
return String.format(
"[id=%d, stream=%s, name=%s, consumer=%d]",
@@ -700,6 +712,7 @@ private ClientSubscriptionsManager(
"Subscription connection has {} consumer(s) over {} stream(s) to recover",
this.subscriptionTrackers.stream().filter(Objects::nonNull).count(),
this.streamToStreamSubscriptions.size());
+ iterate(this.subscriptionTrackers, SubscriptionTracker::markNotConsuming);
environment
.scheduledExecutorService()
.execute(
@@ -774,6 +787,7 @@ private ClientSubscriptionsManager(
}
if (affectedSubscriptions != null && !affectedSubscriptions.isEmpty()) {
+ iterate(affectedSubscriptions, SubscriptionTracker::markNotConsuming);
environment
.scheduledExecutorService()
.execute(
@@ -1132,6 +1146,7 @@ void add(
throw e;
}
subscriptionTracker.state(SubscriptionState.ACTIVE);
+ subscriptionTracker.markConsuming();
LOGGER.debug("Subscribed to '{}'", subscriptionTracker.stream);
} finally {
this.subscriptionManagerLock.unlock();
@@ -1397,4 +1412,13 @@ static Broker pickBroker(
Function, Broker> picker, Collection candidates) {
return picker.apply(keepReplicasIfPossible(candidates));
}
+
+ private static void iterate(
+ Collection l, java.util.function.Consumer c) {
+ for (SubscriptionTracker tracker : l) {
+ if (tracker != null) {
+ c.accept(tracker);
+ }
+ }
+ }
}
diff --git a/src/main/java/com/rabbitmq/stream/impl/CredentialsManagerFactory.java b/src/main/java/com/rabbitmq/stream/impl/CredentialsManagerFactory.java
new file mode 100644
index 0000000000..4ffce36df7
--- /dev/null
+++ b/src/main/java/com/rabbitmq/stream/impl/CredentialsManagerFactory.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2025 Broadcom. All Rights Reserved.
+// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+//
+// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
+// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
+// For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL,
+// please see LICENSE-APACHE2.
+//
+// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
+// either express or implied. See the LICENSE file for specific language governing
+// rights and limitations of this software.
+//
+// If you have any questions regarding licensing, please contact us at
+// info@rabbitmq.com.
+package com.rabbitmq.stream.impl;
+
+import com.rabbitmq.stream.impl.StreamEnvironmentBuilder.DefaultOAuth2Configuration;
+import com.rabbitmq.stream.oauth2.CredentialsManager;
+import com.rabbitmq.stream.oauth2.GsonTokenParser;
+import com.rabbitmq.stream.oauth2.HttpTokenRequester;
+import com.rabbitmq.stream.oauth2.TokenCredentialsManager;
+import java.net.http.HttpClient;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+import javax.net.ssl.SSLContext;
+
+final class CredentialsManagerFactory {
+
+ private static final CredentialsManager.Registration CALLBACK_DELEGATING_REGISTRATION =
+ new CredentialsManager.Registration() {
+ @Override
+ public void connect(CredentialsManager.AuthenticationCallback callback) {
+ callback.authenticate(null, null);
+ }
+
+ @Override
+ public void close() {}
+ };
+
+ private static final CredentialsManager CREDENTIALS_MANAGER =
+ (name, updateCallback) -> CALLBACK_DELEGATING_REGISTRATION;
+
+ static CredentialsManager get(
+ DefaultOAuth2Configuration oauth2, ScheduledExecutorService scheduledExecutorService) {
+ if (oauth2 != null && oauth2.enabled()) {
+ Consumer clientBuilderConsumer;
+ if (oauth2.tlsEnabled()) {
+ SSLContext sslContext = oauth2.sslContext();
+ clientBuilderConsumer = b -> b.sslContext(sslContext);
+ } else {
+ clientBuilderConsumer = ignored -> {};
+ }
+ HttpTokenRequester tokenRequester =
+ new HttpTokenRequester(
+ oauth2.tokenEndpointUri(),
+ oauth2.clientId(),
+ oauth2.clientSecret(),
+ oauth2.grantType(),
+ oauth2.parameters(),
+ clientBuilderConsumer,
+ null,
+ new GsonTokenParser());
+ return new TokenCredentialsManager(
+ tokenRequester, scheduledExecutorService, oauth2.refreshDelayStrategy());
+ } else {
+ return CREDENTIALS_MANAGER;
+ }
+ }
+
+ static CredentialsManager.Registration get() {
+ return CALLBACK_DELEGATING_REGISTRATION;
+ }
+}
diff --git a/src/main/java/com/rabbitmq/stream/impl/StreamConsumer.java b/src/main/java/com/rabbitmq/stream/impl/StreamConsumer.java
index 11c57e4517..1c868002e6 100644
--- a/src/main/java/com/rabbitmq/stream/impl/StreamConsumer.java
+++ b/src/main/java/com/rabbitmq/stream/impl/StreamConsumer.java
@@ -64,6 +64,7 @@ class StreamConsumer implements Consumer {
private final boolean sac;
private final OffsetSpecification initialOffsetSpecification;
private final Lock lock = new ReentrantLock();
+ private volatile boolean consuming;
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
StreamConsumer(
@@ -249,6 +250,7 @@ class StreamConsumer implements Consumer {
this.closed.set(true);
throw e;
}
+ this.consuming = true;
}
static OffsetSpecification getStoredOffset(
@@ -605,4 +607,16 @@ String subscriptionConnectionName() {
return client.clientConnectionName();
}
}
+
+ void notConsuming() {
+ this.consuming = false;
+ }
+
+ void consuming() {
+ this.consuming = true;
+ }
+
+ boolean isConsuming() {
+ return this.consuming;
+ }
}
diff --git a/src/main/java/com/rabbitmq/stream/impl/StreamEnvironment.java b/src/main/java/com/rabbitmq/stream/impl/StreamEnvironment.java
index ad41eccbaf..292dd8ff02 100644
--- a/src/main/java/com/rabbitmq/stream/impl/StreamEnvironment.java
+++ b/src/main/java/com/rabbitmq/stream/impl/StreamEnvironment.java
@@ -31,8 +31,10 @@
import com.rabbitmq.stream.impl.Client.StreamStatsResponse;
import com.rabbitmq.stream.impl.OffsetTrackingCoordinator.Registration;
import com.rabbitmq.stream.impl.StreamConsumerBuilder.TrackingConfiguration;
+import com.rabbitmq.stream.impl.StreamEnvironmentBuilder.DefaultOAuth2Configuration;
import com.rabbitmq.stream.impl.StreamEnvironmentBuilder.DefaultTlsConfiguration;
import com.rabbitmq.stream.impl.Utils.ClientConnectionType;
+import com.rabbitmq.stream.oauth2.CredentialsManager;
import com.rabbitmq.stream.sasl.CredentialsProvider;
import com.rabbitmq.stream.sasl.UsernamePasswordCredentialsProvider;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -103,6 +105,7 @@ class StreamEnvironment implements Environment {
int maxTrackingConsumersByConnection,
int maxConsumersByConnection,
DefaultTlsConfiguration tlsConfiguration,
+ DefaultOAuth2Configuration oauth,
ByteBufAllocator byteBufAllocator,
boolean lazyInit,
Function connectionNamingStrategy,
@@ -158,6 +161,7 @@ class StreamEnvironment implements Environment {
}
AddressResolver addressResolverToUse = addressResolver;
+ // trying to detect development environment
if (this.addresses.size() == 1
&& "localhost".equals(this.addresses.get(0).host())
&& addressResolver == DEFAULT_ADDRESS_RESOLVER) {
@@ -211,18 +215,6 @@ class StreamEnvironment implements Environment {
this.addresses.size(), 1, "rabbitmq-stream-locator-connection-");
shutdownService.wrap(this.executorServiceFactory::close);
- if (clientParametersPrototype.eventLoopGroup == null) {
- this.eventLoopGroup = Utils.eventLoopGroup();
- shutdownService.wrap(() -> closeEventLoopGroup(this.eventLoopGroup));
- this.clientParametersPrototype =
- clientParametersPrototype.duplicate().eventLoopGroup(this.eventLoopGroup);
- } else {
- this.eventLoopGroup = null;
- this.clientParametersPrototype =
- clientParametersPrototype
- .duplicate()
- .eventLoopGroup(clientParametersPrototype.eventLoopGroup);
- }
ScheduledExecutorService executorService;
if (scheduledExecutorService == null) {
int threads = AVAILABLE_PROCESSORS;
@@ -237,6 +229,25 @@ class StreamEnvironment implements Environment {
}
this.scheduledExecutorService = executorService;
+ CredentialsManager credentialsManager =
+ CredentialsManagerFactory.get(oauth, this.scheduledExecutorService);
+
+ clientParametersPrototype =
+ clientParametersPrototype.duplicate().credentialsManager(credentialsManager);
+
+ if (clientParametersPrototype.eventLoopGroup == null) {
+ this.eventLoopGroup = Utils.eventLoopGroup();
+ shutdownService.wrap(() -> closeEventLoopGroup(this.eventLoopGroup));
+ this.clientParametersPrototype =
+ clientParametersPrototype.duplicate().eventLoopGroup(this.eventLoopGroup);
+ } else {
+ this.eventLoopGroup = null;
+ this.clientParametersPrototype =
+ clientParametersPrototype
+ .duplicate()
+ .eventLoopGroup(clientParametersPrototype.eventLoopGroup);
+ }
+
this.producersCoordinator =
new ProducersCoordinator(
this,
diff --git a/src/main/java/com/rabbitmq/stream/impl/StreamEnvironmentBuilder.java b/src/main/java/com/rabbitmq/stream/impl/StreamEnvironmentBuilder.java
index 1be66aa36b..026df49a49 100644
--- a/src/main/java/com/rabbitmq/stream/impl/StreamEnvironmentBuilder.java
+++ b/src/main/java/com/rabbitmq/stream/impl/StreamEnvironmentBuilder.java
@@ -21,7 +21,9 @@
import com.rabbitmq.stream.compression.CompressionCodecFactory;
import com.rabbitmq.stream.impl.Utils.ClientConnectionType;
import com.rabbitmq.stream.metrics.MetricsCollector;
+import com.rabbitmq.stream.oauth2.TokenCredentialsManager;
import com.rabbitmq.stream.sasl.CredentialsProvider;
+import com.rabbitmq.stream.sasl.DefaultSaslConfiguration;
import com.rabbitmq.stream.sasl.SaslConfiguration;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator;
@@ -32,13 +34,16 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
+import java.time.Instant;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,6 +56,7 @@ public class StreamEnvironmentBuilder implements EnvironmentBuilder {
private final Client.ClientParameters clientParameters = new Client.ClientParameters();
private final DefaultTlsConfiguration tls = new DefaultTlsConfiguration(this);
private final DefaultNettyConfiguration netty = new DefaultNettyConfiguration(this);
+ private final DefaultOAuth2Configuration oauth2 = new DefaultOAuth2Configuration(this);
private ScheduledExecutorService scheduledExecutorService;
private List uris = Collections.emptyList();
private BackOffDelayPolicy recoveryBackOffDelayPolicy =
@@ -295,6 +301,11 @@ public NettyConfiguration netty() {
return this.netty;
}
+ @Override
+ public OAuth2Configuration oauth2() {
+ return this.oauth2;
+ }
+
StreamEnvironmentBuilder clientFactory(Function clientFactory) {
this.clientFactory = clientFactory;
return this;
@@ -348,6 +359,7 @@ public Environment build() {
maxTrackingConsumersByConnection,
maxConsumersByConnection,
tls,
+ oauth2,
netty.byteBufAllocator,
lazyInit,
connectionNamingStrategy,
@@ -459,4 +471,110 @@ public EnvironmentBuilder environmentBuilder() {
return this.environmentBuilder;
}
}
+
+ static class DefaultOAuth2Configuration implements OAuth2Configuration {
+
+ private final EnvironmentBuilder builder;
+ private final Map parameters = new HashMap<>();
+ private String tokenEndpointUri;
+ private String clientId;
+ private String clientSecret;
+ private String grantType = "client_credentials";
+ private Function refreshDelayStrategy =
+ TokenCredentialsManager.DEFAULT_REFRESH_DELAY_STRATEGY;
+ private SSLContext sslContext;
+
+ DefaultOAuth2Configuration(StreamEnvironmentBuilder builder) {
+ this.builder = builder;
+ }
+
+ @Override
+ public OAuth2Configuration tokenEndpointUri(String uri) {
+ this.builder.saslConfiguration(DefaultSaslConfiguration.PLAIN);
+ this.builder.credentialsProvider(null);
+ this.tokenEndpointUri = uri;
+ return this;
+ }
+
+ @Override
+ public OAuth2Configuration clientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ @Override
+ public OAuth2Configuration clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ @Override
+ public OAuth2Configuration grantType(String grantType) {
+ this.grantType = grantType;
+ return this;
+ }
+
+ @Override
+ public OAuth2Configuration parameter(String name, String value) {
+ if (value == null) {
+ this.parameters.remove(name);
+ } else {
+ this.parameters.put(name, value);
+ }
+ return this;
+ }
+
+ @Override
+ public OAuth2Configuration sslContext(SSLContext sslContext) {
+ this.sslContext = sslContext;
+ return this;
+ }
+
+ @Override
+ public EnvironmentBuilder environmentBuilder() {
+ return this.builder;
+ }
+
+ DefaultOAuth2Configuration refreshDelayStrategy(
+ Function refreshDelayStrategy) {
+ this.refreshDelayStrategy = refreshDelayStrategy;
+ return this;
+ }
+
+ Function refreshDelayStrategy() {
+ return this.refreshDelayStrategy;
+ }
+
+ String tokenEndpointUri() {
+ return this.tokenEndpointUri;
+ }
+
+ String clientId() {
+ return this.clientId;
+ }
+
+ String clientSecret() {
+ return this.clientSecret;
+ }
+
+ String grantType() {
+ return this.grantType;
+ }
+
+ Map parameters() {
+ return Map.copyOf(this.parameters);
+ }
+
+ SSLContext sslContext() {
+ return this.sslContext;
+ }
+
+ boolean enabled() {
+ return this.tokenEndpointUri != null;
+ }
+
+ boolean tlsEnabled() {
+ return this.sslContext != null;
+ }
+ }
}
diff --git a/src/main/java/com/rabbitmq/stream/oauth2/CredentialsManager.java b/src/main/java/com/rabbitmq/stream/oauth2/CredentialsManager.java
new file mode 100644
index 0000000000..bdc340f037
--- /dev/null
+++ b/src/main/java/com/rabbitmq/stream/oauth2/CredentialsManager.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
+// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+//
+// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
+// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
+// For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL,
+// please see LICENSE-APACHE2.
+//
+// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
+// either express or implied. See the LICENSE file for specific language governing
+// rights and limitations of this software.
+//
+// If you have any questions regarding licensing, please contact us at
+// info@rabbitmq.com.
+package com.rabbitmq.stream.oauth2;
+
+/**
+ * Contract to authenticate and possibly re-authenticate application components.
+ *
+ * A typical "application component" is a connection.
+ */
+public interface CredentialsManager {
+
+ /** No-op credentials manager. */
+ CredentialsManager NO_OP = new NoOpCredentialsManager();
+
+ /**
+ * Register a component for authentication.
+ *
+ * @param name component name (must be unique)
+ * @param updateCallback callback to update the component authentication
+ * @return the registration (must be closed when no longer necessary)
+ */
+ Registration register(String name, AuthenticationCallback updateCallback);
+
+ /** A component registration. */
+ interface Registration extends AutoCloseable {
+
+ /**
+ * Connection request from the component.
+ *
+ *
The component calls this method when it needs to authenticate. The underlying credentials
+ * manager implementation must take care of providing the component with the appropriate
+ * credentials in the callback.
+ *
+ * @param callback client code to authenticate the component
+ */
+ void connect(AuthenticationCallback callback);
+
+ /** Close the registration. */
+ void close();
+ }
+
+ /**
+ * Component authentication callback.
+ *
+ *
The component provides the logic and the manager implementation calls it with the
+ * appropriate credentials.
+ */
+ interface AuthenticationCallback {
+
+ /**
+ * Authentication logic.
+ *
+ * @param username username
+ * @param password password
+ */
+ void authenticate(String username, String password);
+ }
+
+ class NoOpCredentialsManager implements CredentialsManager {
+
+ @Override
+ public Registration register(String name, AuthenticationCallback updateCallback) {
+ return new NoOpRegistration();
+ }
+ }
+
+ class NoOpRegistration implements Registration {
+
+ @Override
+ public void connect(AuthenticationCallback callback) {}
+
+ @Override
+ public void close() {}
+ }
+}
diff --git a/src/main/java/com/rabbitmq/stream/oauth2/GsonTokenParser.java b/src/main/java/com/rabbitmq/stream/oauth2/GsonTokenParser.java
new file mode 100644
index 0000000000..47d060131d
--- /dev/null
+++ b/src/main/java/com/rabbitmq/stream/oauth2/GsonTokenParser.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
+// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
+//
+// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
+// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
+// For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL,
+// please see LICENSE-APACHE2.
+//
+// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
+// either express or implied. See the LICENSE file for specific language governing
+// rights and limitations of this software.
+//
+// If you have any questions regarding licensing, please contact us at
+// info@rabbitmq.com.
+package com.rabbitmq.stream.oauth2;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+
+/**
+ * Token parser for JSON OAuth 2 Access
+ * tokens.
+ *
+ *
Uses GSON for the JSON parsing.
+ */
+public class GsonTokenParser implements TokenParser {
+
+ private static final Gson GSON = new Gson();
+ private static final TypeToken