From 022f920d2e1d1cb4f8b549046ed17a0d5e72e09f Mon Sep 17 00:00:00 2001 From: Colm O hEigeartaigh Date: Thu, 24 Sep 2026 16:46:49 +0100 Subject: [PATCH] Make STSTokenValidator thread safe --- .../ws/security/trust/STSTokenValidator.java | 4 +- .../security/trust/STSTokenValidatorTest.java | 167 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 rt/ws/security/src/test/java/org/apache/cxf/ws/security/trust/STSTokenValidatorTest.java diff --git a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/trust/STSTokenValidator.java b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/trust/STSTokenValidator.java index 9b9bf5f91a9..6c62d267150 100644 --- a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/trust/STSTokenValidator.java +++ b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/trust/STSTokenValidator.java @@ -52,7 +52,6 @@ * "useIssueBinding" to "true" only works for validating UsernameTokens. */ public class STSTokenValidator implements Validator { - private STSSamlAssertionValidator samlValidator = new STSSamlAssertionValidator(); private boolean alwaysValidateToSts; private boolean useIssueBinding; private boolean useOnBehalfOf = true; @@ -179,6 +178,9 @@ protected boolean isValidatedLocally(Credential credential, RequestData data) if (!alwaysValidateToSts && credential.getSamlAssertion() != null) { try { + // STSSamlAssertionValidator records the trust verification result in an instance field, so + // a new instance must be used for each request to avoid sharing state between requests + STSSamlAssertionValidator samlValidator = new STSSamlAssertionValidator(); samlValidator.validate(credential, data); return samlValidator.isTrustVerificationSucceeded(); } catch (RuntimeException e) { diff --git a/rt/ws/security/src/test/java/org/apache/cxf/ws/security/trust/STSTokenValidatorTest.java b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/trust/STSTokenValidatorTest.java new file mode 100644 index 00000000000..fdab35bde4d --- /dev/null +++ b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/trust/STSTokenValidatorTest.java @@ -0,0 +1,167 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.cxf.ws.security.trust; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.apache.cxf.helpers.DOMUtils; +import org.apache.cxf.ws.security.wss4j.saml.SAML2CallbackHandler; +import org.apache.wss4j.common.crypto.Crypto; +import org.apache.wss4j.common.crypto.CryptoFactory; +import org.apache.wss4j.common.saml.SAMLCallback; +import org.apache.wss4j.common.saml.SAMLUtil; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.apache.wss4j.dom.engine.WSSConfig; +import org.apache.wss4j.dom.handler.RequestData; +import org.apache.wss4j.dom.saml.WSSSAMLKeyInfoProcessor; +import org.apache.wss4j.dom.validate.Credential; + +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Tests for local validation of SAML Assertions in the STSTokenValidator. + */ +public class STSTokenValidatorTest { + + private static Crypto signingCrypto; + private static Crypto trustedCrypto; + private static Crypto untrustedCrypto; + + @BeforeClass + public static void init() throws Exception { + WSSConfig.init(); + signingCrypto = CryptoFactory.getInstance("outsecurity.properties"); + // Contains the signing certificate + trustedCrypto = CryptoFactory.getInstance("outsecurity.properties"); + // Only trusts an unrelated CA + untrustedCrypto = CryptoFactory.getInstance("cxfca.properties"); + } + + @Test + public void testTrustedSignedAssertion() throws Exception { + STSTokenValidator validator = new STSTokenValidator(); + RequestData data = createRequestData(trustedCrypto); + assertTrue(validator.isValidatedLocally(createCredential(true, data), data)); + } + + @Test + public void testUntrustedSignedAssertion() throws Exception { + STSTokenValidator validator = new STSTokenValidator(); + RequestData data = createRequestData(untrustedCrypto); + assertFalse(validator.isValidatedLocally(createCredential(true, data), data)); + } + + /** + * An unsigned Assertion must not be treated as validated locally because a previous + * signed Assertion was trusted. + */ + @Test + public void testUnsignedAssertionAfterTrustedAssertion() throws Exception { + STSTokenValidator validator = new STSTokenValidator(); + + RequestData trustedData = createRequestData(trustedCrypto); + assertTrue(validator.isValidatedLocally(createCredential(true, trustedData), trustedData)); + + RequestData data = createRequestData(trustedCrypto); + assertFalse(validator.isValidatedLocally(createCredential(false, data), data)); + } + + /** + * An untrusted Assertion must never be treated as validated locally, even when a trusted + * Assertion is validated concurrently by the same STSTokenValidator. + */ + @Test + public void testConcurrentTrustedAndUntrustedAssertions() throws Exception { + final STSTokenValidator validator = new STSTokenValidator(); + final int iterations = 200; + + final RequestData trustedData = createRequestData(trustedCrypto); + final Credential trustedCredential = createCredential(true, trustedData); + final RequestData untrustedData = createRequestData(untrustedCrypto); + final Credential untrustedCredential = createCredential(true, untrustedData); + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Callable trusted = () -> { + boolean result = true; + for (int i = 0; i < iterations; i++) { + result &= validator.isValidatedLocally(trustedCredential, trustedData); + } + return result; + }; + Callable untrusted = () -> { + boolean result = false; + for (int i = 0; i < iterations; i++) { + result |= validator.isValidatedLocally(untrustedCredential, untrustedData); + } + return result; + }; + + Future trustedResult = executor.submit(trusted); + Future untrustedResult = executor.submit(untrusted); + + assertTrue(trustedResult.get(60, TimeUnit.SECONDS)); + assertFalse(untrustedResult.get(60, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + } + } + + private static RequestData createRequestData(Crypto sigVerCrypto) { + RequestData data = new RequestData(); + data.setSigVerCrypto(sigVerCrypto); + data.setWssConfig(WSSConfig.getNewInstance()); + return data; + } + + private static Credential createCredential(boolean signed, RequestData data) throws Exception { + SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler(); + SAMLCallback samlCallback = new SAMLCallback(); + SAMLUtil.doSAMLCallback(callbackHandler, samlCallback); + SamlAssertionWrapper assertion = new SamlAssertionWrapper(samlCallback); + if (signed) { + assertion.signAssertion("myalias", "myAliasPassword", signingCrypto, false); + } + + // Round-trip the Assertion via DOM, as would happen on receipt of a message + Document doc = DOMUtils.createDocument(); + Element element = assertion.toDOM(doc); + // The Assertion must be attached to the Document so that the signature Reference can be resolved + doc.appendChild(element); + SamlAssertionWrapper receivedAssertion = new SamlAssertionWrapper(element); + if (signed) { + receivedAssertion.verifySignature(new WSSSAMLKeyInfoProcessor(data), data.getSigVerCrypto()); + } + + Credential credential = new Credential(); + credential.setSamlAssertion(receivedAssertion); + return credential; + } +}