TokenStoreUtilsTest.java

/**
 * 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;
    }
}