This is an automated email from the ASF dual-hosted git repository.

jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/artemis.git

commit 16b28f6ca2323482efdb5c2aaeff6a976b7cf571
Author: Grzegorz Grzybek <[email protected]>
AuthorDate: Fri May 15 11:54:58 2026 +0200

    ARTEMIS-6063 Allow configuration of JWT role mapping
---
 .../spi/core/security/jaas/OIDCLoginModule.java    | 16 +++++-
 .../spi/core/security/jaas/oidc/OIDCSupport.java   | 33 ++++++++++++
 .../core/security/jaas/OIDCLoginModuleTest.java    | 62 ++++++++++++++++++++++
 docs/user-manual/security.adoc                     |  7 +++
 4 files changed, 117 insertions(+), 1 deletion(-)

diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModule.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModule.java
index 56ff61b0ed..9021c53951 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModule.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModule.java
@@ -51,6 +51,7 @@ import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -148,6 +149,11 @@ public class OIDCLoginModule implements AuditLoginModule {
     */
    private String[] rolesPaths;
 
+   /**
+    * Mapping for roles, where a role taken from JWT token can be mapped 
(renamed) to <em>local</em> role.
+    */
+   private Map<String, String> roleMapping = new HashMap<>();
+
    /**
     * <p>Flag which enforces OAuth 2.0 Mutual-TLS Client Authentication and 
Certificate-Bound Access Tokens
     * (RFC 8705). If a token contains {@code cnf/x5t#256} claim, it is always 
verified and mTLS is required.</p>
@@ -243,6 +249,7 @@ public class OIDCLoginModule implements AuditLoginModule {
       // configuration for what to extract from the token
       identityPaths = OIDCSupport.stringArrayOption(ConfigKey.IDENTITY_PATHS, 
options);
       rolesPaths = OIDCSupport.stringArrayOption(ConfigKey.ROLES_PATHS, 
options);
+      roleMapping = OIDCSupport.mappingOption(ConfigKey.ROLE_MAPPING, options);
    }
 
    @Override
@@ -373,7 +380,14 @@ public class OIDCLoginModule implements AuditLoginModule {
                   if (!roles.valid()) {
                      throw new LoginException("Can't determine user role from 
JWT using \"" + rolePath + "\" path");
                   }
-                  rolePrincipalNames.addAll(Arrays.asList(roles.value()));
+                  String[] tab = roles.value();
+                  for (String role : tab) {
+                     String mapped = roleMapping.get(role);
+                     if (mapped == null) {
+                        mapped = role;
+                     }
+                     rolePrincipalNames.add(mapped);
+                  }
                }
                if (debug) {
                   logger.debug("Found roles: {}", String.join(", ", 
rolePrincipalNames));
diff --git 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/oidc/OIDCSupport.java
 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/oidc/OIDCSupport.java
index 36f1f78a22..1fa44d73ea 100644
--- 
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/oidc/OIDCSupport.java
+++ 
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/oidc/OIDCSupport.java
@@ -25,6 +25,8 @@ import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -148,6 +150,31 @@ public class OIDCSupport {
       return null;
    }
 
+   public static Map<String, String> mappingOption(ConfigKey configKey, 
Map<String, ?> options) {
+      Object v = options != null ? options.get(configKey.name) : null;
+
+      String vs = configKey.defaultValue;
+      if (v instanceof String s) {
+         vs = s;
+      }
+      String[] values = vs == null ? null : vs.split("\\s*,\\s*");
+      if (values != null) {
+         Map<String, String> result = new HashMap<>();
+         for (String value : values) {
+            if (value != null && !value.trim().isEmpty() && 
value.contains("=")) {
+               String[] kv = value.split("=", 2);
+               String jwtRole = kv[0].trim();
+               String localRole = kv[1].trim();
+               if (!jwtRole.isEmpty() && !localRole.isEmpty()) {
+                  result.put(jwtRole, localRole);
+               }
+            }
+         }
+         return result;
+      }
+      return Collections.emptyMap();
+   }
+
    /**
     * Initialize the {@link OIDCSupport}, so we can do more configuration 
after calling the constructor
     */
@@ -439,6 +466,12 @@ public class OIDCSupport {
       // Each value referred will be added as JAAS subject "role" principal
       ROLES_PATHS("rolesPaths", null),
 
+      // comma-separated original-role=mapped-role list of role mapping.
+      // Without any mapping, roles found using `rolesPath` option are added 
as role principals to the JAAS subject
+      // However we can configure a literal mapping where JWT role names are 
mapped into other values.
+      // By default no mapping is performed
+      ROLE_MAPPING("roleMapping", null),
+
       // Whether the token should contain cnf/x5t#256 claim according to 
https://datatracker.ietf.org/doc/html/rfc8705
       // When enabled, the field contains a base64url(sha256(der(client 
certificate))) value which SHOULD
       // match the certificate from actual mTLS (as handled by
diff --git 
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModuleTest.java
 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModuleTest.java
index c97b4f6fca..8f3178bd1d 100644
--- 
a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModuleTest.java
+++ 
b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/OIDCLoginModuleTest.java
@@ -1075,6 +1075,68 @@ public class OIDCLoginModuleTest {
       assertTrue(roles.isEmpty());
    }
 
+   @Test
+   public void tokenRolesWithMapping() throws NoSuchAlgorithmException, 
JOSEException, LoginException {
+      KeyPairGenerator kpgRSA = KeyPairGenerator.getInstance("RSA");
+      KeyPair pairRSA = kpgRSA.generateKeyPair();
+
+      List<JWK> keys = new ArrayList<>();
+      // directly from the public key
+      keys.add(new RSAKey.Builder((RSAPublicKey) 
pairRSA.getPublic()).keyID("k1").build());
+
+      Map<String, String> config = configMap(
+            OIDCSupport.ConfigKey.DEBUG.getName(), "true",
+            OIDCSupport.ConfigKey.ROLES_PATHS.getName(), "realm_access.roles",
+            OIDCSupport.ConfigKey.ROLE_MAPPING.getName(), 
"realm_admin=broker_admin, realm_viewer = broker=viewer"
+      );
+
+      OIDCLoginModule lm = new OIDCLoginModule();
+      lm.setOidcSupport(new OIDCSupport(config, true) {
+         @Override
+         public JWKSecurityContext currentContext() {
+            return new JWKSecurityContext(keys);
+         }
+      });
+
+      String uuid = UUID.randomUUID().toString();
+
+      JWTClaimsSet claims = new JWTClaimsSet.Builder()
+            .issuer("http://localhost";)
+            .subject("Alice")
+            .audience("me-the-broker")
+            .claim("sub", uuid)
+            .claim("azp", "artemis-oidc-client")
+            .claim("realm_access", Map.of("roles", List.of("realm_admin", 
"realm_manager", "realm_viewer")))
+            .expirationTime(new Date(new Date().getTime() + 3_600_000))
+            .build();
+
+      SignedJWT signedJWT = new SignedJWT(new 
JWSHeader.Builder(JWSAlgorithm.RS256).keyID("k1").build(), claims);
+      JWSSigner signer = new RSASSASigner(pairRSA.getPrivate());
+      signedJWT.sign(signer);
+      String token = signedJWT.serialize();
+
+      Subject subject = new Subject();
+      lm.initialize(subject, new JaasCallbackHandler(null, token, null), null, 
config);
+
+      assertTrue(lm.login());
+      assertTrue(lm.commit());
+
+      Set<Principal> principals = subject.getPrincipals();
+      assertEquals(4, principals.size());
+      Set<String> identities = new HashSet<>(Set.of(uuid));
+      // one should be mapped, the other should be used as in the token
+      Set<String> roles = new HashSet<>(Set.of("broker_admin", 
"realm_manager", "broker=viewer"));
+      principals.forEach(principal -> {
+         if (principal.getClass() == UserPrincipal.class) {
+            identities.remove(principal.getName());
+         } else if (principal.getClass() == RolePrincipal.class) {
+            roles.remove(principal.getName());
+         }
+      });
+      assertTrue(identities.isEmpty());
+      assertTrue(roles.isEmpty());
+   }
+
    @Test
    public void wrongPathsForToken() throws NoSuchAlgorithmException, 
JOSEException, LoginException {
       KeyPairGenerator kpgRSA = KeyPairGenerator.getInstance("RSA");
diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc
index 82c9e5cd07..9c0a158d03 100644
--- a/docs/user-manual/security.adoc
+++ b/docs/user-manual/security.adoc
@@ -1304,6 +1304,13 @@ Comma-separated _JSON paths_ that point to the fields 
(direct or nested) in the
 arrays or whitespace-separated strings) used as _user roles_ (translated into 
`org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal` principals). 
+
 There's no default value. For Keycloak OpenID Connect provider it could be for 
example `realm_access.roles`.
 
+roleMapping::
+Comma-separated list of `jwtRole=localRole` mappings. +
+This option allows configuration of possible _role mapping_ if the roles 
present (as configured by `rolesPaths` option) in the JWT
+token should be _translated_ into other role names. This behavior is usually 
specific to LDAP role mapping and is easy to avoid
+with JWT, but it is still an option.
+Defaults to no mapping and roles are taken directly from JWT token.
+
 requireOAuth2MTLS::
 This option adds extra layer of security and enforces 
https://datatracker.ietf.org/doc/html/rfc8705[OAuth 2.0 Mutual-TLS Client 
Authentication and Certificate-Bound Access Tokens]. +
 With this option enabled, JWT tokens must include `cnf/x5t#256` claim which 
contains


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to