This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 4.1.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit c418b593c8a295a6572b3ae6ba37586872e07f33 Author: Naveed Khan <[email protected]> AuthorDate: Wed Oct 28 15:44:59 2026 +0530 ignore x5c and x5t key headers in JWTTokenValidator (#3636) * ignore x5c and x5t key headers in JWTTokenValidator * make x5c and x5t key headers opt-in via allowX509KeyHeaders in JWTTokenValidator (cherry picked from commit 952b990a897fdf4f60157bb43cd6efd2ee21a831) --- .../sts/token/validator/jwt/JWTTokenValidator.java | 26 ++++++- .../sts/token/validator/JWTTokenValidatorTest.java | 84 ++++++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java index d699ebdda19..d35c7e84766 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/jwt/JWTTokenValidator.java @@ -49,7 +49,7 @@ import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.crypto.Merlin; /** - * Validate a SAML Assertion. It is valid if it was issued and signed by this STS. + * Validate a JWT Token. It is valid if it was issued and signed by this STS. */ public class JWTTokenValidator implements TokenValidator { @@ -58,6 +58,7 @@ public class JWTTokenValidator implements TokenValidator { private int ttl; private JWTRoleParser roleParser; private JWTRealmCodec realmCodec; + private boolean allowX509KeyHeaders; /** * Return true if this TokenValidator implementation is capable of validating the @@ -130,8 +131,12 @@ public class JWTTokenValidator implements TokenValidator { KeyStore keystore = ((Merlin)signatureCrypto).getKeyStore(); verificationProperties.put(JoseConstants.RSSEC_KEY_STORE, keystore); + // Unless allowX509KeyHeaders is enabled, the token headers are not passed on, so the token + // is verified with the configured signature certificate only. Otherwise x5c, x5t and + // x5t#S256 could select any certificate in the keystore as the verification key. JwsSignatureVerifier signatureVerifier = - JwsUtils.loadSignatureVerifier(verificationProperties, jwt.getJwsHeaders()); + JwsUtils.loadSignatureVerifier(verificationProperties, + allowX509KeyHeaders ? jwt.getJwsHeaders() : null); if (!jwtConsumer.verifySignatureWith(signatureVerifier)) { return response; @@ -210,4 +215,21 @@ public class JWTTokenValidator implements TokenValidator { public void setRealmCodec(JWTRealmCodec realmCodec) { this.realmCodec = realmCodec; } + + public boolean isAllowX509KeyHeaders() { + return allowX509KeyHeaders; + } + + /** + * Whether to honor the x5c, x5t and x5t#S256 headers of the received token when selecting the + * verification certificate. The default is false, meaning the token is only verified with the + * certificate of the configured signature username. + * + * Enabling this makes every certificate in the STS signature crypto a trusted JWT issuer, and + * with x5c, so is anyone holding a certificate issued by a CA in that keystore. If it is enabled, + * ideally use a dedicated signature crypto that is not also used as a trust store. + */ + public void setAllowX509KeyHeaders(boolean allowX509KeyHeaders) { + this.allowX509KeyHeaders = allowX509KeyHeaders; + } } diff --git a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java index 59304d343b8..3312cb139d0 100644 --- a/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java +++ b/services/sts/sts-core/src/test/java/org/apache/cxf/sts/token/validator/JWTTokenValidatorTest.java @@ -20,6 +20,8 @@ package org.apache.cxf.sts.token.validator; import java.io.IOException; import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.Properties; import java.util.Set; @@ -30,10 +32,16 @@ import javax.security.auth.callback.UnsupportedCallbackException; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.apache.cxf.common.util.Base64UrlUtility; import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.jaxws.context.WrappedMessageContext; import org.apache.cxf.message.MessageImpl; +import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm; +import org.apache.cxf.rs.security.jose.jws.JwsHeaders; +import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer; +import org.apache.cxf.rs.security.jose.jws.JwsUtils; import org.apache.cxf.rs.security.jose.jwt.JwtClaims; +import org.apache.cxf.rt.security.crypto.MessageDigestUtils; import org.apache.cxf.sts.STSConstants; import org.apache.cxf.sts.StaticSTSProperties; import org.apache.cxf.sts.cache.DefaultInMemoryTokenStore; @@ -56,9 +64,11 @@ import org.apache.cxf.ws.security.tokenstore.TokenStore; import org.apache.cxf.ws.security.tokenstore.TokenStoreException; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.crypto.CryptoFactory; +import org.apache.wss4j.common.crypto.CryptoType; import org.apache.wss4j.common.ext.WSPasswordCallback; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.principal.CustomTokenPrincipal; +import org.apache.wss4j.dom.engine.WSSConfig; import org.junit.BeforeClass; @@ -157,6 +167,69 @@ public class JWTTokenValidatorTest { assertTrue(validatorResponse.getToken().getState() == STATE.INVALID); } + @org.junit.Test + public void testTokenSignedByTrustedCertificate() throws Exception { + // The STS keystore also holds the trusted certificate of "myclientkey". A token signed with + // the client's own key must not be accepted as issued by the STS, even if its x5t header + // points at that certificate. + TokenValidatorResponse validatorResponse = + validateTokenSignedByTrustedCertificate(new JWTTokenValidator()); + assertTrue(validatorResponse.getToken().getState() == STATE.INVALID); + } + + @org.junit.Test + public void testTokenSignedByTrustedCertificateWithX509KeyHeadersAllowed() throws Exception { + // With allowX509KeyHeaders enabled, the x5t header selects the trusted client certificate + JWTTokenValidator jwtTokenValidator = new JWTTokenValidator(); + jwtTokenValidator.setAllowX509KeyHeaders(true); + + TokenValidatorResponse validatorResponse = + validateTokenSignedByTrustedCertificate(jwtTokenValidator); + assertTrue(validatorResponse.getToken().getState() == STATE.VALID); + assertEquals("admin", validatorResponse.getPrincipal().getName()); + } + + private TokenValidatorResponse validateTokenSignedByTrustedCertificate( + TokenValidator jwtTokenValidator) throws Exception { + WSSConfig.init(); + Crypto clientCrypto = CryptoFactory.getInstance(getClientCryptoProperties()); + CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS); + cryptoType.setAlias("myclientkey"); + X509Certificate clientCert = clientCrypto.getX509Certificates(cryptoType)[0]; + PrivateKey clientKey = clientCrypto.getPrivateKey("myclientkey", "cspass"); + + JwsHeaders headers = new JwsHeaders(SignatureAlgorithm.RS256); + headers.setX509Thumbprint(Base64UrlUtility.encode( + MessageDigestUtils.createDigest(clientCert.getEncoded(), MessageDigestUtils.ALGO_SHA_1))); + + JwtClaims claims = new JwtClaims(); + claims.setIssuer("STS"); + claims.setSubject("admin"); + long now = System.currentTimeMillis() / 1000L; + claims.setIssuedAt(now); + claims.setExpiryTime(now + 300L); + + String token = new JwsJwtCompactProducer(headers, claims).signWith( + JwsUtils.getPrivateKeySignatureProvider(clientKey, SignatureAlgorithm.RS256)); + + // Validate the token + TokenValidatorParameters validatorParameters = createValidatorParameters(); + TokenRequirements tokenRequirements = validatorParameters.getTokenRequirements(); + + // Create a ValidateTarget consisting of a JWT Token + ReceivedToken validateTarget = new ReceivedToken(createTokenWrapper(token)); + tokenRequirements.setValidateTarget(validateTarget); + validatorParameters.setToken(validateTarget); + + assertTrue(jwtTokenValidator.canHandleToken(validateTarget)); + + TokenValidatorResponse validatorResponse = + jwtTokenValidator.validateToken(validatorParameters); + assertNotNull(validatorResponse); + assertNotNull(validatorResponse.getToken()); + return validatorResponse; + } + @org.junit.Test public void testUnsignedToken() throws Exception { // Create @@ -423,6 +496,17 @@ public class JWTTokenValidatorTest { return properties; } + private Properties getClientCryptoProperties() { + Properties properties = new Properties(); + properties.put( + "org.apache.wss4j.crypto.provider", "org.apache.wss4j.common.crypto.Merlin" + ); + properties.put("org.apache.wss4j.crypto.merlin.keystore.password", "cspass"); + properties.put("org.apache.wss4j.crypto.merlin.keystore.file", "keys/clientstore.jks"); + + return properties; + } + public class EveCallbackHandler implements CallbackHandler { public void handle(Callback[] callbacks) throws IOException,
