This is an automated email from the ASF dual-hosted git repository.

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 4fc1f0ac CASSSIDECAR-334: Add support for stateless JWT authentication 
using public keys (#247)
4fc1f0ac is described below

commit 4fc1f0ac7589280572cec063563331c0bdc0449b
Author: Isaac Reath <[email protected]>
AuthorDate: Mon Sep 29 13:22:16 2025 -0400

    CASSSIDECAR-334: Add support for stateless JWT authentication using public 
keys (#247)
    
    Patch by Isaac Reath; reviewed by Francisco Guerrero, Saranya Krishnakumar 
for CASSSIDECAR-334
---
 CHANGES.txt                                        |   1 +
 conf/sidecar.yaml                                  |  19 +++
 server/build.gradle                                |   1 +
 .../AuthenticationHandlerFactory.java              |   6 +-
 .../JwtAuthenticationHandlerFactory.java           |   8 +-
 .../acl/authentication/JwtParameterExtractor.java  |  94 +++++++++++++-
 .../sidecar/acl/authentication/JwtParameters.java  |  40 ++++++
 .../MutualTlsAuthenticationHandlerFactory.java     |   4 +-
 .../ReloadingJwtAuthenticationHandler.java         | 142 ++++++++++++++++++++-
 .../sidecar/metrics/server/AuthMetrics.java        |  48 +++++++
 .../sidecar/metrics/server/ServerMetrics.java      |   5 +
 .../sidecar/metrics/server/ServerMetricsImpl.java  |   8 ++
 .../cassandra/sidecar/modules/AuthModule.java      |   6 +-
 .../JWTAuthenticationHandlerFactoryTest.java       |   6 +-
 .../authentication/JwtParameterExtractorTest.java  |  61 +++++++++
 .../MutualTLSAuthenticationHandlerTest.java        |   8 +-
 .../MutualTlsAuthenticationHandlerFactoryTest.java |   6 +-
 .../ReloadingJwtAuthenticationHandlerTest.java     | 133 ++++++++++++++++++-
 18 files changed, 568 insertions(+), 28 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 0fb5a932..35e85a5d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.3.0
 -----
+ * Add support for stateless JWT authentication using public keys 
(CASSSIDECAR-334)
  * Improve FilteringMetricRegistry implementation (CASSSIDECAR-347)
  * Add lifecycle APIs for starting and stopping Cassandra (CASSSIDECAR-266)
  * Implementation of CassandraClusterSchemaMonitor (CASSSIDECAR-245)
diff --git a/conf/sidecar.yaml b/conf/sidecar.yaml
index b295af18..e6e6936a 100644
--- a/conf/sidecar.yaml
+++ b/conf/sidecar.yaml
@@ -258,6 +258,25 @@ access_control:
         # trying to connect.
         client_id: recognized_client_id
         config_discover_interval: 1h
+        #
+        # Authentication type dictates whether to use the Oauth flow or a 
stateless JWT authenticator
+        # If not provided, default is oauth. Valid values are oauth or 
stateless
+        # jwt_auth_type: oauth
+        #
+        # Optional configurations for connecting to JWT PEM provider
+        #
+        # keystore_path defines the path to a keystore which can be used to 
provide SSL certificates to
+        # a JWT pem provider. If specified, a password must be provided.
+        # keystore_path: /path/to/keystore
+        # keystore_password: changeme
+        #
+        # truststore_path defines the path to a truststore that contains the 
CA used to validate SSL certs used
+        # by downstream PEM provider services. If specified, a password must 
be provided.
+        # truststore_path: /path/to/truststore
+        # truststore_password: changeme
+        #
+        # Optional JWT which can be used to authenticate to a downstream PEM 
provider if required.
+        # pem_provider_jwt: changeme
   authorizer:
     # Authorization backend, implementing 
io.vertx.ext.auth.authorization.AuthorizationProvider; used to
     # provide permissions a user holds.
diff --git a/server/build.gradle b/server/build.gradle
index e4653dc2..74f36bdb 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -161,6 +161,7 @@ dependencies {
     testImplementation('org.mockito:mockito-core:4.10.0')
     testImplementation('org.mockito:mockito-inline:4.10.0')
     testImplementation("io.vertx:vertx-junit5:${project.vertxVersion}")
+    testImplementation("com.auth0:java-jwt:4.4.0")
     testImplementation(testFixtures(project(":client-common")))
     testImplementation(testFixtures(project(":server-common")))
     testImplementation(testFixtures(project(":test-common")))
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java
index a12094ca..23a530c8 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/AuthenticationHandlerFactory.java
@@ -24,6 +24,7 @@ import io.vertx.core.Vertx;
 import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal;
 import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 
 /**
  * Factory class for creating {@link AuthenticationHandlerInternal} instances.
@@ -39,10 +40,13 @@ public interface AuthenticationHandlerFactory
      * @param vertx                         instance of Vertx
      * @param accessControlConfiguration    Configuration for creating 
authentication handler
      * @param parameters                    Parameters for creating {@link 
AuthenticationHandlerInternal} implementation
+     * @param metrics                       Reference to the AuthMetrics which 
provide observability into authn / authz
+     *                                      operations.
      * @return a newly created instance of {@link 
AuthenticationHandlerInternal}.
      * @throws ConfigurationException if handler cannot be created
      */
     AuthenticationHandlerInternal create(Vertx vertx,
                                          AccessControlConfiguration 
accessControlConfiguration,
-                                         Map<String, String> parameters) 
throws ConfigurationException;
+                                         Map<String, String> parameters,
+                                         AuthMetrics metrics) throws 
ConfigurationException;
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java
index d14ca715..f6ba0a80 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtAuthenticationHandlerFactory.java
@@ -26,6 +26,7 @@ import io.vertx.core.Vertx;
 import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal;
 import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor;
 
 /**
@@ -51,14 +52,15 @@ public class JwtAuthenticationHandlerFactory implements 
AuthenticationHandlerFac
     @Override
     public AuthenticationHandlerInternal create(Vertx vertx,
                                                 AccessControlConfiguration 
accessControlConfiguration,
-                                                Map<String, String> 
parameters) throws ConfigurationException
+                                                Map<String, String> parameters,
+                                                AuthMetrics metrics) throws 
ConfigurationException
     {
         JwtParameters jwtParameters = parameterParser(parameters);
-
         return new ReloadingJwtAuthenticationHandler(vertx,
                                                      jwtParameters,
                                                      roleProcessor,
-                                                     periodicTaskExecutor);
+                                                     periodicTaskExecutor,
+                                                     metrics);
     }
 
     protected JwtParameters parameterParser(Map<String, String> parameters)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractor.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractor.java
index 3ab48603..95bb251a 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractor.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractor.java
@@ -22,6 +22,8 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -48,13 +50,29 @@ public class JwtParameterExtractor implements JwtParameters
     private static final String SCOPES_SUPPORTED_PARAM_KEY = 
"scopes_supported";
     private static final String CONFIG_DISCOVER_INTERVAL_PARAM_KEY = 
"config_discover_interval";
     private static final SecondBoundConfiguration 
DEFAULT_CONFIG_DISCOVER_INTERVAL
-    = SecondBoundConfiguration.parse("1h");
+            = SecondBoundConfiguration.parse("1h");
+    private static final String JWT_AUTH_TYPE_PARAM_KEY = "jwt_auth_type";
+    private static final String DEFAULT_JWT_AUTH_TYPE = "oauth";
+    private static final String KEYSTORE_PATH_KEY = "keystore_path";
+    private static final String KEYSTORE_PASSWORD_KEY = "keystore_password";
+    private static final String TRUSTSTORE_PATH_KEY = "truststore_path";
+    private static final String TRUSTSTORE_PASSWORD_KEY = 
"truststore_password";
+    private static final String PEM_PROVIDER_JWT_KEY = "pem_provider_jwt";
+    private static final Set<String> SUPPORTED_JWT_AUTH_TYPES = 
Arrays.stream(JwtParameters.AuthType.values())
+                                                                      
.map(authType -> authType.name().toLowerCase())
+                                                                      
.collect(Collectors.toSet());
 
     private final boolean enabled;
     private final String site;
     private final String clientId;
     private final SecondBoundConfiguration configDiscoverInterval;
     private final List<String> scopes;
+    private final AuthType jwtAuthType;
+    private String keystorePath;
+    private String keystorePassword;
+    private String truststorePath;
+    private String truststorePassword;
+    private String pemProviderJwt;
 
     public JwtParameterExtractor(Map<String, String> parameters)
     {
@@ -64,8 +82,14 @@ public class JwtParameterExtractor implements JwtParameters
         this.clientId = parameters.get(CLIENT_ID_PARAM_KEY);
         this.scopes = buildScopes(parameters);
         this.configDiscoverInterval = 
parameters.containsKey(CONFIG_DISCOVER_INTERVAL_PARAM_KEY)
-                                      ? 
SecondBoundConfiguration.parse(parameters.get(CONFIG_DISCOVER_INTERVAL_PARAM_KEY))
-                                      : DEFAULT_CONFIG_DISCOVER_INTERVAL;
+                                              ? 
SecondBoundConfiguration.parse(parameters.get(CONFIG_DISCOVER_INTERVAL_PARAM_KEY))
+                                              : 
DEFAULT_CONFIG_DISCOVER_INTERVAL;
+        this.jwtAuthType = 
JwtParameters.AuthType.valueOf(parameters.getOrDefault(JWT_AUTH_TYPE_PARAM_KEY, 
DEFAULT_JWT_AUTH_TYPE).toUpperCase());
+        this.keystorePath = parameters.getOrDefault(KEYSTORE_PATH_KEY, null);
+        this.keystorePassword = parameters.getOrDefault(KEYSTORE_PASSWORD_KEY, 
null);
+        this.truststorePath = parameters.getOrDefault(TRUSTSTORE_PATH_KEY, 
null);
+        this.truststorePassword = 
parameters.getOrDefault(TRUSTSTORE_PASSWORD_KEY, null);
+        this.pemProviderJwt = parameters.getOrDefault(PEM_PROVIDER_JWT_KEY, 
null);
     }
 
     @Override
@@ -98,15 +122,71 @@ public class JwtParameterExtractor implements JwtParameters
         return configDiscoverInterval;
     }
 
+    @Override
+    public AuthType jwtAuthType()
+    {
+        return jwtAuthType;
+    }
+
+    @Override
+    public String keystorePath()
+    {
+        return keystorePath;
+    }
+
+    @Override
+    public String keystorePassword()
+    {
+        return keystorePassword;
+    }
+
+    @Override
+    public String truststorePath()
+    {
+        return truststorePath;
+    }
+
+    @Override
+    public String truststorePassword()
+    {
+        return truststorePassword;
+    }
+
+    @Override
+    public String pemProviderJwt()
+    {
+        return pemProviderJwt;
+    }
+
     private void validate(Map<String, String> parameters)
     {
         if (parameters == null)
         {
             throw new IllegalArgumentException("JWT parameters can not be 
null");
         }
-
+        String configuredJwtAuthType = 
parameters.getOrDefault(JWT_AUTH_TYPE_PARAM_KEY, DEFAULT_JWT_AUTH_TYPE);
+        if (!SUPPORTED_JWT_AUTH_TYPES.contains(configuredJwtAuthType))
+        {
+            throw new IllegalArgumentException("Invalid JWT authentication 
type: " + configuredJwtAuthType +
+                                                       ". Supported types are: 
" + SUPPORTED_JWT_AUTH_TYPES);
+        }
         validateParameterPresence(parameters, SITE_PARAM_KEY);
-        validateParameterPresence(parameters, CLIENT_ID_PARAM_KEY);
+        if (AuthType.valueOf(configuredJwtAuthType.toUpperCase()) == 
AuthType.OAUTH)
+        {
+            validateParameterPresence(parameters, CLIENT_ID_PARAM_KEY);
+        }
+        if (AuthType.valueOf(configuredJwtAuthType.toUpperCase()) == 
AuthType.STATELESS)
+        {
+            if (parameters.containsKey(KEYSTORE_PATH_KEY))
+            {
+                validateParameterPresence(parameters, KEYSTORE_PASSWORD_KEY);
+            }
+
+            if (parameters.containsKey(TRUSTSTORE_PATH_KEY))
+            {
+                validateParameterPresence(parameters, TRUSTSTORE_PASSWORD_KEY);
+            }
+        }
     }
 
     private void validateParameterPresence(Map<String, String> parameters, 
String paramKey)
@@ -139,8 +219,8 @@ public class JwtParameterExtractor implements JwtParameters
         if (isNotEmpty(parameters.get(SCOPES_SUPPORTED_PARAM_KEY)))
         {
             String delimiter = 
isNotEmpty(parameters.get(SCOPE_SEPARATOR_PARAM_KEY))
-                               ? parameters.get(SCOPE_SEPARATOR_PARAM_KEY)
-                               : DEFAULT_SCOPE_SEPARATOR;
+                                       ? 
parameters.get(SCOPE_SEPARATOR_PARAM_KEY)
+                                       : DEFAULT_SCOPE_SEPARATOR;
             
scopes.addAll(Arrays.asList(parameters.get(SCOPES_SUPPORTED_PARAM_KEY).split(delimiter)));
         }
         return scopes;
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameters.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameters.java
index 9721db1b..00da7971 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameters.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameters.java
@@ -52,4 +52,44 @@ public interface JwtParameters
      * dynamically retrieve configuration information of an OpenID provider.
      */
     SecondBoundConfiguration configDiscoverInterval();
+
+    /**
+     * @return The configured method of JWT authentication to use. Defaults to 
oauth if not supplied.
+     */
+    AuthType jwtAuthType();
+
+    /**
+     * @return Optional path to a keystore to provide mutual TLS to PEM public 
key provider service
+     */
+    String keystorePath();
+
+    /**
+     * @return Optional password for the provided keystore
+     */
+    String keystorePassword();
+
+    /**
+     * @return Optional path to a truststore to validate SSL certs from PEM 
public key provider service
+     */
+    String truststorePath();
+
+    /**
+     * @return Optional password for the provided truststore
+     */
+    String truststorePassword();
+
+    /**
+     * @return Optional JWT to authenticate to PEM public key provider service
+     */
+    String pemProviderJwt();
+
+
+    /**
+     * Supported types of JWT authentication. Today Oauth and Stateless JWT 
token authentication methods are supported.
+     */
+    enum AuthType
+    {
+        OAUTH,
+        STATELESS
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java
index 22075bf8..18b91bef 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactory.java
@@ -33,6 +33,7 @@ import org.apache.cassandra.sidecar.acl.AdminIdentityResolver;
 import org.apache.cassandra.sidecar.acl.IdentityToRoleCache;
 import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 
 /**
  * {@link AuthenticationHandlerFactory} implementation for {@link 
MutualTlsAuthenticationHandler}
@@ -56,7 +57,8 @@ public class MutualTlsAuthenticationHandlerFactory implements 
AuthenticationHand
     @Override
     public AuthenticationHandlerInternal create(Vertx vertx,
                                                 AccessControlConfiguration 
accessControlConfiguration,
-                                                Map<String, String> 
parameters) throws ConfigurationException
+                                                Map<String, String> parameters,
+                                                AuthMetrics metrics) throws 
ConfigurationException
     {
         validate(parameters);
         try
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandler.java
index 7172cb24..60e8d488 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandler.java
@@ -21,6 +21,7 @@ package org.apache.cassandra.sidecar.acl.authentication;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
+import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -29,17 +30,30 @@ import io.vertx.core.Future;
 import io.vertx.core.Handler;
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
 import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.JksOptions;
+import io.vertx.ext.auth.PubSecKeyOptions;
 import io.vertx.ext.auth.User;
 import io.vertx.ext.auth.authentication.AuthenticationProvider;
+import io.vertx.ext.auth.jwt.JWTAuth;
+import io.vertx.ext.auth.jwt.JWTAuthOptions;
 import io.vertx.ext.auth.oauth2.OAuth2Options;
 import io.vertx.ext.auth.oauth2.providers.OpenIDConnectAuth;
 import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.client.HttpRequest;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
 import io.vertx.ext.web.handler.impl.AuthenticationHandlerImpl;
+import io.vertx.ext.web.handler.impl.AuthenticationHandlerInternal;
+import io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl;
 import io.vertx.ext.web.handler.impl.OAuth2AuthHandlerImpl;
 import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
+import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 import org.apache.cassandra.sidecar.tasks.PeriodicTask;
 import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor;
+import org.jetbrains.annotations.NotNull;
 
 import static 
io.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE;
 import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
@@ -58,36 +72,81 @@ extends 
AuthenticationHandlerImpl<ReloadingJwtAuthenticationHandler.NoOpAuthenti
 {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ReloadingJwtAuthenticationHandler.class);
 
-    private final AtomicReference<OAuth2AuthHandlerImpl> delegateHandler = new 
AtomicReference<>();
+    @VisibleForTesting
+    final AtomicReference<AuthenticationHandlerInternal> delegateHandler = new 
AtomicReference<>();
     private final Vertx vertx;
     private final JwtParameters jwtParameters;
     private final JwtRoleProcessor roleProcessor;
+    private final AuthMetrics metrics;
 
     public ReloadingJwtAuthenticationHandler(Vertx vertx,
                                              JwtParameters jwtParameters,
                                              JwtRoleProcessor roleProcessor,
-                                             PeriodicTaskExecutor 
periodicTaskExecutor)
+                                             PeriodicTaskExecutor 
periodicTaskExecutor,
+                                             AuthMetrics metrics)
     {
         super(NoOpAuthenticationProvider.INSTANCE);
         this.vertx = vertx;
         this.jwtParameters = jwtParameters;
         this.roleProcessor = roleProcessor;
+        this.metrics = metrics;
+        if 
(jwtParameters.jwtAuthType().equals(JwtParameters.AuthType.STATELESS))
+        {
+            
periodicTaskExecutor.schedule(buildPeriodicStatelessJwtRefreshTask(vertx, 
jwtParameters));
+        }
+        else if 
(jwtParameters.jwtAuthType().equals(JwtParameters.AuthType.OAUTH))
+        {
+            periodicTaskExecutor.schedule(new OAuth2AuthHandlerGenerateTask());
+        }
+        else
+        {
+            throw new IllegalStateException("Unsupported JWT Auth Type: " + 
jwtParameters.jwtAuthType());
+        }
+    }
 
-        periodicTaskExecutor.schedule(new OAuth2AuthHandlerGenerateTask());
+    private @NotNull PeriodicStatelessJwtRefreshTask 
buildPeriodicStatelessJwtRefreshTask(Vertx vertx, JwtParameters jwtParameters)
+    {
+        WebClientOptions options = new WebClientOptions()
+                .setSsl(jwtParameters.site().startsWith("https"));
+
+        if (jwtParameters.keystorePath() != null)
+        {
+            if (jwtParameters.keystorePassword().isEmpty())
+            {
+                throw new IllegalArgumentException("JWT keystore password 
required when setting JWT keystore path.");
+            }
+            options.setKeyStoreOptions(new JksOptions()
+                    .setPath(jwtParameters.keystorePath())
+                    .setPassword(jwtParameters.keystorePassword())
+            );
+        }
+        if (jwtParameters.truststorePath() != null)
+        {
+            if (jwtParameters.truststorePassword().isEmpty())
+            {
+                throw new IllegalArgumentException("JWT truststore password 
required when setting JWT truststore path.");
+            }
+            options.setTrustStoreOptions(new JksOptions()
+                    .setPath(jwtParameters.truststorePath())
+                    .setPassword(jwtParameters.truststorePassword())
+            );
+        }
+        WebClient webClient = WebClient.create(vertx, options);
+        return new PeriodicStatelessJwtRefreshTask(webClient);
     }
 
     @Override
     public void authenticate(RoutingContext context, 
Handler<AsyncResult<User>> handler)
     {
-        OAuth2AuthHandlerImpl oAuth2AuthHandler = delegateHandler.get();
-        if (oAuth2AuthHandler == null)
+        AuthenticationHandlerInternal authHandler = delegateHandler.get();
+        if (authHandler == null)
         {
             
handler.handle(Future.failedFuture(wrapHttpException(SERVICE_UNAVAILABLE,
                                                                  "JWT 
authentication handler unavailable")));
             return;
         }
 
-        oAuth2AuthHandler.authenticate(context, authN -> {
+        authHandler.authenticate(context, authN -> {
             if (authN.failed())
             {
                 
handler.handle(Future.failedFuture(wrapHttpException(UNAUTHORIZED, 
authN.cause())));
@@ -170,6 +229,12 @@ extends 
AuthenticationHandlerImpl<ReloadingJwtAuthenticationHandler.NoOpAuthenti
             return jwtParameters.configDiscoverInterval();
         }
 
+        @Override
+        public DurationSpec initialDelay()
+        {
+            return SecondBoundConfiguration.ZERO;
+        }
+
         @Override
         public String name()
         {
@@ -205,4 +270,69 @@ extends 
AuthenticationHandlerImpl<ReloadingJwtAuthenticationHandler.NoOpAuthenti
                              });
         }
     }
+
+    private class PeriodicStatelessJwtRefreshTask implements PeriodicTask
+    {
+        private final String taskName = 
String.format("PeriodicStatelessJwtRefreshTask_%s", jwtParameters.site());
+        private final WebClient webClient;
+
+        private PeriodicStatelessJwtRefreshTask(WebClient webClient)
+        {
+            this.webClient = webClient;
+        }
+
+        @Override
+        public DurationSpec delay()
+        {
+            return jwtParameters.configDiscoverInterval();
+        }
+
+        @Override
+        public DurationSpec initialDelay()
+        {
+            return SecondBoundConfiguration.ZERO;
+        }
+
+
+        @Override
+        public void execute(Promise<Void> promise)
+        {
+            if (!jwtParameters.enabled())
+            {
+                delegateHandler.set(null);
+                promise.complete();
+                return;
+            }
+            String jwtPemUri = jwtParameters.site();
+            HttpRequest<Buffer> request = webClient.getAbs(jwtPemUri);
+            if (jwtParameters.pemProviderJwt() != null)
+            {
+                
request.bearerTokenAuthentication(jwtParameters.pemProviderJwt());
+            }
+
+            request.send()
+                    .onSuccess(ar -> {
+                        String pem = ar.bodyAsString();
+                        JWTAuthOptions jwtAuthOptions = new JWTAuthOptions()
+                                                        .addPubSecKey(new 
PubSecKeyOptions()
+                                                                      
.setAlgorithm("RS256")
+                                                                      
.setBuffer(pem));
+                        JWTAuth auth = JWTAuth.create(vertx, jwtAuthOptions);
+                        AuthenticationHandlerInternal jwtAuthHandlerDelegate = 
new JWTAuthHandlerImpl(auth, null);
+                        delegateHandler.set(jwtAuthHandlerDelegate);
+                        metrics.jwtPemRefreshSuccesses.metric.inc();
+                        promise.complete();
+                    }).onFailure(cause -> {
+                        LOGGER.error("Error encountered when refreshing 
stateless JWT PEM material.", cause);
+                        metrics.jwtPemRefreshFailures.metric.inc();
+                        promise.fail(cause);
+                    });
+        }
+
+        @Override
+        public String name()
+        {
+            return taskName;
+        }
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/AuthMetrics.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/AuthMetrics.java
new file mode 100644
index 00000000..816fc536
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/AuthMetrics.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *     http://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 org.apache.cassandra.sidecar.metrics.server;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.cassandra.sidecar.metrics.NamedMetric;
+
+import static 
org.apache.cassandra.sidecar.metrics.server.ServerMetrics.SERVER_PREFIX;
+
+/**
+ * Track metrics related to the authentication and authorization subsystems in 
Sidecar.
+ */
+public class AuthMetrics
+{
+    private static final String DOMAIN = SERVER_PREFIX + ".Auth";
+    public final NamedMetric<Counter> jwtPemRefreshFailures;
+    public final NamedMetric<Counter> jwtPemRefreshSuccesses;
+
+    public AuthMetrics(MetricRegistry metricRegistry)
+    {
+        jwtPemRefreshFailures = NamedMetric.builder(name -> 
metricRegistry.counter(name))
+                                        .withDomain(DOMAIN)
+                                        .withName("JwtPemRefreshFailures")
+                                        .build();
+
+        jwtPemRefreshSuccesses = NamedMetric.builder(name -> 
metricRegistry.counter(name))
+                                         .withDomain(DOMAIN)
+                                         .withName("JwtPemRefreshSuccesses")
+                                         .build();
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetrics.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetrics.java
index 1c05a301..13d8c4d0 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetrics.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetrics.java
@@ -68,4 +68,9 @@ public interface ServerMetrics
      * @return metrics tracked by server for cdc functionality.
      */
     CdcMetrics cdc();
+
+    /**
+     * @return metrics tracked by the server for the authentication / 
authorization subsystem.
+     */
+    AuthMetrics auth();
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetricsImpl.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetricsImpl.java
index 26684be5..f5ae1c63 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetricsImpl.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/metrics/server/ServerMetricsImpl.java
@@ -36,6 +36,7 @@ public class ServerMetricsImpl implements ServerMetrics
     protected final CacheMetrics cacheMetrics;
     protected final CoordinationMetrics coordinationMetrics;
     protected final CdcMetrics cdcMetrics;
+    protected final AuthMetrics authMetrics;
 
     public ServerMetricsImpl(MetricRegistry metricRegistry)
     {
@@ -49,6 +50,7 @@ public class ServerMetricsImpl implements ServerMetrics
         this.cacheMetrics = new CacheMetrics(metricRegistry);
         this.coordinationMetrics = new CoordinationMetrics(metricRegistry);
         this.cdcMetrics = new CdcMetrics(metricRegistry);
+        this.authMetrics = new AuthMetrics(metricRegistry);
     }
 
     @Override
@@ -98,4 +100,10 @@ public class ServerMetricsImpl implements ServerMetrics
     {
         return cdcMetrics;
     }
+
+    @Override
+    public AuthMetrics auth()
+    {
+        return authMetrics;
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java
index 04e72bb1..5ffb5b57 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/AuthModule.java
@@ -51,6 +51,7 @@ import 
org.apache.cassandra.sidecar.db.schema.SidecarRolePermissionsSchema;
 import org.apache.cassandra.sidecar.db.schema.SystemAuthSchema;
 import org.apache.cassandra.sidecar.db.schema.TableSchema;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.SidecarMetrics;
 import org.apache.cassandra.sidecar.modules.multibindings.KeyClassMapKey;
 import org.apache.cassandra.sidecar.modules.multibindings.TableSchemaMapKeys;
 import org.apache.cassandra.sidecar.modules.multibindings.VertxRouteMapKeys;
@@ -114,7 +115,8 @@ public class AuthModule extends AbstractModule
     @KeyClassMapKey(VertxRouteMapKeys.GlobalChainAuthHandlerKey.class)
     VertxRoute chainAuthHandler(Vertx vertx,
                                 SidecarConfiguration sidecarConfiguration,
-                                AuthenticationHandlerFactoryRegistry registry) 
throws ConfigurationException
+                                AuthenticationHandlerFactoryRegistry registry,
+                                SidecarMetrics sidecarMetrics) throws 
ConfigurationException
     {
         AccessControlConfiguration accessControlConfiguration = 
sidecarConfiguration.accessControlConfiguration();
         if (!accessControlConfiguration.enabled())
@@ -141,7 +143,7 @@ public class AuthModule extends AbstractModule
                 throw new RuntimeException(String.format("Implementation for 
class %s has not been registered",
                                                          config.className()));
             }
-            chainAuthHandler.add(factory.create(vertx, 
accessControlConfiguration, config.namedParameters()));
+            chainAuthHandler.add(factory.create(vertx, 
accessControlConfiguration, config.namedParameters(), 
sidecarMetrics.server().auth()));
         }
 
         return VertxRoute.create(router -> router.route()
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java
index de9f20c4..511eb623 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JWTAuthenticationHandlerFactoryTest.java
@@ -43,15 +43,15 @@ class JWTAuthenticationHandlerFactoryTest
     {
         PeriodicTaskExecutor mockTaskExecutor = 
mock(PeriodicTaskExecutor.class);
         JwtAuthenticationHandlerFactory factory = new 
JwtAuthenticationHandlerFactory(mockRoleProcessor, mockTaskExecutor);
-        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, null))
+        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, null, 
null))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessage("JWT parameters can not be null");
 
-        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, 
Map.of()))
+        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, 
Map.of(), null))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessage("Missing site JWT parameter");
 
-        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, 
Map.of("site", "www.apache.org")))
+        assertThatThrownBy(() -> factory.create(mockVertx, mockConfig, 
Map.of("site", "www.apache.org"), null))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessage("Missing client_id JWT parameter");
     }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractorTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractorTest.java
index f92eb2f7..3afb3ee4 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractorTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/JwtParameterExtractorTest.java
@@ -93,4 +93,65 @@ class JwtParameterExtractorTest
         assertThat(parameterExtractor.site()).isEqualTo("www.apache.org");
         assertThat(parameterExtractor.clientId()).isEqualTo("id");
     }
+
+    @Test
+    void testJwtAuthTypeWhenNotSpecifiedSetsToOauth()
+    {
+        JwtParameters params = new JwtParameterExtractor(Map.of("enabled", 
"true",
+                                                                "site", 
"www.apache.org",
+                                                                "client_id", 
"id"));
+        
assertThat(params.jwtAuthType()).isEqualTo(JwtParameters.AuthType.OAUTH);
+    }
+
+    @Test
+    void testJwtAuthTypeWhenOauthSetsToOauth()
+    {
+        JwtParameters params = new JwtParameterExtractor(Map.of("enabled", 
"true",
+                                                                "site", 
"www.apache.org",
+                                                                "client_id", 
"id",
+                                                                
"jwt_auth_type", JwtParameters.AuthType.OAUTH.toString().toLowerCase()));
+        
assertThat(params.jwtAuthType()).isEqualTo(JwtParameters.AuthType.OAUTH);
+    }
+
+    @Test
+    void testJwtAuthTypeWhenStatelessSetsToStateless()
+    {
+        JwtParameters params = new JwtParameterExtractor(Map.of("enabled", 
"true",
+                                                                "site", 
"www.apache.org",
+                                                                
"jwt_auth_type", JwtParameters.AuthType.STATELESS.toString().toLowerCase()));
+        
assertThat(params.jwtAuthType()).isEqualTo(JwtParameters.AuthType.STATELESS);
+    }
+
+
+    @Test
+    void testJwtAuthTypeWhenInvalidThrowsIllegalArgumentException()
+    {
+        assertThatThrownBy(() -> new JwtParameterExtractor(Map.of("enabled", 
"true",
+                                                                  "site", 
"www.apache.org",
+                                                                  
"jwt_auth_type", "bear")))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessageContaining("Invalid JWT authentication type: bear. 
Supported types are: [");
+    }
+
+    @Test
+    void testJwtAuthTypeWhenStatelessAndSetsKeystoreMustSetPassword()
+    {
+        assertThatThrownBy(() -> new JwtParameterExtractor(Map.of("enabled", 
"true",
+                "site", "www.apache.org",
+                "jwt_auth_type", 
JwtParameters.AuthType.STATELESS.toString().toLowerCase(),
+                "keystore_path", "/path/to/keystore")))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Missing keystore_password JWT parameter");
+    }
+
+    @Test
+    void testJwtAuthTypeWhenStatelessAndSetsTruststoreMustSetPassword()
+    {
+        assertThatThrownBy(() -> new JwtParameterExtractor(Map.of("enabled", 
"true",
+                "site", "www.apache.org",
+                "jwt_auth_type", 
JwtParameters.AuthType.STATELESS.toString().toLowerCase(),
+                "truststore_path", "/path/to/keystore")))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Missing truststore_password JWT parameter");
+    }
 }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
index 3d053af3..360d6046 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.io.TempDir;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.codahale.metrics.MetricRegistry;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import com.google.inject.Provides;
@@ -77,6 +78,7 @@ import 
org.apache.cassandra.sidecar.config.yaml.ParameterizedClassConfigurationI
 import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 import org.apache.cassandra.sidecar.modules.SidecarModules;
 import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.testing.utils.tls.CertificateBuilder;
@@ -241,7 +243,9 @@ class MutualTLSAuthenticationHandlerTest
         ChainAuthHandlerImpl chainAuthHandlerSucceeds = (ChainAuthHandlerImpl) 
ChainAuthHandler.any();
         MutualTlsAuthenticationHandlerFactory mTLSAuthFactory = 
injector.getInstance(MutualTlsAuthenticationHandlerFactory.class);
         SidecarConfiguration config = 
injector.getInstance(SidecarConfiguration.class);
-        chainAuthHandlerSucceeds.add(mTLSAuthFactory.create(vertx, 
config.accessControlConfiguration(), params));
+        MetricRegistry metricRegistry = new MetricRegistry();
+        AuthMetrics authMetrics = new AuthMetrics(metricRegistry);
+        chainAuthHandlerSucceeds.add(mTLSAuthFactory.create(vertx, 
config.accessControlConfiguration(), params, authMetrics));
         chainAuthHandlerSucceeds.add(new TestAuthHandler(new 
NoOpAuthentication(), true));
 
         RoutingContext mockContext = mock(RoutingContext.class);
@@ -251,7 +255,7 @@ class MutualTLSAuthenticationHandlerTest
         when(mockContext.request()).thenReturn(mockServerRequest);
 
         ChainAuthHandlerImpl chainAuthHandlerFails = (ChainAuthHandlerImpl) 
ChainAuthHandler.any();
-        chainAuthHandlerFails.add(mTLSAuthFactory.create(vertx, 
config.accessControlConfiguration(), params));
+        chainAuthHandlerFails.add(mTLSAuthFactory.create(vertx, 
config.accessControlConfiguration(), params, authMetrics));
         chainAuthHandlerFails.add(new TestAuthHandler(new 
NoOpAuthentication(), false));
 
         CountDownLatch waitForResponse = new CountDownLatch(2);
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
index d7dbde10..66e567da 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTlsAuthenticationHandlerFactoryTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import com.codahale.metrics.MetricRegistry;
 import io.vertx.core.Vertx;
 import org.apache.cassandra.sidecar.TestResourceReaper;
 import org.apache.cassandra.sidecar.acl.AdminIdentityResolver;
@@ -37,6 +38,7 @@ import org.apache.cassandra.sidecar.config.CacheConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.db.SystemAuthDatabaseAccessor;
 import org.apache.cassandra.sidecar.exceptions.ConfigurationException;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 
 import static 
org.apache.cassandra.sidecar.ExecutorPoolsHelper.createdSharedTestPool;
 import static 
org.apache.cassandra.sidecar.acl.authentication.MutualTlsAuthenticationHandlerFactory.CERTIFICATE_IDENTITY_EXTRACTOR_PARAM_KEY;
@@ -110,7 +112,9 @@ class MutualTlsAuthenticationHandlerFactoryTest
         
when(mockSidecarConfig.accessControlConfiguration()).thenReturn(mockAccessControlConfig);
         SystemAuthDatabaseAccessor mockAccessor = 
mock(SystemAuthDatabaseAccessor.class);
         MutualTlsAuthenticationHandlerFactory factory = 
factory(mockSidecarConfig, mockAccessor);
-        assertThatThrownBy(() -> factory.create(vertx, 
mockAccessControlConfig, parameters))
+        MetricRegistry metricRegistry = new MetricRegistry();
+        AuthMetrics authMetrics = new AuthMetrics(metricRegistry);
+        assertThatThrownBy(() -> factory.create(vertx, 
mockAccessControlConfig, parameters, authMetrics))
         .isInstanceOf(ConfigurationException.class)
         .hasMessage(expectedErrMsg);
     }
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandlerTest.java
index e147b2e3..ddba88cd 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/ReloadingJwtAuthenticationHandlerTest.java
@@ -18,15 +18,39 @@
 
 package org.apache.cassandra.sidecar.acl.authentication;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.Base64;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.codahale.metrics.MetricRegistry;
+import io.vertx.core.AsyncResult;
 import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpServer;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.json.JsonArray;
+import io.vertx.ext.auth.User;
 import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.config.yaml.ServiceConfigurationImpl;
+import org.apache.cassandra.sidecar.coordination.ClusterLease;
+import org.apache.cassandra.sidecar.metrics.server.AuthMetrics;
 import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor;
+import org.jetbrains.annotations.NotNull;
 
+import static org.apache.cassandra.testing.utils.AssertionUtils.loopAssert;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
@@ -49,8 +73,10 @@ class ReloadingJwtAuthenticationHandlerTest
         
when(mockRoleProcessor.processRoles(any())).thenReturn(List.of("test_role"));
         PeriodicTaskExecutor mockTaskExecutor = 
mock(PeriodicTaskExecutor.class);
         doNothing().when(mockTaskExecutor).schedule(any());
+        MetricRegistry metricRegistry = new MetricRegistry();
+        AuthMetrics authMetrics = new AuthMetrics(metricRegistry);
         ReloadingJwtAuthenticationHandler reloadingJwtAuthenticationHandler
-        = new ReloadingJwtAuthenticationHandler(mockVertx, parameterExtractor, 
mockRoleProcessor, mockTaskExecutor);
+        = new ReloadingJwtAuthenticationHandler(mockVertx, parameterExtractor, 
mockRoleProcessor, mockTaskExecutor, authMetrics);
         RoutingContext mockCtx = mock(RoutingContext.class);
         reloadingJwtAuthenticationHandler.authenticate(mockCtx, result -> {
             assertThat(result.failed()).isTrue();
@@ -67,12 +93,115 @@ class ReloadingJwtAuthenticationHandlerTest
                                                                                
     "client_id", "id"));
         JwtRoleProcessor mockRoleProcessor = mock(JwtRoleProcessor.class);
         PeriodicTaskExecutor mockTaskExecutor = 
mock(PeriodicTaskExecutor.class);
+        MetricRegistry metricRegistry = new MetricRegistry();
+        AuthMetrics authMetrics = new AuthMetrics(metricRegistry);
         ReloadingJwtAuthenticationHandler reloadingJwtAuthenticationHandler
-        = new ReloadingJwtAuthenticationHandler(mockVertx, parameterExtractor, 
mockRoleProcessor, mockTaskExecutor);
+        = new ReloadingJwtAuthenticationHandler(mockVertx, parameterExtractor, 
mockRoleProcessor, mockTaskExecutor, authMetrics);
         RoutingContext mockCtx = mock(RoutingContext.class);
         reloadingJwtAuthenticationHandler.authenticate(mockCtx, result -> {
             assertThat(result.failed()).isTrue();
             assertThat(result.cause()).hasMessage("Service Unavailable");
         });
     }
+
+    @Test
+    void testStatelessJwtAuthenticationWithValidToken() throws Exception
+    {
+        // Generate a test RSA key pair and PEM
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        KeyPair keyPair = keyGen.generateKeyPair();
+
+        String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
+                              
Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()) +
+                              "\n-----END PUBLIC KEY-----";
+
+        Vertx vertx = Vertx.vertx();
+        try
+        {
+            // Mock HTTP server to serve the PEM
+            HttpServer mockServer = vertx.createHttpServer();
+            CountDownLatch serverLatch = new CountDownLatch(1);
+
+            mockServer.requestHandler(request -> {
+                request.response()
+                       .putHeader("Content-Type", "text/plain")
+                       .end(publicKeyPem);
+            }).listen(0, result -> serverLatch.countDown());
+
+            serverLatch.await(5, TimeUnit.SECONDS);
+
+            // Configure for stateless authentication
+            String site = String.format("http://localhost:%d/jwks";, 
mockServer.actualPort());
+            JwtParameterExtractor parameterExtractor = new 
JwtParameterExtractor(Map.of("enabled", "true",
+                                                                               
         "site", site,
+                                                                               
         "jwt_auth_type", 
JwtParameters.AuthType.STATELESS.toString().toLowerCase()));
+            JwtRoleProcessor mockRoleProcessor = mock(JwtRoleProcessor.class);
+            
when(mockRoleProcessor.processRoles(any())).thenReturn(List.of("test_role"));
+            ReloadingJwtAuthenticationHandler handler = 
getReloadingJwtAuthenticationHandler(vertx, parameterExtractor, 
mockRoleProcessor);
+
+            // Wait a bit for the handler to be set
+            loopAssert(1, () -> assertNotNull(handler.delegateHandler.get()));
+
+            // Test authentication with valid token - create proper mocks
+            RoutingContext mockCtx = mock(RoutingContext.class);
+            HttpServerRequest mockRequest = mock(HttpServerRequest.class);
+
+            // Mock the request properly for JWT parsing
+            when(mockCtx.request()).thenReturn(mockRequest);
+            // Create a valid JWT token using the private key
+            String validToken = createTestJwtToken(keyPair.getPrivate());
+            when(mockRequest.getHeader("Authorization")).thenReturn("Bearer " 
+ validToken);
+            
when(mockRequest.headers()).thenReturn(io.vertx.core.MultiMap.caseInsensitiveMultiMap()
+                                                                         
.add("Authorization", "Bearer " + validToken));
+
+            CountDownLatch authLatch = new CountDownLatch(1);
+            AtomicReference<AsyncResult<User>> authResult = new 
AtomicReference<>();
+
+            handler.authenticate(mockCtx, result -> {
+                authResult.set(result);
+                authLatch.countDown();
+            });
+
+            authLatch.await(5, TimeUnit.SECONDS);
+
+            // The authentication should process (may succeed or fail based on 
token validation)
+            assertThat(authResult.get()).isNotNull();
+            // Ensure that user and role context was properly passed oto the 
next step.
+            
assertThat(authResult.get().result().attributes().getString("sub")).isEqualTo("test-user");
+            
assertThat(authResult.get().result().attributes().getJsonArray("cassandra_roles")).isEqualTo(new
 JsonArray(List.of("test_role")));
+            mockServer.close();
+        }
+        finally
+        {
+            vertx.close();
+        }
+    }
+
+    private static @NotNull ReloadingJwtAuthenticationHandler 
getReloadingJwtAuthenticationHandler(Vertx vertx, JwtParameterExtractor 
parameterExtractor, JwtRoleProcessor mockRoleProcessor)
+    {
+        ExecutorPools executorPools = new ExecutorPools(vertx, new 
ServiceConfigurationImpl());
+        ClusterLease clusterLease = new ClusterLease();
+        PeriodicTaskExecutor executor = new 
PeriodicTaskExecutor(executorPools, clusterLease);
+        MetricRegistry metricRegistry = new MetricRegistry();
+        AuthMetrics authMetrics = new AuthMetrics(metricRegistry);
+        return new ReloadingJwtAuthenticationHandler(vertx,
+                                                     parameterExtractor,
+                                                     mockRoleProcessor,
+                                                     executor,
+                                                     authMetrics
+        );
+    }
+
+    private String createTestJwtToken(PrivateKey privateKey)
+    {
+        Algorithm algorithm = Algorithm.RSA256(null, (RSAPrivateKey) 
privateKey);
+
+        return JWT.create()
+                  .withIssuer("test-issuer")
+                  .withSubject("test-user")
+                  .withExpiresAt(new Date(System.currentTimeMillis() + 
3600000)) // 1 hour
+                  .withClaim("roles", List.of("test_role"))
+                  .sign(algorithm);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to