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]