From 217511a5b05e452aa21f6bc54f335df9bbf22037 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 22 Nov 2023 15:30:58 +0000 Subject: [PATCH] Support certificate role mappings --- .../runtime/X509IdentityProvider.java | 52 +++++++- .../deployment/HttpSecurityProcessor.java | 12 ++ .../vertx/http/runtime/AuthRuntimeConfig.java | 11 ++ .../vertx/http/runtime/CertificateConfig.java | 1 - .../security/HttpSecurityRecorder.java | 61 +++++++++ .../security/MtlsAuthenticationMechanism.java | 14 ++- integration-tests/mtls-certificates/pom.xml | 117 ++++++++++++++++++ .../vertx/CertificateRoleMappingResource.java | 42 +++++++ .../src/main/resources/application.properties | 10 ++ .../src/main/resources/role-mappings.txt | 2 + .../src/main/resources/server-keystore.jks | Bin 0 -> 4369 bytes .../src/main/resources/server-truststore.jks | Bin 0 -> 1834 bytes .../it/vertx/CertificateRoleMappingIT.java | 7 ++ .../it/vertx/CertificateRoleMappingTest.java | 60 +++++++++ .../src/test/resources/client-keystore-1.jks | Bin 0 -> 2214 bytes .../src/test/resources/client-keystore-2.jks | Bin 0 -> 2228 bytes .../src/test/resources/client-truststore.jks | Bin 0 -> 925 bytes integration-tests/pom.xml | 1 + 18 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 integration-tests/mtls-certificates/pom.xml create mode 100644 integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java create mode 100644 integration-tests/mtls-certificates/src/main/resources/application.properties create mode 100644 integration-tests/mtls-certificates/src/main/resources/role-mappings.txt create mode 100644 integration-tests/mtls-certificates/src/main/resources/server-keystore.jks create mode 100644 integration-tests/mtls-certificates/src/main/resources/server-truststore.jks create mode 100644 integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java create mode 100644 integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java create mode 100644 integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks create mode 100644 integration-tests/mtls-certificates/src/test/resources/client-keystore-2.jks create mode 100644 integration-tests/mtls-certificates/src/test/resources/client-truststore.jks 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 index ae1ff274da163..d7bcff7deb67c 100644 --- 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 @@ -1,6 +1,13 @@ package io.quarkus.security.runtime; import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.x500.X500Principal; import jakarta.inject.Singleton; @@ -12,6 +19,8 @@ @Singleton public class X509IdentityProvider implements IdentityProvider { + private static final String COMMON_NAME = "CN"; + private static final String ROLES_ATTRIBUTE = "roles"; @Override public Class getRequestType() { @@ -21,10 +30,51 @@ public Class getRequestType() { @Override public Uni authenticate(CertificateAuthenticationRequest request, AuthenticationRequestContext context) { X509Certificate certificate = request.getCertificate().getCertificate(); - + Map> roles = request.getAttribute(ROLES_ATTRIBUTE); return Uni.createFrom().item(QuarkusSecurityIdentity.builder() .setPrincipal(certificate.getSubjectX500Principal()) .addCredential(request.getCertificate()) + .addRoles(extractRoles(certificate, roles)) .build()); } + + private Set extractRoles(X509Certificate certificate, Map> roles) { + if (roles == null) { + return Set.of(); + } + X500Principal principal = certificate.getSubjectX500Principal(); + if (principal == null || principal.getName() == null) { + return Set.of(); + } + Set matchedRoles = roles.get(principal.getName()); + if (matchedRoles != null) { + return matchedRoles; + } + String commonName = getCommonName(principal); + if (commonName != null) { + matchedRoles = roles.get(commonName); + if (matchedRoles != null) { + return matchedRoles; + } + } + return Set.of(); + } + + private static String getCommonName(X500Principal principal) { + try { + LdapName ldapDN = new LdapName(principal.getName()); + + // Apparently for some CN variations it might not produce correct results + // Can be tuned as necessary. + for (Rdn rdn : ldapDN.getRdns()) { + if (COMMON_NAME.equals(rdn.getType())) { + return rdn.getValue().toString(); + } + } + } catch (InvalidNameException ex) { + // Failing the augmentation process because of this exception seems unnecessary + // The common name my include some characters unexpected by the legacy LdapName API specification. + } + return null; + } } 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 e414b69b3f7a9..2375e244ddb28 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 @@ -30,6 +30,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.security.spi.runtime.MethodDescription; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; +import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.quarkus.vertx.http.runtime.management.ManagementInterfaceBuildTimeConfig; import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage; @@ -92,6 +93,17 @@ AdditionalBeanBuildItem initMtlsClientAuth(HttpBuildTimeConfig buildTimeConfig) return null; } + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + void setMtlsCertificateRoleProperties( + HttpSecurityRecorder recorder, + HttpConfiguration config, + HttpBuildTimeConfig buildTimeConfig) { + if (isMtlsClientAuthenticationEnabled(buildTimeConfig)) { + recorder.setMtlsCertificateRoleProperties(config); + } + } + @BuildStep(onlyIf = IsApplicationBasicAuthRequired.class) AdditionalBeanBuildItem initBasicAuth(HttpBuildTimeConfig buildTimeConfig, BuildProducer annotationsTransformerProducer, diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java index 8552e30ef6b6b..f601117ff07e9 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/AuthRuntimeConfig.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.runtime; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; @@ -24,6 +25,16 @@ public class AuthRuntimeConfig { @ConfigItem(name = "policy") public Map rolePolicy; + /** + * Properties file containing the client certificate common name (CN) to role mappings. + * Use it only if the mTLS authentication mechanism is enabled with either + * `quarkus.http.ssl.client-auth=required` or `quarkus.http.ssl.client-auth=request`. + *

+ * Properties file is expected to have the `CN=role1,role,...,roleN` format and should be encoded using UTF-8. + */ + @ConfigItem + public Optional certificateRoleProperties; + /** * The authentication realm */ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java index 9666a72d9f2db..377a59dc44a70 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/CertificateConfig.java @@ -14,7 +14,6 @@ * Provide either the certificate and key files or a keystore. */ @ConfigGroup -@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class CertificateConfig { /** 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 65d25a92394a8..6345c47d66458 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 @@ -1,7 +1,18 @@ package io.quarkus.vertx.http.runtime.security; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.CompletionException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -13,8 +24,11 @@ import org.jboss.logging.Logger; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.security.AuthenticationCompletionException; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; @@ -362,4 +376,51 @@ private synchronized void setPatchMatchingPolicyEnabled() { protected abstract boolean httpPermissionsEmpty(); } + + public void setMtlsCertificateRoleProperties(HttpConfiguration config) { + InstanceHandle mtls = Arc.container().instance(MtlsAuthenticationMechanism.class); + + if (mtls.isAvailable() && config.auth.certificateRoleProperties.isPresent()) { + Path rolesPath = config.auth.certificateRoleProperties.get(); + URL rolesResource = null; + if (Files.exists(rolesPath)) { + try { + rolesResource = rolesPath.toUri().toURL(); + } catch (MalformedURLException e) { + // The Files.exists(rolesPath) check has succeeded therefore this exception can't happen in this case + } + } else { + rolesResource = Thread.currentThread().getContextClassLoader().getResource(rolesPath.toString()); + } + if (rolesResource == null) { + throw new ConfigurationException( + "quarkus.http.auth.certificate-role-properties location can not be resolved", + Set.of("quarkus.http.auth.certificate-role-properties")); + } + + try (Reader reader = new BufferedReader( + new InputStreamReader(rolesResource.openStream(), StandardCharsets.UTF_8))) { + Properties rolesProps = new Properties(); + rolesProps.load(reader); + + Map> roles = new HashMap<>(); + for (Map.Entry e : rolesProps.entrySet()) { + log.debugf("Added role mapping for %s:%s", e.getKey(), e.getValue()); + roles.put((String) e.getKey(), parseRoles((String) e.getValue())); + } + + mtls.get().setRoleMappings(roles); + } catch (Exception e) { + log.warnf("Unable to read roles mappings from %s:%s", rolesPath, e.getMessage()); + } + } + } + + private static Set parseRoles(String value) { + Set roles = new HashSet<>(); + for (String s : value.split(",")) { + roles.add(s.trim()); + } + return Set.copyOf(roles); + } } 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 index 32ad48bdf07fe..ee95746c7006c 100644 --- 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 @@ -20,6 +20,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Collections; +import java.util.Map; import java.util.Set; import javax.net.ssl.SSLPeerUnverifiedException; @@ -38,6 +39,8 @@ * The authentication handler responsible for mTLS client authentication */ public class MtlsAuthenticationMechanism implements HttpAuthenticationMechanism { + private static final String ROLES_ATTRIBUTE = "roles"; + Map> roles = Map.of(); @Override public Uni authenticate(RoutingContext context, @@ -56,9 +59,12 @@ public Uni authenticate(RoutingContext context, return Uni.createFrom().nullItem(); } context.put(HttpAuthenticationMechanism.class.getName(), this); + + AuthenticationRequest authRequest = new CertificateAuthenticationRequest( + new CertificateCredential(X509Certificate.class.cast(certificate))); + authRequest.setAttribute(ROLES_ATTRIBUTE, roles); return identityProviderManager - .authenticate(HttpSecurityUtils.setRoutingContextAttribute(new CertificateAuthenticationRequest( - new CertificateCredential(X509Certificate.class.cast(certificate))), context)); + .authenticate(HttpSecurityUtils.setRoutingContextAttribute(authRequest, context)); } @Override @@ -76,4 +82,8 @@ public Set> getCredentialTypes() { public Uni getCredentialTransport(RoutingContext context) { return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509")); } + + void setRoleMappings(Map> roles) { + this.roles = Collections.unmodifiableMap(roles); + } } diff --git a/integration-tests/mtls-certificates/pom.xml b/integration-tests/mtls-certificates/pom.xml new file mode 100644 index 0000000000000..c62a8e7a0a050 --- /dev/null +++ b/integration-tests/mtls-certificates/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-mtls-certificates + Quarkus - Integration Tests - mTLS Client Certificate tests + + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-security + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + io.quarkus + quarkus-security-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-reactive-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + native + + + native + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + native + + + + diff --git a/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java b/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java new file mode 100644 index 0000000000000..d42ec1fc65425 --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/java/io/quarkus/it/vertx/CertificateRoleMappingResource.java @@ -0,0 +1,42 @@ +package io.quarkus.it.vertx; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/protected") +public class CertificateRoleMappingResource { + + @Inject + SecurityIdentity identity; + + @Authenticated + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authenticated") + public String name() { + return identity.getPrincipal().getName(); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authorized-user") + @RolesAllowed("user") + public String authorizedName() { + return identity.getPrincipal().getName(); + } + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("/authorized-admin") + @RolesAllowed("admin") + public String authorizedAdmin() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/mtls-certificates/src/main/resources/application.properties b/integration-tests/mtls-certificates/src/main/resources/application.properties new file mode 100644 index 0000000000000..871c0f0ee71bb --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/resources/application.properties @@ -0,0 +1,10 @@ +quarkus.http.ssl.certificate.key-store-file=server-keystore.jks +quarkus.http.ssl.certificate.key-store-password=password +quarkus.http.ssl.certificate.key-store-key-alias=server +quarkus.http.ssl.certificate.key-store-key-password=serverpw +quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks +quarkus.http.ssl.certificate.trust-store-password=password +quarkus.http.ssl.client-auth=REQUIRED +quarkus.http.auth.certificate-role-properties=role-mappings.txt +quarkus.native.additional-build-args=-H:IncludeResources=.*\\.jks,-H:IncludeResources=.*\\.txt + diff --git a/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt b/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt new file mode 100644 index 0000000000000..506946d5cdf50 --- /dev/null +++ b/integration-tests/mtls-certificates/src/main/resources/role-mappings.txt @@ -0,0 +1,2 @@ +client=user,admin +localhost=user \ No newline at end of file diff --git a/integration-tests/mtls-certificates/src/main/resources/server-keystore.jks b/integration-tests/mtls-certificates/src/main/resources/server-keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..c7ac8b12c43bf6b7050d904fe0795f9c0d25d142 GIT binary patch literal 4369 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 XRn(8T1wfSHMr ziHSv4lQ-0WmyJ`a&7hN3K;N#xLm^Qg{6r_*`>uW5pINtkbwY54YM$3PJVJ?PDXxliGiFr zuaS{~v4N4HnW>4XWfYKWjKrmrE1DRUkiE~y%D~*j$j@NV#K^_e#K_37p+58CiCfPn zS~9P2O*fajx9dQ@*Gm75S5KVrVYdHzEIDvD2j5J1bISOU@YIW3A%}c6$8{e2 z;I3?VdsYVOi`2E3c>@9fe~TF1A$?_o;4qh_y!_-BO$)lpLBfhXP-)RdSk<9~EM z$!~4>{O1w-qEz~4*{yX=2E44bz1tUp`2|GWiW(JU+bp5 z{^7~Q%*epFSkXYfD*5@C-1Qr|jX7}=Z~gg@0}+^pfPu)! zVBx5DIJV^*S1-TMNAXuD4_ z@56T8+25eZ-2Y|s7on%8K1*Gn_fYiIUge6d7bY1-hrh^O^nHeDX?3@?vP;~aX3n<} z+%NsU=B(0H`zL+<<-3=XhaNEbTsB|!P47sbLc|xhV1uQJ396Iq|J$$#>)IpS08xAAS4&KFU#$vi)ygvq5qb%+mj}? zzW?T6%aO-ZUB5kUIb?D38DKX!iOjhOn&Dn*dp^9s}a@YuAhb5L)N4?eIs8-+ce6CO1GTnAgGOh#ec-I)hx32xsP zXf~m?q+Qi>huwN@sh_;hLUwvHh~7LR8R;YF;;LMm9eHP(D8m-n*!(?Z7BAgZLrN15 zY@EL?;7Eo0X>8e#`TNpo4}TsIs_j`mWr^VCX<hnr~OFb>m9~A z1$L=RFBZOfVY2<@l&ZsPIo94f^IS1ja>fcb#{}ML^O=ViC*}XR{ddLU_F@K2`Rh0H zWLA6ryB8aycKQO_YT08~ysovI#jxHxEMl%AUHsvlM)%L-y(x_iF-t;aUp$>QE#vT) z*nUlsyLIdAA8avPGBZn6rAzAGgxMR!RvTn4pB-fW{`}wPocSMqnNiRXZ VY3RnOS!#SO`T5EEan5|{Pym^>tr7qL literal 0 HcmV?d00001 diff --git a/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java new file mode 100644 index 0000000000000..2e33b3dc59c98 --- /dev/null +++ b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.vertx; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class CertificateRoleMappingIT extends CertificateRoleMappingTest { +} diff --git a/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java new file mode 100644 index 0000000000000..5490ca1d97ab1 --- /dev/null +++ b/integration-tests/mtls-certificates/src/test/java/io/quarkus/it/vertx/CertificateRoleMappingTest.java @@ -0,0 +1,60 @@ +package io.quarkus.it.vertx; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; + +@QuarkusTest +public class CertificateRoleMappingTest { + + @TestHTTPResource(ssl = true) + URL url; + + @Test + public void testAuthenticated() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authenticated") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authenticated") + .then().body(equalTo("CN=localhost,OU=quarkus,O=quarkus,L=city,ST=state,C=IE")); + } + + @Test + public void testAuthorizedUser() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authorized-user") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authorized-user") + .then().body(equalTo("CN=localhost,OU=quarkus,O=quarkus,L=city,ST=state,C=IE")); + } + + @Test + public void testAuthorizedAdmin() { + given().spec(getMtlsRequestSpec("client-keystore-1.jks")).get("/protected/authorized-admin") + .then().body(equalTo("CN=client,OU=cert,O=quarkus,L=city,ST=state,C=AU")); + given().spec(getMtlsRequestSpec("client-keystore-2.jks")).get("/protected/authorized-admin") + .then().statusCode(403); + } + + @Test + public void testNoClientCertificate() { + given().get("/protected/authenticated").then().statusCode(401); + given().get("/protected/authorized-user").then().statusCode(401); + given().get("/protected/authorized-admin").then().statusCode(401); + } + + private RequestSpecification getMtlsRequestSpec(String clientKeyStore) { + return new RequestSpecBuilder() + .setBaseUri(String.format("%s://%s", url.getProtocol(), url.getHost())) + .setPort(url.getPort()) + .setKeyStore(clientKeyStore, "password") + .setTrustStore("client-truststore.jks", "password") + .build(); + } +} diff --git a/integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks b/integration-tests/mtls-certificates/src/test/resources/client-keystore-1.jks new file mode 100644 index 0000000000000000000000000000000000000000..cf6d6ba454864d18322799afac37f520673193d6 GIT binary patch literal 2214 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 literal 0 HcmV?d00001 diff --git a/integration-tests/mtls-certificates/src/test/resources/client-truststore.jks b/integration-tests/mtls-certificates/src/test/resources/client-truststore.jks new file mode 100644 index 0000000000000000000000000000000000000000..112fb9857fbd71c688d2bdd6ea2bf647790def70 GIT binary patch literal 925 zcmezO_TO6u1_mY|W(3o$xs}cI8JL?G`56qF7`d357#SJb>>3m5 zZ=H#Lv+59Q`XPs%cW*|zcHdloqAxePpx)#s-v#GfEiaLcc0X6@y`H-%=v#*1=Oe7K z)qW-B>_$?)l+O%qOP zY5rU~k%^g+fpM{-fxLk%FydtSSj1RFgzrhQ6sa|LY5ME@@w;|V$wlpR7jhs1(+w~X z85yJu_Xh_F$};l(IqMm5)_GpSy6dL=5m%H>zWY>H{I`CVzt}sMyehA$jr&-7PpP*z zM48ljF&z(>+8^ZC_}KfmbhqPb-)8xgH)l67pO-z~qM~p}$NWZ#hk(j5-lwf^Z+9IR zc2RuKX)N84ko-LLoWpNbiHME=d^>D69Nu<9Q%!uM)C=*$E z`%UAe|2n@mpUG}^eHO=ZUMDN|=Mhak)*TB!DV*2nlr){SFL#&7L?v}&&wkHmlGl&k z3^#C^8TY=sB=b?#=~rQg60Xlac=V9r%)_@Gyd?kf1a~&vU$Fg3$o1A5_JX^7TZ`vz kyRg-4({opScL4{{nL7JKu02z^Frzkj_mi(*L+#BK0Mrvya{vGU literal 0 HcmV?d00001 diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 69e5ccd2ea296..3e9da6012ddb9 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -416,6 +416,7 @@ istio management-interface management-interface-auth + mtls-certificates virtual-threads