net.link.util.ws.security.x509.WSSecurityX509TokenHandler.java Source code

Java tutorial

Introduction

Here is the source code for net.link.util.ws.security.x509.WSSecurityX509TokenHandler.java

Source

/*
 * SafeOnline project.
 *
 * Copyright 2006-2007 Lin.k N.V. All rights reserved.
 * Lin.k N.V. proprietary/confidential. Use is subject to license terms.
 */

package net.link.util.ws.security.x509;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import net.link.util.logging.Logger;
import java.security.cert.X509Certificate;
import java.util.*;
import javax.annotation.PostConstruct;
import javax.naming.*;
import javax.xml.crypto.dsig.Reference;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPPart;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.MessageContext.Scope;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import net.link.util.common.CertificateChain;
import net.link.util.common.DomUtils;
import net.link.util.j2ee.JNDIUtils;
import net.link.util.pkix.ClientCrypto;
import net.link.util.pkix.ServerCrypto;
import net.link.util.ws.security.SOAPUtils;
import org.apache.ws.security.*;
import org.apache.ws.security.message.*;
import org.apache.ws.security.message.token.Timestamp;
import org.apache.ws.security.util.WSSecurityUtil;
import org.joda.time.Duration;

/**
 * JAX-WS SOAP Handler that provides WS-Security server-side verification.
 *
 * @author fcorneli
 */
public class WSSecurityX509TokenHandler implements SOAPHandler<SOAPMessageContext> {

    static final Logger logger = Logger.get(WSSecurityX509TokenHandler.class);

    public static final String CERTIFICATE_CHAIN_PROPERTY = WSSecurityX509TokenHandler.class + ".x509";
    public static final String TO_BE_SIGNED_IDS_SET = WSSecurityX509TokenHandler.class + ".toBeSignedIDs";
    public static final String SIGNED_ELEMENTS_CONTEXT_KEY = WSSecurityX509TokenHandler.class + ".signed.elements";

    private WSSecurityConfiguration configuration;

    public WSSecurityX509TokenHandler() {
        // do nothing, needed tho
    }

    public WSSecurityX509TokenHandler(final WSSecurityConfiguration configuration) {

        this.configuration = configuration;
    }

    private WSSecurityConfiguration getWSSecConfiguration() {

        if (null == this.configuration) {

            try {
                Context ctx = new InitialContext();
                try {
                    Context env = (Context) ctx.lookup("java:comp/env");
                    String configurationServiceJndiName = (String) env
                            .lookup("wsSecurityConfigurationServiceJndiName");
                    this.configuration = JNDIUtils.getComponent(configurationServiceJndiName,
                            WSSecurityConfiguration.class);
                } finally {
                    try {
                        ctx.close();
                    } catch (NamingException e) {
                        logger.err(e, "While closing: %s", ctx);
                    }
                }
            } catch (NamingException e) {
                throw new RuntimeException("'wsSecurityConfigurationServiceJndiName' not specified", e);
            }
        }

        return this.configuration;
    }

    @PostConstruct
    public void postConstructCallback() {

        System.setProperty("com.sun.xml.ws.fault.SOAPFaultBuilder.disableCaptureStackTrace",
                Boolean.toString(true));
    }

    @Override
    public Set<QName> getHeaders() {

        return ImmutableSet.of(new QName(
                "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", "Security"));
    }

    @Override
    public void close(@SuppressWarnings("unused") MessageContext messageContext) {

    }

    @Override
    public boolean handleFault(@SuppressWarnings("unused") SOAPMessageContext soapMessageContext) {

        return true;
    }

    @Override
    public boolean handleMessage(SOAPMessageContext soapMessageContext) {

        SOAPPart soapPart = soapMessageContext.getMessage().getSOAPPart();
        boolean isOutbound = (Boolean) soapMessageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);

        if (isOutbound)
            return handleOutboundDocument(soapPart, soapMessageContext);
        else
            return handleInboundDocument(soapPart, soapMessageContext);
    }

    /**
     * Handles the outbound SOAP message. Adds the WS Security Header containing a signed timestamp, and signed SOAP body.
     */
    private boolean handleOutboundDocument(SOAPPart document, SOAPMessageContext soapMessageContext) {

        if (!getWSSecConfiguration().isOutboundSignatureNeeded()) {
            logger.dbg("Out: Not adding WS-Security SOAP header");
            return true;
        }

        logger.dbg("Out: Adding WS-Security SOAP header");
        try {
            CertificateChain certificateChain = getWSSecConfiguration().getIdentityCertificateChain();

            WSSecHeader wsSecHeader = new WSSecHeader();
            wsSecHeader.insertSecurityHeader(document);

            WSSecSignature wsSecSignature = new WSSecSignature();
            wsSecSignature.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
            wsSecSignature.setUseSingleCertificate(!certificateChain.hasRootCertificate());
            wsSecSignature.prepare(document,
                    new ClientCrypto(certificateChain, getWSSecConfiguration().getPrivateKey()), wsSecHeader);

            List<WSEncryptionPart> wsEncryptionParts = Lists.newLinkedList();

            SOAPConstants soapConstants = WSSecurityUtil.getSOAPConstants(document.getDocumentElement());
            wsEncryptionParts.add(new WSEncryptionPart(soapConstants.getBodyQName().getLocalPart(),
                    soapConstants.getEnvelopeURI(), "Content"));

            WSSecTimestamp wsSecTimeStamp = new WSSecTimestamp();
            wsSecTimeStamp.setTimeToLive(0);
            wsSecTimeStamp.prepare(document);
            wsSecTimeStamp.prependToHeader(wsSecHeader);
            wsEncryptionParts.add(new WSEncryptionPart(wsSecTimeStamp.getId()));

            @SuppressWarnings("unchecked")
            Set<String> toBeSignedIDs = (Set<String>) soapMessageContext.get(TO_BE_SIGNED_IDS_SET);
            if (null != toBeSignedIDs)
                for (String toBeSignedID : toBeSignedIDs)
                    wsEncryptionParts.add(new WSEncryptionPart(toBeSignedID));

            List<Reference> references = wsSecSignature.addReferencesToSign(wsEncryptionParts, wsSecHeader);
            wsSecSignature.prependBSTElementToHeader(wsSecHeader);
            wsSecSignature.computeSignature(references);
        } catch (WSSecurityException e) {
            logger.err(e, "While handling outbound WS request");
            return false;
        }

        logger.dbg("document: %s", DomUtils.domToString(document));
        return true;
    }

    private boolean handleInboundDocument(SOAPPart document, SOAPMessageContext soapMessageContext) {

        logger.dbg("In: WS-Security header validation");
        List<WSSecurityEngineResult> wsSecurityEngineResults;
        try {
            //noinspection unchecked
            wsSecurityEngineResults = new WSSecurityEngine().processSecurityHeader(document, null, null,
                    new ServerCrypto());
        } catch (WSSecurityException e) {
            throw SOAPUtils.createSOAPFaultException("The signature or decryption was invalid", "FailedCheck", e);
        }
        logger.dbg("results: %s", wsSecurityEngineResults);
        if (null == wsSecurityEngineResults) {
            if (!getWSSecConfiguration().isInboundSignatureOptional())
                throw SOAPUtils.createSOAPFaultException("No WS-Security header was found but is required.",
                        "InvalidSecurity");

            logger.dbg("Allowing inbound message without signature: it's set to optional");
            return true;
        }

        Timestamp timestamp = null;
        List<WSDataRef> signedElements = null;
        for (WSSecurityEngineResult result : wsSecurityEngineResults) {

            @SuppressWarnings("unchecked")
            List<WSDataRef> resultSignedElements = (List<WSDataRef>) result
                    .get(WSSecurityEngineResult.TAG_DATA_REF_URIS);
            //            Set<String> resultSignedElements = (Set<String>) result.get( WSSecurityEngineResult.TAG_SIGNED_ELEMENT_IDS );
            if (null != resultSignedElements)
                signedElements = resultSignedElements;

            X509Certificate[] certificateChain = (X509Certificate[]) result
                    .get(WSSecurityEngineResult.TAG_X509_CERTIFICATES);
            X509Certificate certificate = (X509Certificate) result.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE);
            if (null != certificateChain)
                setCertificateChain(soapMessageContext, certificateChain);
            else if (null != certificate)
                setCertificateChain(soapMessageContext, certificate);

            Timestamp resultTimestamp = (Timestamp) result.get(WSSecurityEngineResult.TAG_TIMESTAMP);
            if (null != resultTimestamp)
                timestamp = resultTimestamp;
        }

        if (null == signedElements)
            throw SOAPUtils.createSOAPFaultException("No signed elements found.", "FailedCheck");
        logger.dbg("# signed elements: %d", signedElements.size());
        soapMessageContext.put(SIGNED_ELEMENTS_CONTEXT_KEY, signedElements);

        // Check whether the SOAP Body has been signed.
        try {
            String bodyId = document.getEnvelope().getBody().getAttributeNS(WSConstants.WSU_NS, "Id");

            if (null == bodyId || bodyId.isEmpty())
                throw SOAPUtils.createSOAPFaultException("SOAP Body should have a wsu:Id attribute", "FailedCheck");
            if (!isElementSigned(soapMessageContext, bodyId))
                throw SOAPUtils.createSOAPFaultException("SOAP Body was not signed", "FailedCheck");
        } catch (SOAPException e) {
            throw SOAPUtils.createSOAPFaultException("error retrieving SOAP Body", "FailedCheck", e);
        }

        /*
        * Validate certificate.
        */
        CertificateChain certificateChain = findCertificateChain(soapMessageContext);
        if (null == certificateChain)
            throw SOAPUtils.createSOAPFaultException("missing X509Certificate chain in WS-Security header",
                    "InvalidSecurity");
        if (!getWSSecConfiguration().isCertificateChainTrusted(certificateChain))
            throw SOAPUtils.createSOAPFaultException("can't trust X509Certificate chain in WS-Security header",
                    "InvalidSecurity");

        /*
         * Check timestamp.
         */
        if (null == timestamp)
            throw SOAPUtils.createSOAPFaultException("missing Timestamp in WS-Security header", "InvalidSecurity");
        String timestampId = timestamp.getID();
        if (!isElementSigned(soapMessageContext, timestampId))
            throw SOAPUtils.createSOAPFaultException("Timestamp not signed", "FailedCheck");
        Duration age = new Duration(timestamp.getCreated().getTime(), System.currentTimeMillis());
        Duration maximumAge = getWSSecConfiguration().getMaximumAge();
        if (age.isLongerThan(maximumAge)) {
            logger.dbg("Maximum age exceeded by %s (since %s)", maximumAge.minus(age),
                    timestamp.getCreated().getTime());
            throw SOAPUtils.createSOAPFaultException("Message too old", "FailedCheck");
        }

        return true;
    }

    private static void setCertificateChain(SOAPMessageContext context, X509Certificate... certificate) {

        context.put(CERTIFICATE_CHAIN_PROPERTY, new CertificateChain(certificate));
        context.setScope(CERTIFICATE_CHAIN_PROPERTY, Scope.APPLICATION);
    }

    /**
     * @return the X509 certificate chain that was set previously by a WS-Security handler.
     */
    @SuppressWarnings("unchecked")
    public static CertificateChain findCertificateChain(MessageContext context) {

        return (CertificateChain) context.get(CERTIFICATE_CHAIN_PROPERTY);
    }

    /**
     * Adds a new WS-Security client handler to the handler chain of the given JAX-WS port.
     */
    public static void install(BindingProvider port, WSSecurityConfiguration configuration) {

        @SuppressWarnings("unchecked")
        List<Handler> handlerChain = port.getBinding().getHandlerChain();
        handlerChain.add(new WSSecurityX509TokenHandler(configuration));
        port.getBinding().setHandlerChain(handlerChain);
    }

    /**
     * Add an XML Id that needs to be included in the WS-Security signature digest.
     */
    public static void addSignedElement(SOAPMessageContext context, String id) {

        @SuppressWarnings("unchecked")
        Set<String> toBeSignedIds = (Set<String>) context.get(TO_BE_SIGNED_IDS_SET);
        if (null == toBeSignedIds)
            context.put(TO_BE_SIGNED_IDS_SET, toBeSignedIds = new HashSet<String>());

        toBeSignedIds.add(id);
    }

    /**
     * Checks whether a WS-Security handler did verify that the element with given Id was signed correctly.
     */
    public static boolean isElementSigned(SOAPMessageContext context, String id) {

        logger.dbg("isElementSigned: %s", id);
        @SuppressWarnings("unchecked")
        List<WSDataRef> signedElements = (List<WSDataRef>) context.get(SIGNED_ELEMENTS_CONTEXT_KEY);
        return isElementSigned(signedElements, id);
    }

    public static boolean isElementSigned(final List<WSDataRef> signedElements, final String id) {

        if (null == signedElements)
            return false;

        for (WSDataRef signedElement : signedElements) {

            String wsuId = signedElement.getWsuId();
            if (wsuId.charAt(0) == '#') {
                wsuId = wsuId.substring(1);
            }
            logger.dbg("signed element: %s", wsuId);

            if (wsuId.equals(id))
                return true;
        }

        return false;
    }
}