This is an automated email from the ASF dual-hosted git repository.
gtully pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/artemis.git
The following commit(s) were added to refs/heads/main by this push:
new c51738ec21 ARTEMIS-5959 add option to externalcert login module to
populate roles from cert san uris
c51738ec21 is described below
commit c51738ec2146afab1d16969975eb1c0083861665
Author: Gary Tully <[email protected]>
AuthorDate: Thu Mar 19 16:49:01 2026 +0000
ARTEMIS-5959 add option to externalcert login module to populate roles from
cert san uris
---
.../jaas/ExternalCertificateLoginModule.java | 44 +++++++++--
.../jaas/ExternalCertificateLoginModuleTest.java | 84 +++++++++++++++++++++
docs/user-manual/security.adoc | 6 ++
tests/security-resources/build.sh | 3 +
tests/security-resources/san-keystore.p12 | Bin 0 -> 4923 bytes
5 files changed, 132 insertions(+), 5 deletions(-)
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
index 3284843aa5..5813852f7c 100644
---
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModule.java
@@ -16,23 +16,26 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
-import java.security.cert.X509Certificate;
import java.io.IOException;
+import java.lang.invoke.MethodHandles;
import java.security.Principal;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import java.lang.invoke.MethodHandles;
-
/**
* A LoginModule that propagates TLS certificates subject DN as a
UserPrincipal.
*/
@@ -40,11 +43,15 @@ public class ExternalCertificateLoginModule implements
AuditLoginModule {
private static final Logger logger =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ public static final String SAN_URI_ROLE_PREFIX_PROP = "sanUriRolePrefix";
+ public static final int SAN_EXT_URI_TYPE = 6;
private CallbackHandler callbackHandler;
private Subject subject;
private String userName;
private final Set<Principal> principals = new HashSet<>();
+ private final Set<Principal> roles = new HashSet<>();
+ private String sanUriRolePrefix;
@Override
public void initialize(Subject subject,
@@ -53,6 +60,9 @@ public class ExternalCertificateLoginModule implements
AuditLoginModule {
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
+ if (options != null && options.containsKey(SAN_URI_ROLE_PREFIX_PROP)) {
+ sanUriRolePrefix =
String.valueOf(options.get(SAN_URI_ROLE_PREFIX_PROP));
+ }
}
@Override
@@ -73,6 +83,29 @@ public class ExternalCertificateLoginModule implements
AuditLoginModule {
userName = certificates[0].getSubjectDN().getName();
}
+ if (userName != null && sanUriRolePrefix != null) {
+ // getSubjectAlternativeNames returns a Collection of Lists
+ // Each inner list is [Integer type, Object value]
+ Collection<List<?>> sans = null;
+ try {
+ sans = certificates[0].getSubjectAlternativeNames();
+ } catch (CertificateParsingException e) {
+ throw new LoginException(e.getMessage());
+ }
+
+ if (sans != null) {
+ for (List<?> san : sans) {
+ int type = (Integer) san.get(0);
+ if (type == SAN_EXT_URI_TYPE) {
+ final String value = (String) san.get(1);
+ if (value != null && value.startsWith(sanUriRolePrefix)) {
+ roles.add(new
RolePrincipal(value.substring(sanUriRolePrefix.length())));
+ }
+ }
+ }
+ }
+ }
+
if (logger.isDebugEnabled()) {
logger.debug("Certificates: {}, userName: {}",
Arrays.toString(certificates), userName);
}
@@ -84,6 +117,7 @@ public class ExternalCertificateLoginModule implements
AuditLoginModule {
public boolean commit() throws LoginException {
if (userName != null) {
principals.add(new UserPrincipal(userName));
+ principals.addAll(roles);
subject.getPrincipals().addAll(principals);
}
diff --git
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModuleTest.java
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModuleTest.java
new file mode 100644
index 0000000000..6f59908c54
--- /dev/null
+++
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/ExternalCertificateLoginModuleTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.activemq.artemis.spi.core.security.jaas;
+
+import org.apache.activemq.artemis.utils.ClassloadingUtil;
+import org.junit.jupiter.api.Test;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ExternalCertificateLoginModuleTest {
+
+ @Test
+ void loginFails() throws LoginException {
+
+ ExternalCertificateLoginModule underTest = new
ExternalCertificateLoginModule();
+
+ final Subject subject = new Subject();
+ underTest.initialize(subject, callbacks -> {
+ }, null, null);
+
+ assertFalse(underTest.login());
+ assertTrue(subject.getPrincipals().isEmpty());
+ }
+
+ @Test
+ void loginSuccess() throws Exception {
+ ExternalCertificateLoginModule underTest = new
ExternalCertificateLoginModule();
+
+ String ksPath =
ClassloadingUtil.findResource("san-keystore.p12").getPath();
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ try (FileInputStream fis = new FileInputStream(ksPath)) {
+ ks.load(fis, "securepass".toCharArray());
+ }
+ X509Certificate cert = (X509Certificate) ks.getCertificate("san-roles");
+
+ Subject subject = new Subject();
+ underTest.initialize(subject, callbacks -> {
+ ((CertificateCallback) callbacks[0]).setCertificates(new
X509Certificate[]{cert});
+ }, null, Map.of(ExternalCertificateLoginModule.SAN_URI_ROLE_PREFIX_PROP,
"urn:jaas:role:"));
+
+ assertTrue(underTest.login());
+ assertTrue(underTest.commit());
+ assertFalse(subject.getPrincipals().isEmpty());
+ assertEquals("CN=ok",
subject.getPrincipals(UserPrincipal.class).iterator().next().getName());
+ assertTrue(subject.getPrincipals(RolePrincipal.class).contains(new
RolePrincipal("admin")));
+ assertTrue(subject.getPrincipals(RolePrincipal.class).contains(new
RolePrincipal("view")));
+
+ // again without the prefix property and same cert
+ underTest = new ExternalCertificateLoginModule();
+ subject = new Subject();
+ underTest.initialize(subject, callbacks -> {
+ ((CertificateCallback) callbacks[0]).setCertificates(new
X509Certificate[]{cert});
+ }, null, null);
+
+ assertTrue(underTest.login());
+ assertTrue(underTest.commit());
+ assertFalse(subject.getPrincipals().isEmpty());
+ assertEquals("CN=ok",
subject.getPrincipals(UserPrincipal.class).iterator().next().getName());
+ assertTrue(subject.getPrincipals(RolePrincipal.class).isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc
index e42a3123c1..47cb672639 100644
--- a/docs/user-manual/security.adoc
+++ b/docs/user-manual/security.adoc
@@ -1125,6 +1125,12 @@ This Principal can then be used for <<role-mapping,role
mapping>>.
The external certificate login module is used to propagate a validated TLS
client certificate's subjectDN into a JAAS UserPrincipal.
This allows subsequent login modules to do role mapping for the TLS client
certificate.
+It is possible to populate RolePrincipals with values from the certificate
Subject Alternative Name(SAN) URIs that match a configured prefix.
+
+sanUriRolePrefix::
+when non-null, this value is used as a prefix to extract role information from
a certificate SAN URIs.
+If keytool was used with -ext
"SAN=uri:urn:jaas:role:admin,uri:urn:jaas:role:view" to populate the
certificate SAN.
+Then a sanUriRolePrefix value of `urn:jaas:role:` would extract them into
subject RolePrincipals
----
org.apache.activemq.artemis.spi.core.security.jaas.ExternalCertificateLoginModule
required
diff --git a/tests/security-resources/build.sh
b/tests/security-resources/build.sh
index c4028648ae..93d6ee2059 100755
--- a/tests/security-resources/build.sh
+++ b/tests/security-resources/build.sh
@@ -179,6 +179,9 @@ keytool -storetype pkcs12 -keystore client-ca-keystore.p12
-storepass $STORE_PAS
## Combined ca-certs pem to verify loading of multiple certs
cat client-ca-cert.pem server-ca-cert.pem > client-and-server-ca-certs.pem
+## a cert for the ExternalCertificateLoginModule
+keytool -storetype pkcs12 -keystore san-keystore.p12 -storepass $STORE_PASS
-keypass $KEY_PASS -alias san-roles -genkey -keyalg "RSA" -keysize 2048 -dname
"CN=ok" -validity $VALIDITY -ext
"SAN=uri:urn:jaas:role:admin,uri:urn:jaas:role:view"
+
# Clean up working files
# -----------------------
rm -f *.crt *.csr openssl-*
diff --git a/tests/security-resources/san-keystore.p12
b/tests/security-resources/san-keystore.p12
new file mode 100644
index 0000000000..e54bbbd01a
Binary files /dev/null and b/tests/security-resources/san-keystore.p12 differ
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]