be.fedict.eid.idp.sp.protocol.saml2.artifact.ArtifactServiceClient.java Source code

Java tutorial

Introduction

Here is the source code for be.fedict.eid.idp.sp.protocol.saml2.artifact.ArtifactServiceClient.java

Source

/*
 * eID Identity Provider Project.
 * Copyright (C) 2010 FedICT.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see
 * http://www.gnu.org/licenses/.
 */

package be.fedict.eid.idp.sp.protocol.saml2.artifact;

import java.net.ProxySelector;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.xml.bind.JAXBElement;
import javax.xml.ws.Binding;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.soap.SOAPHandler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.saml2.core.Artifact;
import org.opensaml.saml2.core.ArtifactResolve;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.StatusCode;

import be.fedict.eid.idp.common.saml2.Saml2Util;
import be.fedict.eid.idp.saml2.ws.ArtifactService;
import be.fedict.eid.idp.saml2.ws.ArtifactServiceFactory;
import be.fedict.eid.idp.saml2.ws.ArtifactServicePortType;
import be.fedict.eid.idp.saml2.ws.LoggingSoapHandler;
import be.fedict.eid.idp.saml2.ws.jaxb.ArtifactResolveType;
import be.fedict.eid.idp.saml2.ws.jaxb.ArtifactResponseType;
import be.fedict.eid.idp.saml2.ws.jaxb.ResponseType;
import be.fedict.eid.idp.sp.protocol.saml2.AuthenticationResponseProcessorException;

import com.sun.xml.ws.developer.JAXWSProperties;

/**
 * Client for the SAML v2.0 HTTP-Artifact Binding Web Service.
 * 
 * @author Wim Vandenhaute
 */
public class ArtifactServiceClient {

    private static final Log LOG = LogFactory.getLog(ArtifactServiceClient.class);

    private final ArtifactServicePortType port;

    private final String location;
    private final String issuerName;

    private final ArtifactServiceClientHandler clientHandler;

    private static ArtifactProxySelector proxySelector;

    static {
        ProxySelector defaultProxySelector = ProxySelector.getDefault();
        ArtifactServiceClient.proxySelector = new ArtifactProxySelector(defaultProxySelector);
        ProxySelector.setDefault(ArtifactServiceClient.proxySelector);
    }

    /**
     * Main Constructor.
     * <p/>
     * The location is the complete WS location of the eID IdP Artifact
     * Resolution Service.
     * <p/>
     * The SSL Hostname is used when sending requests over SSL and JAX-WS's
     * default hostname verification needs to be overrided. Default it will
     * validate the location's hostname against the SSL certificates CN, which
     * can be unwanted behaviour, especially in test environments. Specifying
     * <code>null</code> will accept any hostname.
     * 
     * @param location
     *            location of the eID IdP Artifact Resolution Service.
     * @param sslHostname
     *            optional SSL hostname, can be <code>null</code>.
     * @param spIdentity
     *            optional Service Provider's identity to be used to sign
     *            outgoing SAML2 Artifact Resolve requests.
     * @param issuer
     *            issuer of the ArtifactResolve request
     */
    public ArtifactServiceClient(String location, String sslHostname, KeyStore.PrivateKeyEntry spIdentity,
            String issuer) {

        this.location = location;
        this.issuerName = issuer;

        ArtifactService artifactService = ArtifactServiceFactory.getInstance();
        this.port = artifactService.getArtifactServicePort();

        setEndpointAddress(sslHostname);

        // register client SOAP handler
        this.clientHandler = new ArtifactServiceClientHandler(spIdentity);
        registerSoapHandler(this.clientHandler);
    }

    /**
     * Enables/disables logging of all SOAP requests/responses.
     * 
     * @param logging
     *            logging or not
     */
    public void setLogging(boolean logging) {

        if (logging) {
            registerSoapHandler(new LoggingSoapHandler());
        } else {
            removeSoapHandler(LoggingSoapHandler.class);
        }
    }

    /**
     * Proxy configuration setting ( both http as https ).
     * 
     * @param proxyHost
     *            proxy hostname
     * @param proxyPort
     *            proxy port
     */
    public void setProxy(String proxyHost, int proxyPort) {
        ArtifactServiceClient.proxySelector.setProxy(this.location, proxyHost, proxyPort);
    }

    /**
     * Resolve the specified artifact ID via the eID IdP's SAML v2.0 Artifact
     * Service
     * 
     * @param artifactId
     *            ID off the to be resolved SAML v2.0 artifact.
     * @return SAML v2.0 Response
     * @throws AuthenticationResponseProcessorException
     *             something went wrong
     */
    public Response resolve(String artifactId) throws AuthenticationResponseProcessorException {

        LOG.debug("resolve: " + artifactId);

        String resolveId = UUID.randomUUID().toString();

        ArtifactResolve artifactResolve = Saml2Util.buildXMLObject(ArtifactResolve.class,
                ArtifactResolve.DEFAULT_ELEMENT_NAME);
        artifactResolve.setID(resolveId);
        LOG.debug("request ID=" + resolveId);

        // Issuer
        Issuer issuer = Saml2Util.buildXMLObject(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME);
        issuer.setValue(this.issuerName);
        artifactResolve.setIssuer(issuer);

        Artifact artifact = Saml2Util.buildXMLObject(Artifact.class, Artifact.DEFAULT_ELEMENT_NAME);
        artifact.setArtifact(artifactId);
        artifactResolve.setArtifact(artifact);

        // to JAXB
        ArtifactResolveType artifactResolveType = Saml2Util.toJAXB(artifactResolve, ArtifactResolveType.class);

        // Resolve
        ArtifactResponseType response = this.port.resolve(artifactResolveType);

        // Validate response
        if (null == response) {
            throw new AuthenticationResponseProcessorException("No Artifact Response returned");
        }

        if (null == response.getStatus()) {
            throw new AuthenticationResponseProcessorException("No Status Code in Artifact Response");
        }

        if (!response.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
            throw new AuthenticationResponseProcessorException(
                    "Resolve failed: " + response.getStatus().getStatusCode().getValue());
        }

        if (!response.getInResponseTo().equals(resolveId)) {
            throw new AuthenticationResponseProcessorException("Response not matching resolve?");
        }

        if (null == response.getAny()) {
            throw new AuthenticationResponseProcessorException("No content in Artifact Response?");
        }

        if (!(response.getAny() instanceof JAXBElement)) {
            throw new AuthenticationResponseProcessorException("Unexpected content in Artifact Response.");
        }

        if (!(((JAXBElement) response.getAny()).getValue() instanceof ResponseType)) {
            throw new AuthenticationResponseProcessorException("Unexpected content in Artifact Response.");
        }

        /*
         * We do not get the SAML v2.0 Response from JAXB but from the client
         * SOAP handler as JAXB breaks any XML Signatures...
         */
        if (null == this.clientHandler.getResponse()) {
            throw new AuthenticationResponseProcessorException(
                    "Artifact Service SOAP handler did not return" + "a SAML v2.0 Response.");
        }
        return this.clientHandler.getResponse();
    }

    /**
     * If set, unilateral TLS authentication will occur, verifying the server
     * {@link X509Certificate} specified against the {@link PublicKey}.
     * 
     * @param publicKey
     *            public key to validate server TLS certificate against.
     */
    public void setServicePublicKey(final PublicKey publicKey) {

        // Create TrustManager
        TrustManager[] trustManager = { new X509TrustManager() {

            public X509Certificate[] getAcceptedIssuers() {

                return null;
            }

            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                X509Certificate serverCertificate = chain[0];
                LOG.debug("server X509 subject: " + serverCertificate.getSubjectX500Principal().toString());
                LOG.debug("authentication type: " + authType);
                if (null == publicKey) {
                    return;
                }

                try {
                    serverCertificate.verify(publicKey);
                    LOG.debug("valid server certificate");
                } catch (InvalidKeyException e) {
                    throw new CertificateException("Invalid Key");
                } catch (NoSuchAlgorithmException e) {
                    throw new CertificateException("No such algorithm");
                } catch (NoSuchProviderException e) {
                    throw new CertificateException("No such provider");
                } catch (SignatureException e) {
                    throw new CertificateException("Wrong signature");
                }
            }

            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                throw new CertificateException("this trust manager cannot be used as server-side trust manager");
            }
        } };

        // Create SSL Context
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            SecureRandom secureRandom = new SecureRandom();
            sslContext.init(null, trustManager, secureRandom);
            LOG.debug("SSL context provider: " + sslContext.getProvider().getName());

            // Setup TrustManager for validation
            Map<String, Object> requestContext = ((BindingProvider) this.port).getRequestContext();
            requestContext.put(JAXWSProperties.SSL_SOCKET_FACTORY, sslContext.getSocketFactory());

        } catch (KeyManagementException e) {
            String msg = "key management error: " + e.getMessage();
            LOG.error(msg, e);
            throw new RuntimeException(msg, e);
        } catch (NoSuchAlgorithmException e) {
            String msg = "TLS algo not present: " + e.getMessage();
            LOG.error(msg, e);
            throw new RuntimeException(msg, e);
        }
    }

    private void setEndpointAddress(String sslHostname) {

        LOG.debug("ws location: " + location);
        if (null == location) {
            throw new IllegalArgumentException("SAML Artifact " + "Service location URL cannot be null");
        }

        BindingProvider bindingProvider = (BindingProvider) this.port;
        bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, location);
        bindingProvider.getRequestContext().put(JAXWSProperties.HOSTNAME_VERIFIER,
                new CustomHostnameVerifier(sslHostname));

    }

    /*
     * Registers the specifed SOAP handler on the given JAX-WS port component.
     */
    protected void registerSoapHandler(SOAPHandler soapHandler) {

        BindingProvider bindingProvider = (BindingProvider) this.port;

        Binding binding = bindingProvider.getBinding();
        @SuppressWarnings("unchecked")
        List<Handler> handlerChain = binding.getHandlerChain();
        handlerChain.add(soapHandler);
        binding.setHandlerChain(handlerChain);
    }

    /*
     * Unregister possible SOAP handlers of specified typeon the given JAX-WS
     * port component.
     */
    protected void removeSoapHandler(Class<? extends SOAPHandler> soapHandlerClass) {

        BindingProvider bindingProvider = (BindingProvider) this.port;

        Binding binding = bindingProvider.getBinding();
        @SuppressWarnings("unchecked")
        List<Handler> handlerChain = binding.getHandlerChain();
        Iterator<Handler> iter = handlerChain.iterator();
        while (iter.hasNext()) {
            Handler handler = iter.next();
            if (handler.getClass().isAssignableFrom(soapHandlerClass)) {
                iter.remove();
            }

        }
    }

    /**
     * SSL Hostname verifier, hostname of WS call over SSL is checked against
     * SSL's CN...
     */
    class CustomHostnameVerifier implements HostnameVerifier {

        private final String hostname;

        public CustomHostnameVerifier(String hostname) {
            this.hostname = hostname;
        }

        public boolean verify(String hostname, SSLSession sslSession) {

            LOG.debug("verify: " + hostname);
            return null == this.hostname || this.hostname.equals(hostname);
        }
    }
}