This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 952b990a897 ignore x5c and x5t key headers in JWTTokenValidator (#3636)
952b990a897 is described below
commit 952b990a897fdf4f60157bb43cd6efd2ee21a831
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
---
.../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,