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 4c379ab19188bd8fbea36723f374002da4dde672
Author: Grzegorz Grzybek <[email protected]>
AuthorDate: Fri May 15 11:34:16 2026 +0200

    ARTEMIS-6063 Allow configuration of JWT required claims
---
 .../spi/core/security/jaas/OIDCLoginModule.java    | 11 +++-
 .../spi/core/security/jaas/oidc/OIDCSupport.java   | 15 ++++-
 .../core/security/jaas/OIDCLoginModuleTest.java    | 77 ++++++++++++++++++++++
 docs/user-manual/security.adoc                     |  5 ++
 4 files changed, 105 insertions(+), 3 deletions(-)

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 2df15aebb0..56ff61b0ed 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
@@ -133,7 +133,8 @@ public class OIDCLoginModule implements AuditLoginModule {
     * Set of required JWT claims that should be present (with any value - to 
be validated by different means)
     * in each processed JWT token.
     */
-   private final Set<String> requiredClaims;
+   private Set<String> requiredClaims;
+   private boolean requiredClaimsConfigured = false;
 
    /**
     * "JSON paths" to claims (possibly nested) which should point to JSON 
strings or JSON string arrays, which
@@ -194,6 +195,7 @@ public class OIDCLoginModule implements AuditLoginModule {
     */
    OIDCLoginModule(Set<String> requiredClaims) {
       this.requiredClaims = requiredClaims == null ? defaultRequiredClaims : 
requiredClaims;
+      this.requiredClaimsConfigured = true;
    }
 
    @Override
@@ -223,8 +225,13 @@ public class OIDCLoginModule implements AuditLoginModule {
       Set<String> audience = audiences == null ? null : new 
HashSet<>(Arrays.asList(audiences));
       int maxClockSkew = 
OIDCSupport.intOption(ConfigKey.MAX_CLOCK_SKEW_SECONDS, options);
 
+      String[] requiredClaimsArray = 
OIDCSupport.stringArrayOption(ConfigKey.REQUIRED_CLAIMS, options);
+      if (!requiredClaimsConfigured && requiredClaimsArray != null) {
+         this.requiredClaims = new 
HashSet<>(Arrays.asList(requiredClaimsArray));
+      }
+
       DefaultJWTClaimsVerifier<JWKSecurityContext> claimsVerifier = new 
DefaultJWTClaimsVerifier<>(
-            audience, exactMatchClaims, requiredClaims, prohibitedClaims
+            audience, exactMatchClaims, this.requiredClaims, prohibitedClaims
       );
       claimsVerifier.setMaxClockSkew(maxClockSkew);
 
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 f6860a6e41..36f1f78a22 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
@@ -135,7 +135,17 @@ public class OIDCSupport {
       if (v instanceof String s) {
          vs = s;
       }
-      return vs == null ? null : vs.split("\\s*,\\s*");
+      String[] values = vs == null ? null : vs.split("\\s*,\\s*");
+      if (values != null) {
+         List<String> result = new ArrayList<>(values.length);
+         for (String value : values) {
+            if (value != null && !value.trim().isEmpty()) {
+               result.add(value);
+            }
+         }
+         return result.toArray(new String[0]);
+      }
+      return null;
    }
 
    /**
@@ -390,6 +400,9 @@ public class OIDCSupport {
       // comma-separated required/expected audience ("aud" string/string[] 
claim)
       AUDIENCE("audience", null),
 
+      // comma-separated required claims (must exist, but validation is 
performed with other options, like "audience")
+      REQUIRED_CLAIMS("requiredClaims", "aud, iss, sub, azp, exp"),
+
       // comma-separated "json paths" to fields (could be nested using "." 
separator, but no complex array navigation.
       // just field1.field2.xxx) with the identity of the caller. For Keycloak 
it could be:
       // "preferred_username": from "profile" client scope  -> "User 
Attribute" mapper, "username" field
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 6bc3aaa39a..c97b4f6fca 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
@@ -222,6 +222,83 @@ public class OIDCLoginModuleTest {
       lm.validateToken(JWTParser.parse(token4));
    }
 
+   @Test
+   public void plainJWTWithDefaultRequiredClaims() throws BadJOSEException, 
ParseException, JOSEException {
+      OIDCLoginModule lm = new OIDCLoginModule();
+      Subject subject = new Subject();
+      lm.initialize(subject, new JaasCallbackHandler(null, null, null), null, 
configMap(
+            OIDCSupport.ConfigKey.ALLOW_PLAIN_JWT.getName(), "true"
+      ));
+
+      String jwt = new PlainJWT(new JWTClaimsSet.Builder()
+            .audience("anything")
+            .claim("azp", "authorized-party")
+            .claim("iss", "some-issuer")
+            .claim("sub", "some-subject")
+            .expirationTime(new Date(new Date().getTime() + 5000L))
+            .build()).serialize();
+      lm.validateToken(JWTParser.parse(jwt));
+   }
+
+   @Test
+   public void plainJWTWithCustomRequiredClaims() throws BadJOSEException, 
ParseException, JOSEException {
+      OIDCLoginModule lm = new OIDCLoginModule();
+      Subject subject = new Subject();
+      lm.initialize(subject, new JaasCallbackHandler(null, null, null), null, 
configMap(
+            OIDCSupport.ConfigKey.ALLOW_PLAIN_JWT.getName(), "true",
+            OIDCSupport.ConfigKey.REQUIRED_CLAIMS.getName(), "aud, sub"
+      ));
+
+      String jwt1 = new PlainJWT(new JWTClaimsSet.Builder()
+            .audience("anything")
+            .claim("sub", "some-subject")
+            .build()).serialize();
+      lm.validateToken(JWTParser.parse(jwt1));
+
+      String jwt2 = new PlainJWT(new JWTClaimsSet.Builder()
+            .audience("anything")
+            .build()).serialize();
+      try {
+         lm.validateToken(JWTParser.parse(jwt2));
+         fail("Should fail with missing \"sub\" claim");
+      } catch (BadJWTException e) {
+         assertTrue(e.getMessage().contains("JWT missing required claims: 
[sub]"));
+      }
+   }
+
+   @Test
+   public void plainJWTWithNoRequiredClaims() throws BadJOSEException, 
ParseException, JOSEException {
+      OIDCLoginModule lm = new OIDCLoginModule();
+      Subject subject = new Subject();
+      lm.initialize(subject, new JaasCallbackHandler(null, null, null), null, 
configMap(
+            OIDCSupport.ConfigKey.ALLOW_PLAIN_JWT.getName(), "true",
+            OIDCSupport.ConfigKey.REQUIRED_CLAIMS.getName(), ""
+      ));
+
+      String jwt1 = new PlainJWT(new JWTClaimsSet.Builder()
+            .build()).serialize();
+      lm.validateToken(JWTParser.parse(jwt1));
+   }
+
+   @Test
+   public void plainJWTWithoutCustomClaims() throws BadJOSEException, 
ParseException, JOSEException {
+      OIDCLoginModule lm = new OIDCLoginModule();
+      Subject subject = new Subject();
+      lm.initialize(subject, new JaasCallbackHandler(null, null, null), null, 
configMap(
+            OIDCSupport.ConfigKey.ALLOW_PLAIN_JWT.getName(), "true",
+            OIDCSupport.ConfigKey.REQUIRED_CLAIMS.getName(), null
+      ));
+
+      String jwt1 = new PlainJWT(new JWTClaimsSet.Builder()
+            .build()).serialize();
+      try {
+         lm.validateToken(JWTParser.parse(jwt1));
+         fail("Should fail with missing default claim");
+      } catch (BadJWTException e) {
+         assertTrue(e.getMessage().contains("JWT missing required claims: 
[aud, azp, exp, iss, sub]"));
+      }
+   }
+
    @Test
    public void plainJWTWithIncorrectDates() throws BadJOSEException, 
JOSEException, ParseException {
       OIDCLoginModule lm = new OIDCLoginModule(NO_CLAIMS);
diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc
index f2cd498910..82c9e5cd07 100644
--- a/docs/user-manual/security.adoc
+++ b/docs/user-manual/security.adoc
@@ -1289,6 +1289,11 @@ audience::
 Comma-separated list of values which must be present in the `aud` (_audience_) 
claim of the JWT token. +
 There's no default value.
 
+requiredClaims::
+Comma-separated list of claim names that should be present (validation of the 
claim values is configured separately)
+in the JWT token. +
+Defaults to `aud, iss, sub, azp, exp` (audience, issuer, subject, authorize 
party, expiration date).
+
 identityPaths::
 Comma-separated _JSON paths_ that point to the fields (direct or nested) in 
the JWT token which contain values (strings, JSON string
 arrays or whitespace-separated strings) used as _user identities_ (translated 
into `org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal` 
principals). +


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

Reply via email to