Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support certificate role mappings #37269

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -12,6 +19,8 @@

@Singleton
public class X509IdentityProvider implements IdentityProvider<CertificateAuthenticationRequest> {
private static final String COMMON_NAME = "CN";
private static final String ROLES_ATTRIBUTE = "roles";

@Override
public Class<CertificateAuthenticationRequest> getRequestType() {
Expand All @@ -21,10 +30,51 @@ public Class<CertificateAuthenticationRequest> getRequestType() {
@Override
public Uni<SecurityIdentity> authenticate(CertificateAuthenticationRequest request, AuthenticationRequestContext context) {
X509Certificate certificate = request.getCertificate().getCertificate();

Map<String, Set<String>> roles = request.getAttribute(ROLES_ATTRIBUTE);
return Uni.createFrom().item(QuarkusSecurityIdentity.builder()
.setPrincipal(certificate.getSubjectX500Principal())
.addCredential(request.getCertificate())
.addRoles(extractRoles(certificate, roles))
.build());
}

private Set<String> extractRoles(X509Certificate certificate, Map<String, Set<String>> roles) {
if (roles == null) {
return Set.of();
}
X500Principal principal = certificate.getSubjectX500Principal();
if (principal == null || principal.getName() == null) {
return Set.of();
}
Set<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<AnnotationsTransformerBuildItem> annotationsTransformerProducer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.vertx.http.runtime;

import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;

Expand All @@ -24,6 +25,16 @@ public class AuthRuntimeConfig {
@ConfigItem(name = "policy")
public Map<String, PolicyConfig> 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`.
* <p/>
* Properties file is expected to have the `CN=role1,role,...,roleN` format and should be encoded using UTF-8.
*/
@ConfigItem
public Optional<Path> certificateRoleProperties;

/**
* The authentication realm
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* Provide either the certificate and key files or a keystore.
*/
@ConfigGroup
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class CertificateConfig {

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -362,4 +376,51 @@ private synchronized void setPatchMatchingPolicyEnabled() {

protected abstract boolean httpPermissionsEmpty();
}

public void setMtlsCertificateRoleProperties(HttpConfiguration config) {
InstanceHandle<MtlsAuthenticationMechanism> 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<String, Set<String>> roles = new HashMap<>();
for (Map.Entry<Object, Object> 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<String> parseRoles(String value) {
Set<String> roles = new HashSet<>();
for (String s : value.split(",")) {
roles.add(s.trim());
}
return Set.copyOf(roles);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String, Set<String>> roles = Map.of();

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context,
Expand All @@ -56,9 +59,12 @@ public Uni<SecurityIdentity> 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
Expand All @@ -76,4 +82,8 @@ public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509"));
}

void setRoleMappings(Map<String, Set<String>> roles) {
this.roles = Collections.unmodifiableMap(roles);
}
}
117 changes: 117 additions & 0 deletions integration-tests/mtls-certificates/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>quarkus-integration-tests-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>quarkus-integration-test-mtls-certificates</artifactId>
<name>Quarkus - Integration Tests - mTLS Client Certificate tests</name>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>
Loading
Loading