Skip to content

Commit

Permalink
[fixes quarkusio#8508] - mTLS client authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroigor committed Jun 3, 2020
1 parent 5b4683d commit 687875c
Show file tree
Hide file tree
Showing 19 changed files with 317 additions and 20 deletions.
2 changes: 1 addition & 1 deletion bom/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
<mockito.version>3.3.3</mockito.version>
<jna.version>5.3.1</jna.version><!-- should satisfy both testcontainers and mongodb -->
<antlr.version>4.7.2</antlr.version>
<quarkus-security.version>1.1.1.Final</quarkus-security.version>
<quarkus-security.version>1.1.2.Final</quarkus-security.version>
<keycloak.version>10.0.1</keycloak.version>
<logstash-gelf.version>1.14.0</logstash-gelf.version>
<jsch.version>0.1.55</jsch.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import io.quarkus.security.runtime.SecurityBuildTimeConfig;
import io.quarkus.security.runtime.SecurityIdentityAssociation;
import io.quarkus.security.runtime.SecurityIdentityProxy;
import io.quarkus.security.runtime.X509IdentityProvider;
import io.quarkus.security.runtime.interceptor.AuthenticatedInterceptor;
import io.quarkus.security.runtime.interceptor.DenyAllInterceptor;
import io.quarkus.security.runtime.interceptor.PermitAllInterceptor;
Expand Down Expand Up @@ -354,5 +355,6 @@ void registerAdditionalBeans(BuildProducer<AdditionalBeanBuildItem> beans) {
beans.produce(AdditionalBeanBuildItem.unremovableOf(SecurityIdentityAssociation.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(IdentityProviderManagerCreator.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(SecurityIdentityProxy.class));
beans.produce(AdditionalBeanBuildItem.unremovableOf(X509IdentityProvider.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.quarkus.security.runtime;

import java.security.cert.X509Certificate;

import javax.inject.Singleton;

import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
import io.smallrye.mutiny.Uni;

@Singleton
public class X509IdentityProvider implements IdentityProvider<CertificateAuthenticationRequest> {

@Override
public Class<CertificateAuthenticationRequest> getRequestType() {
return CertificateAuthenticationRequest.class;
}

@Override
public Uni<SecurityIdentity> authenticate(CertificateAuthenticationRequest request, AuthenticationRequestContext context) {
X509Certificate certificate = request.getCertificate().getCertificate();

return Uni.createFrom().item(QuarkusSecurityIdentity.builder()
.setPrincipal(certificate.getSubjectX500Principal())
.addCredential(request.getCertificate())
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
import io.quarkus.vertx.http.runtime.security.HttpAuthorizer;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder;
import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy;
import io.quarkus.vertx.http.runtime.security.SupplierImpl;
import io.vertx.core.http.ClientAuth;

public class HttpSecurityProcessor {

Expand Down Expand Up @@ -67,12 +69,28 @@ SyntheticBeanBuildItem initFormAuth(
return null;
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
SyntheticBeanBuildItem initMtlsClientAuth(
HttpSecurityRecorder recorder,
HttpBuildTimeConfig buildTimeConfig) {
if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) {
return SyntheticBeanBuildItem.configure(MtlsAuthenticationMechanism.class)
.types(HttpAuthenticationMechanism.class)
.setRuntimeInit()
.scope(Singleton.class)
.supplier(recorder.setupMtlsClientAuth()).done();
}
return null;
}

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
SyntheticBeanBuildItem initBasicAuth(
HttpSecurityRecorder recorder,
HttpBuildTimeConfig buildTimeConfig) {
if (buildTimeConfig.auth.form.enabled && !buildTimeConfig.auth.basic) {
if ((buildTimeConfig.auth.form.enabled || isMtlsClientAuthenticationEnabled(buildTimeConfig))
&& !buildTimeConfig.auth.basic) {
//if form auth is enabled and we are not then we don't install
return null;
}
Expand All @@ -82,7 +100,8 @@ SyntheticBeanBuildItem initBasicAuth(
.setRuntimeInit()
.scope(Singleton.class)
.supplier(recorder.setupBasicAuth(buildTimeConfig));
if (!buildTimeConfig.auth.form.enabled && !buildTimeConfig.auth.basic) {
if (!buildTimeConfig.auth.form.enabled && !isMtlsClientAuthenticationEnabled(buildTimeConfig)
&& !buildTimeConfig.auth.basic) {
//if not explicitly enabled we make this a default bean, so it is the fallback if nothing else is defined
configurator.defaultBean();
}
Expand Down Expand Up @@ -134,4 +153,8 @@ void setupAuthenticationMechanisms(
}
}
}

private boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) {
return !ClientAuth.NONE.equals(buildTimeConfig.tlsClientAuth);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ void openSocket(ApplicationStartBuildItem start,
// start http socket in dev/test mode even if virtual http is required
boolean startSocket = !startVirtual || launchMode.getLaunchMode() != LaunchMode.NORMAL;
recorder.startServer(vertx.getVertx(), shutdown,
httpConfiguration, launchMode.getLaunchMode(), startVirtual, startSocket,
httpBuildTimeConfig, httpConfiguration, launchMode.getLaunchMode(), startVirtual, startSocket,
eventLoopCount.getEventLoopCount(),
websocketSubProtocols.stream().map(bi -> bi.getWebsocketSubProtocols())
.collect(Collectors.joining(",")));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package io.quarkus.vertx.http.security;

import static org.hamcrest.Matchers.is;

import java.io.File;
import java.net.URL;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.restassured.RestAssured;
import io.vertx.ext.web.Router;

public class MtlsRequestTest {

@TestHTTPResource(value = "/mtls", ssl = true)
URL url;

@TestHTTPResource(value = "/mtls", ssl = false)
URL urlNoTls;

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(MyBean.class)
.addAsResource(new File("src/test/resources/conf/mtls/mtls-no-auth-jks.conf"), "application.properties")
.addAsResource(new File("src/test/resources/conf/mtls/server-keystore.jks"), "server-keystore.jks")
.addAsResource(new File("src/test/resources/conf/mtls/server-truststore.jks"), "server-truststore.jks"));

@Test
public void testClientAuthentication() {
RestAssured.given()
.keyStore(new File("src/test/resources/conf/mtls/client-keystore.jks"), "password")
.trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password")
.get(url).then().statusCode(200).body(is("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU"));
}

@Test
public void testNoClientCert() {
RestAssured.given()
.trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password")
.get(url).then().statusCode(401);
}

@ApplicationScoped
static class MyBean {

public void register(@Observes Router router) {
router.get("/mtls").handler(rc -> {
rc.response().end(QuarkusHttpUser.class.cast(rc.user()).getSecurityIdentity().getPrincipal().getName());
});
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.quarkus.vertx.http.security;

import static org.hamcrest.Matchers.is;

import java.io.File;
import java.net.URL;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.restassured.RestAssured;
import io.vertx.ext.web.Router;

public class MtlsRequiredTest {

@TestHTTPResource(value = "/mtls", ssl = true)
URL url;

@TestHTTPResource(value = "/mtls", ssl = false)
URL urlNoTls;

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(MyBean.class)
.addAsResource(new File("src/test/resources/conf/mtls/mtls-jks.conf"), "application.properties")
.addAsResource(new File("src/test/resources/conf/mtls/server-keystore.jks"), "server-keystore.jks")
.addAsResource(new File("src/test/resources/conf/mtls/server-truststore.jks"), "server-truststore.jks"));

@Test
public void testClientAuthentication() {
RestAssured.given()
.keyStore(new File("src/test/resources/conf/mtls/client-keystore.jks"), "password")
.trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password")
.get(url).then().statusCode(200).body(is("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU"));
}

@Test
public void testNoSslServerWithJKS() {
RestAssured.given()
.get(urlNoTls).then().statusCode(401);
}

@Test
public void testNoClientCert() {
RestAssured.given()
.trustStore(new File("src/test/resources/conf/mtls/client-truststore.jks"), "password")
.get(url).then().statusCode(401);
}

@ApplicationScoped
static class MyBean {

public void register(@Observes Router router) {
router.get("/mtls").handler(rc -> {
rc.response().end(QuarkusHttpUser.class.cast(rc.user()).getSecurityIdentity().getPrincipal().getName());
});
}

}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=secret
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
quarkus.http.ssl.client-auth=REQUIRED
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=secret
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
quarkus.http.ssl.client-auth=REQUEST
quarkus.http.auth.permission.all.paths=/*
quarkus.http.auth.permission.all.policy=authenticated
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.vertx.core.http.ClientAuth;

@ConfigRoot(name = "http", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class HttpBuildTimeConfig {
Expand All @@ -15,6 +16,13 @@ public class HttpBuildTimeConfig {

public AuthConfig auth;

/**
* Configures the engine to require/request client authentication.
* NONE, REQUEST, REQUIRED
*/
@ConfigItem(name = "ssl.client-auth", defaultValue = "NONE")
public ClientAuth tlsClientAuth;

/**
* If this is true then only a virtual channel will be set up for vertx web.
* We have this switch for testing purposes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.DefaultConverter;
import io.vertx.core.http.ClientAuth;

/**
* Shared configuration for setting up server-side SSL.
Expand All @@ -31,11 +30,4 @@ public class ServerSslConfig {
@ConfigItem(defaultValue = "TLSv1.3,TLSv1.2")
public List<String> protocols;

/**
* Configures the engine to require/request client authentication.
* NONE, REQUEST, REQUIRED
*/
@ConfigItem(defaultValue = "NONE")
public ClientAuth clientAuth;

}
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ public static void startServerAfterFailedStart() {
Vertx vertx = VertxCoreRecorder.initialize(vertxConfiguration, null);

try {
HttpBuildTimeConfig buildConfig = new HttpBuildTimeConfig();
ConfigInstantiator.handleObject(buildConfig);
HttpConfiguration config = new HttpConfiguration();
ConfigInstantiator.handleObject(config);

Expand All @@ -160,7 +162,7 @@ public static void startServerAfterFailedStart() {
rootHandler = router;

//we can't really do
doServerStart(vertx, config, LaunchMode.DEVELOPMENT, new Supplier<Integer>() {
doServerStart(vertx, buildConfig, config, LaunchMode.DEVELOPMENT, new Supplier<Integer>() {
@Override
public Integer get() {
return ProcessorInfo.availableProcessors() * 2; //this is dev mode, so the number of IO threads not always being 100% correct does not really matter in this case
Expand All @@ -185,7 +187,8 @@ public RuntimeValue<Router> initializeRouter(final Supplier<Vertx> vertxRuntimeV
}

public void startServer(Supplier<Vertx> vertx, ShutdownContext shutdown,
HttpConfiguration httpConfiguration, LaunchMode launchMode,
HttpBuildTimeConfig httpBuildTimeConfig, HttpConfiguration httpConfiguration,
LaunchMode launchMode,
boolean startVirtual, boolean startSocket, Supplier<Integer> ioThreads, String websocketSubProtocols)
throws IOException {

Expand All @@ -195,7 +198,8 @@ public void startServer(Supplier<Vertx> vertx, ShutdownContext shutdown,
if (startSocket) {
// Start the server
if (closeTask == null) {
doServerStart(vertx.get(), httpConfiguration, launchMode, ioThreads, websocketSubProtocols);
doServerStart(vertx.get(), httpBuildTimeConfig, httpConfiguration, launchMode, ioThreads,
websocketSubProtocols);
if (launchMode != LaunchMode.DEVELOPMENT) {
shutdown.addShutdownTask(closeTask);
}
Expand Down Expand Up @@ -364,12 +368,13 @@ public void handle(RoutingContext event) {
rootHandler = root;
}

private static void doServerStart(Vertx vertx, HttpConfiguration httpConfiguration, LaunchMode launchMode,
private static void doServerStart(Vertx vertx, HttpBuildTimeConfig httpBuildTimeConfig,
HttpConfiguration httpConfiguration, LaunchMode launchMode,
Supplier<Integer> eventLoops, String websocketSubProtocols) throws IOException {
// Http server configuration
HttpServerOptions httpServerOptions = createHttpServerOptions(httpConfiguration, launchMode, websocketSubProtocols);
HttpServerOptions domainSocketOptions = createDomainSocketOptions(httpConfiguration, websocketSubProtocols);
HttpServerOptions sslConfig = createSslOptions(httpConfiguration, launchMode);
HttpServerOptions sslConfig = createSslOptions(httpBuildTimeConfig, httpConfiguration, launchMode);
if (httpConfiguration.insecureRequests != HttpConfiguration.InsecureRequests.ENABLED && sslConfig == null) {
throw new IllegalStateException("Cannot set quarkus.http.redirect-insecure-requests without enabling SSL.");
}
Expand Down Expand Up @@ -467,7 +472,8 @@ private static void setHttpServerTiming(InsecureRequests insecureRequests, HttpS
/**
* Get an {@code HttpServerOptions} for this server configuration, or null if SSL should not be enabled
*/
private static HttpServerOptions createSslOptions(HttpConfiguration httpConfiguration, LaunchMode launchMode)
private static HttpServerOptions createSslOptions(HttpBuildTimeConfig buildTimeConfig, HttpConfiguration httpConfiguration,
LaunchMode launchMode)
throws IOException {
if (!httpConfiguration.hostEnabled) {
return null;
Expand Down Expand Up @@ -562,7 +568,7 @@ private static HttpServerOptions createSslOptions(HttpConfiguration httpConfigur
serverOptions.setSsl(true);
serverOptions.setHost(httpConfiguration.host);
serverOptions.setPort(httpConfiguration.determineSslPort(launchMode));
serverOptions.setClientAuth(sslConfig.clientAuth);
serverOptions.setClientAuth(buildTimeConfig.tlsClientAuth);
serverOptions.setReusePort(httpConfiguration.soReusePort);
serverOptions.setTcpQuickAck(httpConfiguration.tcpQuickAck);
serverOptions.setTcpCork(httpConfiguration.tcpCork);
Expand Down
Loading

0 comments on commit 687875c

Please sign in to comment.