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
commit d8cd96a1b45237287d81b6a64cd7e987ec726fb2 Author: Grzegorz Grzybek <[email protected]> AuthorDate: Wed Apr 1 14:42:32 2026 +0200 ARTEMIS-5200 Clarify configuration of Certificate-Bound JWT Access Tokens --- .../spi/core/security/jaas/OIDCLoginModule.java | 23 ++++----- .../spi/core/security/jaas/oidc/OIDCSupport.java | 6 +-- .../core/security/jaas/OIDCLoginModuleTest.java | 59 ++++++++++++++++++++++ artemis-server/src/test/resources/login.config | 5 +- docs/user-manual/security.adoc | 11 ++-- 5 files changed, 84 insertions(+), 20 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 edc398b3fc..2df15aebb0 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 @@ -148,8 +148,9 @@ public class OIDCLoginModule implements AuditLoginModule { private String[] rolesPaths; /** - * <p>Flag which turns on OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens - * (RFC 8705).</p> + * <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> + * * <p>{@code cnf} claim itself comes from RFC 7800 (Proof-of-Possession Key Semantics for JSON Web Tokens (JWTs)) * and represents a proof that the token was issued for the actual sender (and was not stolen). {@code x5t#256} * is a specific type of proof from RFC 7515 (JSON Web Signature (JWS)) and represents an SHA-256 digest @@ -245,14 +246,10 @@ public class OIDCLoginModule implements AuditLoginModule { JWT jwt; JWTClaimsSet claims = null; try { - CertificateCallback x509Callback = null; + CertificateCallback x509Callback = new CertificateCallback(); JwtCallback jwtCallback = new JwtCallback(); - if (requireOAuth2MTLS) { - x509Callback = new CertificateCallback(); - handler.handle(new Callback[] {x509Callback, jwtCallback}); - } else { - handler.handle(new Callback[] {jwtCallback}); - } + // attempt to get the certificate in all the cases - to use it if JWT contains the cnf claim + handler.handle(new Callback[] {x509Callback, jwtCallback}); String token = jwtCallback.getJwtToken(); @@ -278,7 +275,7 @@ public class OIDCLoginModule implements AuditLoginModule { // we may want to verify the proof of possession even if it's not required, but the claim is // present in the token String thumbprint = OIDCSupport.getThumbprint(claims); - X509Certificate[] certificates = x509Callback == null ? new X509Certificate[0] : x509Callback.getCertificates(); + X509Certificate[] certificates = x509Callback.getCertificates(); if (requireOAuth2MTLS || thumbprint != null) { String msg = null; @@ -303,10 +300,12 @@ public class OIDCLoginModule implements AuditLoginModule { } if (debug) { - if (requireOAuth2MTLS) { + if (requireOAuth2MTLS || thumbprint != null) { + // if there's no LoginException, the cnf claim is ensured to be in the token + String digest = OIDCSupport.stringArrayForPath(claims, "cnf.x5t#256").value()[0]; logger.debug("JAAS login successful for JWT token with {} and X.509 thumbprint {}", OIDCSupport.getTokenSummary(claims), - OIDCSupport.stringArrayForPath(claims, "cnf.x5t#256").value()[0]); + digest); } else { logger.debug("JAAS login successful for JWT token with {}", OIDCSupport.getTokenSummary(claims)); } 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 08672e2b0a..92f0a02b35 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 @@ -430,9 +430,9 @@ public class OIDCSupport { // When enabled, the field contains a base64url(sha256(der(client certificate))) value which SHOULD // match the certificate from actual mTLS (as handled by // org.apache.activemq.artemis.spi.core.security.jaas.CertificateLoginModule - but this module is not - // required as a prerequisite of OIDCLoginModule, as it doesn't put the certificate as "public credential") - // this flag defaults to false, but when cnf/x5t#256 is present in the token, it's used - and the - // validation fails if there's no underlying mTLS + // required as a prerequisite of OIDCLoginModule, as it doesn't put the certificate into "public credentials"). + // This flag defaults to false, but when cnf/x5t#256 is present in the token, the proof-of-possession + // validation is performed regardless. REQUIRE_OAUTH_MTLS("requireOAuth2MTLS", "false"); private final String name; 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 dfd939033c..28ab28ce43 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 @@ -788,6 +788,65 @@ public class OIDCLoginModuleTest { assertTrue(lm.login()); } + @Test + public void correctProofOfPossessionWithoutExplicitConfiguration() 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", + // set to false (the default), but should be enforced for tokens with cnf/x5t#256 claim + OIDCSupport.ConfigKey.REQUIRE_OAUTH_MTLS.getName(), "false" + ); + + OIDCLoginModule lm = new OIDCLoginModule(); + lm.setOidcSupport(new OIDCSupport(config, true) { + @Override + public JWKSecurityContext currentContext() { + return new JWKSecurityContext(keys); + } + }); + + StubX509Certificate cert = new StubX509Certificate(new UserPrincipal("Alice")) { + @Override + public byte[] getEncoded() { + // see for example org.keycloak.crypto.elytron.ElytronPEMUtilsProvider#encode() + return new byte[] {0x42, 0x2a}; + } + }; + + byte[] digest = MessageDigest.getInstance("SHA256").digest(cert.getEncoded()); + String x5t = Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer("http://localhost") + .subject("Alice") + .jwtID(UUID.randomUUID().toString()) + .audience(List.of("me-the-broker", "some-other-api")) + .claim("azp", "artemis-oidc-client") + .claim("cnf", Map.of("x5t#256", x5t)) + .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(); + + RemotingConnection remotingConnection = mock(RemotingConnection.class); + NettyServerConnection nettyConnection = mock(NettyServerConnection.class); + when(remotingConnection.getTransportConnection()).thenReturn(nettyConnection); + when(nettyConnection.getPeerCertificates()).thenReturn(new X509Certificate[] {cert}); + + Subject subject = new Subject(); + lm.initialize(subject, new JaasCallbackHandler(null, token, remotingConnection), null, config); + + assertTrue(lm.login()); + } + @Test public void correctProofOfPossessionButNotConfiguredAndWithoutMTLS() throws NoSuchAlgorithmException, JOSEException { KeyPairGenerator kpgRSA = KeyPairGenerator.getInstance("RSA"); diff --git a/artemis-server/src/test/resources/login.config b/artemis-server/src/test/resources/login.config index c34edf82aa..085d54a696 100644 --- a/artemis-server/src/test/resources/login.config +++ b/artemis-server/src/test/resources/login.config @@ -235,8 +235,9 @@ OIDCLogin { identityPaths=sub // comma-separated "json paths" to JWT fields with the roles of the caller rolesPaths="realm_access.roles" - // Whether the token should contain cnf/x5t#256 claim according RFC 8705 (requires mTLS enabled) - // defaults to false, unless the token contains cnf/x5t#256 - then it's treated as enabled + // Enforces cnf/x5t#256 claim validation according to RFC 8705 (requires mTLS enabled) + // defaults to false, unless the token contains cnf/x5t#256 - then the validation + // is performed regardless of this option's value requireOAuth2MTLS=false // these use defaults values: diff --git a/docs/user-manual/security.adoc b/docs/user-manual/security.adoc index 2fa2d8e460..d4dc40b063 100644 --- a/docs/user-manual/security.adoc +++ b/docs/user-manual/security.adoc @@ -1309,9 +1309,14 @@ arrays or whitespace-separated strings) used as _user roles_ (translated into `o There's no default value. For Keycloak OpenID Connect provider it could be for example `realm_access.roles`. requireOAuth2MTLS:: -This option adds extra layer of security and enables 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 -a SHA256 digest of client's X.509 certificate. This certificate should match a certificate from TLS layer and requires enabled mTLS at the broker level. + -Defaults to `false`. But when the token is sent with `cnf/x5t#256`, the option is ignored and treated as enabled. +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 +a SHA256 digest of client's X.509 certificate. This certificate should match a certificate from TLS layer and requires mTLS +being enabled at the broker level. + +This mechanism protects against token stealing, making the tokens valid only in the +context of mTLS connection with a matching certificate. + +Defaults to `false`. But when the JWT token is sent with `cnf/x5t#256` claim, the validation is performed regardless of this +option's value. === SCRAM-SHA SASL Mechanism --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
