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]

Reply via email to