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"); + } +}