This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/sts-caching in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 9776b8e6b980bfd53519b31157c6b416cedc2229 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 24 06:06:39 2026 +0100 Adding a more secure way of caching tokens in the STS --- .../ws/security/tokenstore/TokenStoreUtils.java | 150 +++++++++++++++++++ .../cxf/ws/security/trust/STSTokenValidator.java | 31 ++-- .../security/tokenstore/TokenStoreUtilsTest.java | 162 +++++++++++++++++++++ .../java/org/apache/cxf/sts/cache/CacheUtils.java | 22 ++- .../cxf/sts/token/provider/SAMLTokenProvider.java | 2 +- .../cxf/sts/token/renewer/SAMLTokenRenewer.java | 62 +++++++- .../sts/token/validator/SAMLTokenValidator.java | 15 +- .../token/validator/UsernameTokenValidator.java | 12 +- .../CustomUsernameTokenProvider.java | 5 +- 9 files changed, 409 insertions(+), 52 deletions(-) diff --git a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtils.java b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtils.java index 438e5e79e16..69ecf62e998 100644 --- a/rt/ws/security/src/main/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtils.java +++ b/rt/ws/security/src/main/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtils.java @@ -18,9 +18,28 @@ */ package org.apache.cxf.ws.security.tokenstore; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +import org.w3c.dom.Element; + +import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.message.Message; import org.apache.cxf.service.model.EndpointInfo; import org.apache.cxf.ws.security.SecurityConstants; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; +import org.apache.wss4j.common.token.BinarySecurity; +import org.apache.wss4j.dom.WSConstants; +import org.apache.wss4j.dom.message.token.SecurityContextToken; +import org.apache.wss4j.dom.message.token.UsernameToken; +import org.apache.xml.security.Init; +import org.apache.xml.security.c14n.Canonicalizer; /** * Some common functionality @@ -61,4 +80,135 @@ public final class TokenStoreUtils { return tokenStore; } } + + /** + * Get a cache key for a signed SAML Assertion, that can be used to store and retrieve the (validated) + * Assertion in/from a TokenStore. The key is a SHA-256 digest over the canonicalized SignedInfo + * (which binds the signed content of the Assertion) and the SignatureValue. It returns null if the + * Assertion is not signed, or if the signature does not conform to the SAML signature profile (in which + * case the signature might not cover the Assertion itself, and so the Assertion must not be cached). + * + * This method must be used to look up a received Assertion in a TokenStore. + */ + public static String getCacheKey(SamlAssertionWrapper assertion) throws WSSecurityException { + return getCacheKey(assertion, true); + } + + /** + * Get a cache key for a signed SAML Assertion - see getCacheKey(SamlAssertionWrapper). + * @param validateSignatureProfile whether to check that the signature conforms to the SAML signature + * profile first, returning null if it doesn't. This must be true when looking up a received + * Assertion. It can be false when storing an Assertion that was just signed by the STS, as + * its DOM Element might not be attached to a Document, which the profile validation requires. + */ + public static String getCacheKey(SamlAssertionWrapper assertion, boolean validateSignatureProfile) + throws WSSecurityException { + byte[] signatureValue = assertion.getSignatureValue(); + if (signatureValue == null || signatureValue.length == 0) { + return null; + } + + if (validateSignatureProfile) { + try { + assertion.validateSignatureAgainstProfile(); + } catch (WSSecurityException ex) { + return null; + } + } + + Element signedInfo = null; + Element assertionElement = assertion.getElement(); + if (assertionElement == null && assertion.getSamlObject() != null) { + assertionElement = assertion.getSamlObject().getDOM(); + } + if (assertionElement != null) { + Element signature = + DOMUtils.getFirstChildWithName(assertionElement, WSConstants.SIG_NS, WSConstants.SIG_LN); + if (signature != null) { + signedInfo = DOMUtils.getFirstChildWithName(signature, WSConstants.SIG_NS, "SignedInfo"); + } + } + if (signedInfo == null) { + return null; + } + + try { + if (!Init.isInitialized()) { + Init.init(); + } + ByteArrayOutputStream signedInfoBytes = new ByteArrayOutputStream(); + Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS) + .canonicalizeSubtree(signedInfo, signedInfoBytes); + + return computeCacheKey("SAML", signedInfoBytes.toByteArray(), signatureValue); + } catch (Exception ex) { + throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, ex); + } + } + + /** + * Get a cache key for a UsernameToken, that can be used to store and retrieve the (validated) + * UsernameToken in/from a TokenStore. The key is a SHA-256 digest over the UsernameToken values. + */ + public static String getCacheKey(UsernameToken usernameToken) throws WSSecurityException { + byte[] salt = usernameToken.getSalt(); + return computeCacheKey("UsernameToken", + toBytes(usernameToken.getName()), + toBytes(usernameToken.getPassword()), + toBytes(usernameToken.getPasswordType()), + toBytes(usernameToken.getNonce()), + toBytes(usernameToken.getCreated()), + salt, + toBytes(Integer.toString(usernameToken.getIteration()))); + } + + /** + * Get a cache key for a BinarySecurityToken, that can be used to store and retrieve the (validated) + * BinarySecurityToken in/from a TokenStore. The key is a SHA-256 digest over the token values. + */ + public static String getCacheKey(BinarySecurity binarySecurity) { + return computeCacheKey("BinarySecurityToken", + toBytes(binarySecurity.getValueType()), + toBytes(binarySecurity.getEncodingType()), + binarySecurity.getToken()); + } + + /** + * Get a cache key for a SecurityContextToken, that can be used to store and retrieve the (validated) + * SecurityContextToken in/from a TokenStore. The key is a SHA-256 digest over the token identifier. + */ + public static String getCacheKey(SecurityContextToken securityContextToken) { + String identifier = securityContextToken.getIdentifier(); + if (identifier == null) { + return null; + } + return computeCacheKey("SecurityContextToken", toBytes(identifier)); + } + + private static byte[] toBytes(String value) { + return value == null ? null : value.getBytes(StandardCharsets.UTF_8); + } + + private static String computeCacheKey(String tokenType, byte[]... values) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(bytes); + out.writeUTF(tokenType); + for (byte[] value : values) { + // Length-prefix each value so that different combinations can't produce the same input + if (value == null) { + out.writeInt(-1); + } else { + out.writeInt(value.length); + out.write(value); + } + } + out.flush(); + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return HexFormat.of().formatHex(digest.digest(bytes.toByteArray())); + } catch (IOException | NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); + } + } } 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 df76fb086ff..9b9bf5f91a9 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 @@ -20,7 +20,6 @@ package org.apache.cxf.ws.security.trust; import java.io.IOException; -import java.util.Arrays; import java.util.List; import javax.security.auth.callback.Callback; @@ -86,23 +85,20 @@ public class STSTokenValidator implements Validator { try { SecurityToken token = new SecurityToken(); Element tokenElement = null; - int hash = 0; + String cacheKey = null; if (credential.getSamlAssertion() != null) { SamlAssertionWrapper assertion = credential.getSamlAssertion(); - byte[] signatureValue = assertion.getSignatureValue(); - if (signatureValue != null && signatureValue.length > 0) { - hash = Arrays.hashCode(signatureValue); - } - tokenElement = credential.getSamlAssertion().getElement(); + cacheKey = TokenStoreUtils.getCacheKey(assertion); + tokenElement = assertion.getElement(); } else if (credential.getUsernametoken() != null) { tokenElement = credential.getUsernametoken().getElement(); - hash = credential.getUsernametoken().hashCode(); + cacheKey = TokenStoreUtils.getCacheKey(credential.getUsernametoken()); } else if (credential.getBinarySecurityToken() != null) { tokenElement = credential.getBinarySecurityToken().getElement(); - hash = credential.getBinarySecurityToken().hashCode(); + cacheKey = TokenStoreUtils.getCacheKey(credential.getBinarySecurityToken()); } else if (credential.getSecurityContextToken() != null) { tokenElement = credential.getSecurityContextToken().getElement(); - hash = credential.getSecurityContextToken().hashCode(); + cacheKey = TokenStoreUtils.getCacheKey(credential.getSecurityContextToken()); } token.setToken(tokenElement); @@ -112,8 +108,8 @@ public class STSTokenValidator implements Validator { if (ts == null) { ts = tokenStore; } - if (ts != null && hash != 0) { - SecurityToken transformedToken = getTransformedToken(ts, hash); + if (ts != null && cacheKey != null) { + SecurityToken transformedToken = getTransformedToken(ts, cacheKey); if (transformedToken != null && !transformedToken.isExpired()) { SamlAssertionWrapper assertion = new SamlAssertionWrapper(transformedToken.getToken()); credential.setPrincipal(new SAMLTokenPrincipalImpl(assertion)); @@ -122,7 +118,6 @@ public class STSTokenValidator implements Validator { } } } - token.setTokenHash(hash); STSClient c = stsClient; if (c == null) { @@ -156,10 +151,10 @@ public class STSTokenValidator implements Validator { SamlAssertionWrapper assertion = new SamlAssertionWrapper(returnedToken.getToken()); credential.setTransformedToken(assertion); credential.setPrincipal(new SAMLTokenPrincipalImpl(assertion)); - if (!disableCaching && hash != 0 && ts != null) { + if (!disableCaching && cacheKey != null && ts != null) { ts.add(returnedToken); token.setTransformedTokenIdentifier(returnedToken.getId()); - ts.add(Integer.toString(hash), token); + ts.add(cacheKey, token); } } return credential; @@ -195,9 +190,9 @@ public class STSTokenValidator implements Validator { return false; } - private SecurityToken getTransformedToken(TokenStore ts, int hash) { - SecurityToken recoveredToken = ts.getToken(Integer.toString(hash)); - if (recoveredToken != null && recoveredToken.getTokenHash() == hash) { + private SecurityToken getTransformedToken(TokenStore ts, String cacheKey) { + SecurityToken recoveredToken = ts.getToken(cacheKey); + if (recoveredToken != null) { String transformedTokenId = recoveredToken.getTransformedTokenIdentifier(); if (transformedTokenId != null) { return ts.getToken(transformedTokenId); diff --git a/rt/ws/security/src/test/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtilsTest.java b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtilsTest.java new file mode 100644 index 00000000000..99ad53f8727 --- /dev/null +++ b/rt/ws/security/src/test/java/org/apache/cxf/ws/security/tokenstore/TokenStoreUtilsTest.java @@ -0,0 +1,162 @@ +/** + * 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.tokenstore; + +import java.io.StringReader; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.apache.cxf.helpers.DOMUtils; +import org.apache.cxf.staxutils.StaxUtils; +import org.apache.wss4j.common.WSS4JConstants; +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.SamlAssertionWrapper; +import org.apache.wss4j.common.saml.bean.SubjectBean; +import org.apache.wss4j.common.saml.bean.Version; +import org.apache.wss4j.common.saml.builder.SAML2Constants; +import org.apache.wss4j.dom.message.token.UsernameToken; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TokenStoreUtilsTest { + + @org.junit.Test + public void testUnsignedAssertionHasNoCacheKey() throws Exception { + SamlAssertionWrapper assertion = createAssertion("alice", false); + assertNull(TokenStoreUtils.getCacheKey(assertion)); + } + + @org.junit.Test + public void testSignedAssertionCacheKey() throws Exception { + SamlAssertionWrapper assertion = createAssertion("alice", true); + String cacheKey = TokenStoreUtils.getCacheKey(assertion); + assertNotNull(cacheKey); + // SHA-256 in hex + assertTrue(cacheKey.matches("[0-9a-f]{64}")); + + // The key must be the same for the Assertion once it has been serialized + re-parsed + String serialized = StaxUtils.toString(assertion.getElement()); + Document doc = StaxUtils.read(new StringReader(serialized)); + SamlAssertionWrapper parsedAssertion = new SamlAssertionWrapper(doc.getDocumentElement()); + assertEquals(cacheKey, TokenStoreUtils.getCacheKey(parsedAssertion)); + } + + @org.junit.Test + public void testIssuedAssertionCacheKey() throws Exception { + // An Assertion that was just signed is not attached to a Document, and so the (non-validating) key + // is used to store it. It must match the key used to look up the Assertion when it is received. + SamlAssertionWrapper assertion = createAssertion("alice", true, false); + String issuedKey = TokenStoreUtils.getCacheKey(assertion, false); + assertNotNull(issuedKey); + + String serialized = StaxUtils.toString(assertion.getElement()); + Document doc = StaxUtils.read(new StringReader(serialized)); + SamlAssertionWrapper receivedAssertion = new SamlAssertionWrapper(doc.getDocumentElement()); + assertEquals(issuedKey, TokenStoreUtils.getCacheKey(receivedAssertion)); + } + + @org.junit.Test + public void testDifferentAssertionsHaveDifferentCacheKeys() throws Exception { + String aliceKey = TokenStoreUtils.getCacheKey(createAssertion("alice", true)); + String bobKey = TokenStoreUtils.getCacheKey(createAssertion("bob", true)); + assertNotEquals(aliceKey, bobKey); + } + + @org.junit.Test + public void testModifiedSignedInfoChangesCacheKey() throws Exception { + SamlAssertionWrapper assertion = createAssertion("alice", true); + String cacheKey = TokenStoreUtils.getCacheKey(assertion); + + // Change the DigestValue of the SignedInfo, keeping the same SignatureValue + Element digestValue = (Element)assertion.getElement() + .getElementsByTagNameNS(WSS4JConstants.SIG_NS, "DigestValue").item(0); + digestValue.setTextContent("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + SamlAssertionWrapper modifiedAssertion = new SamlAssertionWrapper(assertion.getElement()); + + assertNotEquals(cacheKey, TokenStoreUtils.getCacheKey(modifiedAssertion)); + } + + @org.junit.Test + public void testSignatureNotConformingToProfileHasNoCacheKey() throws Exception { + SamlAssertionWrapper assertion = createAssertion("alice", true); + assertNotNull(TokenStoreUtils.getCacheKey(assertion)); + + // The signature Reference no longer points to the Assertion itself + Element reference = (Element)assertion.getElement() + .getElementsByTagNameNS(WSS4JConstants.SIG_NS, "Reference").item(0); + reference.setAttributeNS(null, "URI", "#some-other-id"); + SamlAssertionWrapper modifiedAssertion = new SamlAssertionWrapper(assertion.getElement()); + + assertNull(TokenStoreUtils.getCacheKey(modifiedAssertion)); + } + + @org.junit.Test + public void testUsernameTokenCacheKey() throws Exception { + String key1 = TokenStoreUtils.getCacheKey(createUsernameToken("alice", "password")); + String key2 = TokenStoreUtils.getCacheKey(createUsernameToken("alice", "password")); + assertEquals(key1, key2); + assertTrue(key1.matches("[0-9a-f]{64}")); + + assertNotEquals(key1, TokenStoreUtils.getCacheKey(createUsernameToken("bob", "password"))); + assertNotEquals(key1, TokenStoreUtils.getCacheKey(createUsernameToken("alice", "password2"))); + // Values must not be able to "shift" between fields + assertNotEquals(TokenStoreUtils.getCacheKey(createUsernameToken("ab", "c")), + TokenStoreUtils.getCacheKey(createUsernameToken("a", "bc"))); + } + + private static UsernameToken createUsernameToken(String name, String password) { + Document doc = DOMUtils.createDocument(); + UsernameToken usernameToken = new UsernameToken(true, doc, WSS4JConstants.PASSWORD_TEXT); + usernameToken.setName(name); + usernameToken.setPassword(password); + return usernameToken; + } + + private static SamlAssertionWrapper createAssertion(String subjectName, boolean signed) throws Exception { + return createAssertion(subjectName, signed, true); + } + + private static SamlAssertionWrapper createAssertion(String subjectName, boolean signed, boolean attach) + throws Exception { + SAMLCallback callback = new SAMLCallback(); + callback.setSamlVersion(Version.SAML_20); + callback.setIssuer("sts"); + callback.setSubject(new SubjectBean(subjectName, null, SAML2Constants.CONF_BEARER)); + + SamlAssertionWrapper assertion = new SamlAssertionWrapper(callback); + if (signed) { + Crypto crypto = CryptoFactory.getInstance("alice.properties"); + assertion.signAssertion("alice", "password", crypto, false); + } + Document doc = DOMUtils.createDocument(); + Element element = assertion.toDOM(doc); + if (attach) { + // As for a received token, the signature profile validation requires the Element to be attached + doc.appendChild(element); + } + return assertion; + } +} diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/cache/CacheUtils.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/cache/CacheUtils.java index 8f566cab8e7..9553ea84cfa 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/cache/CacheUtils.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/cache/CacheUtils.java @@ -21,7 +21,6 @@ package org.apache.cxf.sts.cache; import java.security.Principal; import java.time.Instant; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -31,6 +30,9 @@ import org.apache.cxf.sts.STSConstants; import org.apache.cxf.sts.request.Renewing; import org.apache.cxf.ws.security.tokenstore.SecurityToken; import org.apache.cxf.ws.security.tokenstore.TokenStore; +import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.common.saml.SamlAssertionWrapper; public final class CacheUtils { @@ -74,14 +76,20 @@ public final class CacheUtils { return securityToken; } + /** + * Store the given SecurityToken in the cache, using a (SHA-256 based) key derived from the signature + * of the given (signed) SAML Assertion - see TokenStoreUtils.getCacheKey(SamlAssertionWrapper). + * Nothing is stored if the Assertion is not signed. The signature profile is not checked here, as the + * Assertion might have just been signed by the STS - it is checked when a received token is looked up. + */ public static void storeTokenInCache( SecurityToken securityToken, TokenStore cache, - byte[] signatureValue - ) { - int hash = Arrays.hashCode(signatureValue); - securityToken.setTokenHash(hash); - String identifier = Integer.toString(hash); - cache.add(identifier, securityToken); + SamlAssertionWrapper assertion + ) throws WSSecurityException { + String identifier = TokenStoreUtils.getCacheKey(assertion, false); + if (identifier != null) { + cache.add(identifier, securityToken); + } } } diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java index 31b9f676767..f5962fd1061 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/provider/SAMLTokenProvider.java @@ -131,7 +131,7 @@ public class SAMLTokenProvider extends AbstractSAMLTokenProvider implements Toke assertion.getNotOnOrAfter(), tokenParameters.getPrincipal(), tokenParameters.getRealm(), tokenParameters.getTokenRequirements().getRenewing()); CacheUtils.storeTokenInCache( - securityToken, tokenParameters.getTokenStore(), signatureValue); + securityToken, tokenParameters.getTokenStore(), assertion); } TokenProviderResponse response = new TokenProviderResponse(); diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/renewer/SAMLTokenRenewer.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/renewer/SAMLTokenRenewer.java index d7d0d3cdfa5..cfa4e9f7a27 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/renewer/SAMLTokenRenewer.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/renewer/SAMLTokenRenewer.java @@ -23,7 +23,6 @@ import java.security.Principal; import java.security.cert.Certificate; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -33,6 +32,7 @@ import java.util.logging.Logger; import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.apache.cxf.common.logging.LogUtils; import org.apache.cxf.helpers.CastUtils; @@ -51,10 +51,12 @@ import org.apache.cxf.sts.token.realm.RealmProperties; import org.apache.cxf.ws.security.sts.provider.STSException; import org.apache.cxf.ws.security.tokenstore.SecurityToken; import org.apache.cxf.ws.security.tokenstore.TokenStore; +import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; import org.apache.wss4j.common.WSS4JConstants; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.saml.SAMLKeyInfo; +import org.apache.wss4j.common.saml.SAMLUtil; import org.apache.wss4j.common.saml.SamlAssertionWrapper; import org.apache.wss4j.common.saml.bean.ConditionsBean; import org.apache.wss4j.common.saml.builder.SAML1ComponentBuilder; @@ -73,6 +75,7 @@ import org.opensaml.saml.common.SAMLVersion; import org.opensaml.saml.saml1.core.Audience; import org.opensaml.saml.saml1.core.AudienceRestrictionCondition; import org.opensaml.saml.saml2.core.AudienceRestriction; +import org.opensaml.xmlsec.signature.Signature; /** * A TokenRenewer implementation that renews a (valid or expired) SAML Token. @@ -176,11 +179,15 @@ public class SAMLTokenRenewer extends AbstractSAMLTokenProvider implements Token } try { - SamlAssertionWrapper assertion = new SamlAssertionWrapper((Element)tokenToRenew.getToken()); + Element tokenElement = getAttachedElement((Element)tokenToRenew.getToken()); + SamlAssertionWrapper assertion = new SamlAssertionWrapper(tokenElement); - byte[] oldSignature = assertion.getSignatureValue(); - int hash = Arrays.hashCode(oldSignature); - SecurityToken cachedToken = tokenStore.getToken(Integer.toString(hash)); + // Verify the signature, so that the signed content matches the (cached) token that was + // previously issued or validated. The Assertion is re-signed below with the STS key. + verifySignature(assertion, tokenElement, tokenParameters); + + String cacheKey = TokenStoreUtils.getCacheKey(assertion); + SecurityToken cachedToken = cacheKey != null ? tokenStore.getToken(cacheKey) : null; if (cachedToken == null) { LOG.log(Level.FINE, "The token to be renewed must be stored in the cache"); throw new STSException("Can't renew SAML assertion", STSException.REQUEST_FAILED); @@ -193,7 +200,7 @@ public class SAMLTokenRenewer extends AbstractSAMLTokenProvider implements Token String oldId = createNewId(renewedAssertion); // Remove the previous token (now expired) from the cache tokenStore.remove(oldId); - tokenStore.remove(Integer.toString(hash)); + tokenStore.remove(cacheKey); // Create new Conditions & sign the Assertion createNewConditions(renewedAssertion, tokenParameters); @@ -281,6 +288,47 @@ public class SAMLTokenRenewer extends AbstractSAMLTokenProvider implements Token return Collections.unmodifiableMap(realmMap); } + /** + * The signature (profile) validation requires the Element to be attached to a Document. This is the case + * for a received token, but not e.g. for a token that was just created by the SAMLTokenProvider. + */ + private static Element getAttachedElement(Element element) { + Node root = element; + while (root.getParentNode() != null) { + root = root.getParentNode(); + } + if (root.getNodeType() == Node.DOCUMENT_NODE) { + return element; + } + + Document doc = DOMUtils.createDocument(); + return (Element)doc.appendChild(doc.importNode(element, true)); + } + + private void verifySignature( + SamlAssertionWrapper assertion, + Element tokenElement, + TokenRenewerParameters tokenParameters + ) throws WSSecurityException { + Signature sig = assertion.getSignature(); + if (sig == null || sig.getKeyInfo() == null) { + LOG.log(Level.WARNING, "The token to be renewed must be signed"); + throw new STSException("Can't renew SAML assertion", STSException.REQUEST_FAILED); + } + + Crypto sigCrypto = tokenParameters.getStsProperties().getSignatureCrypto(); + RequestData requestData = new RequestData(); + requestData.setSigVerCrypto(sigCrypto); + requestData.setWssConfig(WSSConfig.getNewInstance()); + requestData.setWsDocInfo(new WSDocInfo(tokenElement.getOwnerDocument())); + + SAMLKeyInfo samlKeyInfo = + SAMLUtil.getCredentialFromKeyInfo( + sig.getKeyInfo().getDOM(), new WSSSAMLKeyInfoProcessor(requestData), sigCrypto + ); + assertion.verifySignature(samlKeyInfo); + } + private void validateAssertion( SamlAssertionWrapper assertion, ReceivedToken tokenToRenew, @@ -522,7 +570,7 @@ public class SAMLTokenRenewer extends AbstractSAMLTokenProvider implements Token assertion.getNotOnOrAfter(), tokenParameters.getPrincipal(), tokenParameters.getRealm(), tokenParameters.getTokenRequirements().getRenewing()); CacheUtils.storeTokenInCache( - securityToken, tokenParameters.getTokenStore(), signatureValue); + securityToken, tokenParameters.getTokenStore(), assertion); } } diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java index ab6d46689e0..e6592eedf99 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/SAMLTokenValidator.java @@ -21,7 +21,6 @@ package org.apache.cxf.sts.token.validator; import java.security.Principal; import java.security.cert.X509Certificate; import java.time.Instant; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -43,6 +42,7 @@ import org.apache.cxf.sts.token.realm.CertConstraintsParser; import org.apache.cxf.sts.token.realm.SAMLRealmCodec; import org.apache.cxf.ws.security.tokenstore.SecurityToken; import org.apache.cxf.ws.security.tokenstore.TokenStore; +import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; import org.apache.wss4j.common.WSS4JConstants; import org.apache.wss4j.common.crypto.Crypto; import org.apache.wss4j.common.ext.WSSecurityException; @@ -179,13 +179,10 @@ public class SAMLTokenValidator implements TokenValidator { assertion.verifySignature(samlKeyInfo); SecurityToken secToken = null; - byte[] signatureValue = assertion.getSignatureValue(); - if (tokenParameters.getTokenStore() != null && signatureValue != null - && signatureValue.length > 0) { - int hash = Arrays.hashCode(signatureValue); - secToken = tokenParameters.getTokenStore().getToken(Integer.toString(hash)); - if (secToken != null && secToken.getTokenHash() != hash) { - secToken = null; + if (tokenParameters.getTokenStore() != null) { + String cacheKey = TokenStoreUtils.getCacheKey(assertion); + if (cacheKey != null) { + secToken = tokenParameters.getTokenStore().getToken(cacheKey); } } if (secToken != null && secToken.isExpired()) { @@ -333,7 +330,7 @@ public class SAMLTokenValidator implements TokenValidator { SecurityToken securityToken = CacheUtils.createSecurityTokenForStorage(assertion.getElement(), assertion.getId(), assertion.getNotOnOrAfter(), principal, tokenRealm, null); - CacheUtils.storeTokenInCache(securityToken, tokenStore, signatureValue); + CacheUtils.storeTokenInCache(securityToken, tokenStore, assertion); } } diff --git a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java index 6555d925f81..6ec17796c40 100644 --- a/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java +++ b/services/sts/sts-core/src/main/java/org/apache/cxf/sts/token/validator/UsernameTokenValidator.java @@ -48,6 +48,7 @@ import org.apache.cxf.sts.token.realm.UsernameTokenRealmCodec; import org.apache.cxf.ws.security.sts.provider.model.ObjectFactory; import org.apache.cxf.ws.security.sts.provider.model.secext.UsernameTokenType; import org.apache.cxf.ws.security.tokenstore.SecurityToken; +import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; import org.apache.wss4j.common.WSS4JConstants; import org.apache.wss4j.common.bsp.BSPEnforcer; import org.apache.wss4j.common.crypto.Crypto; @@ -176,11 +177,11 @@ public class UsernameTokenValidator implements TokenValidator { } // See if the UsernameToken is stored in the cache - int hash = ut.hashCode(); + String cacheKey = TokenStoreUtils.getCacheKey(ut); SecurityToken secToken = null; if (tokenParameters.getTokenStore() != null) { - secToken = tokenParameters.getTokenStore().getToken(Integer.toString(hash)); - if (secToken != null && (secToken.getTokenHash() != hash || secToken.isExpired())) { + secToken = tokenParameters.getTokenStore().getToken(cacheKey); + if (secToken != null && secToken.isExpired()) { secToken = null; } } @@ -226,10 +227,7 @@ public class UsernameTokenValidator implements TokenValidator { if (tokenParameters.getTokenStore() != null && secToken == null) { secToken = new SecurityToken(ut.getID()); secToken.setToken(ut.getElement()); - int hashCode = ut.hashCode(); - String identifier = Integer.toString(hashCode); - secToken.setTokenHash(hashCode); - tokenParameters.getTokenStore().add(identifier, secToken); + tokenParameters.getTokenStore().add(cacheKey, secToken); } response.setPrincipal(principal); diff --git a/services/sts/systests/advanced/src/test/java/org/apache/cxf/systest/sts/distributed_caching/CustomUsernameTokenProvider.java b/services/sts/systests/advanced/src/test/java/org/apache/cxf/systest/sts/distributed_caching/CustomUsernameTokenProvider.java index b9a555a5674..4f28be4e6d1 100644 --- a/services/sts/systests/advanced/src/test/java/org/apache/cxf/systest/sts/distributed_caching/CustomUsernameTokenProvider.java +++ b/services/sts/systests/advanced/src/test/java/org/apache/cxf/systest/sts/distributed_caching/CustomUsernameTokenProvider.java @@ -27,6 +27,7 @@ import org.apache.cxf.sts.token.provider.TokenProviderParameters; import org.apache.cxf.sts.token.provider.TokenProviderResponse; import org.apache.cxf.ws.security.sts.provider.STSException; import org.apache.cxf.ws.security.tokenstore.SecurityToken; +import org.apache.cxf.ws.security.tokenstore.TokenStoreUtils; import org.apache.wss4j.common.WSS4JConstants; import org.apache.wss4j.dom.message.token.UsernameToken; @@ -67,9 +68,7 @@ public class CustomUsernameTokenProvider implements TokenProvider { if (tokenParameters.getTokenStore() != null) { SecurityToken securityToken = new SecurityToken(usernameToken.getID()); securityToken.setToken(usernameToken.getElement()); - int hashCode = usernameToken.hashCode(); - String identifier = Integer.toString(hashCode); - securityToken.setTokenHash(hashCode); + String identifier = TokenStoreUtils.getCacheKey(usernameToken); tokenParameters.getTokenStore().add(identifier, securityToken); }
