diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index b23933870f049..aa62d991663ab 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -183,7 +183,7 @@
3.3.3
5.3.1
4.7.2
- 1.1.1.Final
+ 1.1.2.Final-SNAPSHOT
9.0.2
1.14.0
0.1.55
diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
index 8ba803d5b416d..ee76775ce8a71 100644
--- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
+++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/SecurityProcessor.java
@@ -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;
@@ -354,5 +355,6 @@ void registerAdditionalBeans(BuildProducer 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));
}
}
diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java
new file mode 100644
index 0000000000000..37e990aa4b60b
--- /dev/null
+++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java
@@ -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 {
+
+ @Override
+ public Class getRequestType() {
+ return CertificateAuthenticationRequest.class;
+ }
+
+ @Override
+ public Uni authenticate(CertificateAuthenticationRequest request, AuthenticationRequestContext context) {
+ X509Certificate certificate = request.getCertificate().getCertificate();
+
+ return Uni.createFrom().item(QuarkusSecurityIdentity.builder()
+ .setPrincipal(certificate.getSubjectX500Principal())
+ .addCredential(request.getCertificate())
+ .build());
+ }
+}
diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java
index 55ac5491c20d0..2611f7f5d3c4c 100644
--- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java
+++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java
@@ -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 {
@@ -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;
}
@@ -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();
}
@@ -134,4 +153,8 @@ void setupAuthenticationMechanisms(
}
}
}
+
+ private boolean isMtlsClientAuthenticationEnabled(HttpBuildTimeConfig buildTimeConfig) {
+ return !ClientAuth.NONE.equals(buildTimeConfig.clientAuth);
+ }
}
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestTest.java
new file mode 100644
index 0000000000000..50da1c7295a51
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequestTest.java
@@ -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());
+ });
+ }
+
+ }
+}
diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequiredTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequiredTest.java
new file mode 100644
index 0000000000000..667f067b79a00
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/MtlsRequiredTest.java
@@ -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());
+ });
+ }
+
+ }
+}
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-keystore.jks b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-keystore.jks
new file mode 100644
index 0000000000000..cf6d6ba454864
Binary files /dev/null and b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-keystore.jks differ
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-truststore.jks b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-truststore.jks
new file mode 100644
index 0000000000000..bf6371859c55f
Binary files /dev/null and b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/client-truststore.jks differ
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf
new file mode 100644
index 0000000000000..1219f0623d7ed
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-jks.conf
@@ -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
\ No newline at end of file
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-no-auth-jks.conf b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-no-auth-jks.conf
new file mode 100644
index 0000000000000..e1f95f0d7b8f5
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/mtls-no-auth-jks.conf
@@ -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
\ No newline at end of file
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-keystore.jks b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-keystore.jks
new file mode 100644
index 0000000000000..da33e8e7a1668
Binary files /dev/null and b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-keystore.jks differ
diff --git a/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-truststore.jks b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-truststore.jks
new file mode 100644
index 0000000000000..8ec8e126507b6
Binary files /dev/null and b/extensions/vertx-http/deployment/src/test/resources/conf/mtls/server-truststore.jks differ
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java
index 41147163da72f..390a85a92672f 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java
@@ -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 {
@@ -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 clientAuth;
+
/**
* If this is true then only a virtual channel will be set up for vertx web.
* We have this switch for testing purposes.
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java
index 78b89a527f72e..41575c3ed9c9f 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java
@@ -39,7 +39,11 @@ public enum Type {
/**
* A post request, target is the POST URI
*/
- POST
+ POST,
+ /**
+ * X509
+ */
+ X509
}
@Override
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
index 6cdf9daf03725..50eb322b54404 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
@@ -209,4 +209,13 @@ public BasicAuthenticationMechanism get() {
}
};
}
+
+ public Supplier> setupMtlsClientAuth() {
+ return new Supplier() {
+ @Override
+ public MtlsAuthenticationMechanism get() {
+ return new MtlsAuthenticationMechanism();
+ }
+ };
+ }
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java
new file mode 100644
index 0000000000000..ebafd1c5cc000
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java
@@ -0,0 +1,81 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2014 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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 io.quarkus.vertx.http.runtime.security;
+
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.quarkus.security.credential.CertificateCredential;
+import io.quarkus.security.identity.IdentityProviderManager;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.identity.request.AuthenticationRequest;
+import io.quarkus.security.identity.request.CertificateAuthenticationRequest;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.RoutingContext;
+
+/**
+ * The authentication handler responsible for mTLS client authentication
+ */
+public class MtlsAuthenticationMechanism implements HttpAuthenticationMechanism {
+
+ @Override
+ public Uni authenticate(RoutingContext context,
+ IdentityProviderManager identityProviderManager) {
+ HttpServerRequest request = context.request();
+
+ if (!request.isSSL()) {
+ // we don't handle insecure requests
+ return Uni.createFrom().failure(new AuthenticationCompletionException());
+ }
+
+ Certificate certificate;
+
+ try {
+ certificate = request.sslSession().getPeerCertificates()[0];
+ } catch (SSLPeerUnverifiedException e) {
+ return Uni.createFrom().optional(Optional.empty());
+ }
+
+ return identityProviderManager
+ .authenticate(new CertificateAuthenticationRequest(
+ new CertificateCredential(X509Certificate.class.cast(certificate))));
+ }
+
+ @Override
+ public Uni getChallenge(RoutingContext context) {
+ return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(),
+ null, null));
+ }
+
+ @Override
+ public Set> getCredentialTypes() {
+ return Collections.singleton(CertificateAuthenticationRequest.class);
+ }
+
+ @Override
+ public HttpCredentialTransport getCredentialTransport() {
+ return new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509");
+ }
+}