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]
