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 May 4, 2020
1 parent d5f8c25 commit 75f17cf
Show file tree
Hide file tree
Showing 16 changed files with 305 additions and 4 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>
<antlr.version>4.7.2</antlr.version>
<quarkus-security.version>1.1.1.Final</quarkus-security.version>
<quarkus-security.version>1.1.2.Final-SNAPSHOT</quarkus-security.version>
<keycloak.version>9.0.2</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.clientAuth);
}
}
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 clientAuth;

/**
* 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 @@ -39,7 +39,11 @@ public enum Type {
/**
* A post request, target is the POST URI
*/
POST
POST,
/**
* X509
*/
X509
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,13 @@ public BasicAuthenticationMechanism get() {
}
};
}

public Supplier<?> setupMtlsClientAuth() {
return new Supplier<MtlsAuthenticationMechanism>() {
@Override
public MtlsAuthenticationMechanism get() {
return new MtlsAuthenticationMechanism();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -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<SecurityIdentity> 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<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().item(new ChallengeData(HttpResponseStatus.UNAUTHORIZED.code(),
null, null));
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(CertificateAuthenticationRequest.class);
}

@Override
public HttpCredentialTransport getCredentialTransport() {
return new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509");
}
}

0 comments on commit 75f17cf

Please sign in to comment.