eu.eidas.auth.engine.metadata.impl.BaseMetadataFetcher.java Source code

Java tutorial

Introduction

Here is the source code for eu.eidas.auth.engine.metadata.impl.BaseMetadataFetcher.java

Source

/*
 * This work is Open Source and licensed by the European Commission under the
 * conditions of the European Public License v1.1
 *
 * (http://www.osor.eu/eupl/european-union-public-licence-eupl-v.1.1);
 *
 * any use of this file implies acceptance of the conditions of this license.
 * 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 eu.eidas.auth.engine.metadata.impl;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.util.Locale;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;

import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
import org.apache.commons.lang.StringUtils;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.ws.soap.client.http.HttpClientBuilder;
import org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.security.x509.tls.StrictHostnameVerifier;
import org.opensaml.xml.signature.SignableXMLObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import eu.eidas.auth.commons.EidasErrorKey;
import eu.eidas.auth.engine.AbstractProtocolEngine;
import eu.eidas.auth.engine.metadata.MetadataFetcherI;
import eu.eidas.auth.engine.metadata.MetadataSignerI;
import eu.eidas.auth.engine.metadata.MetadataUtil;
import eu.eidas.engine.exceptions.EIDASMetadataProviderException;
import eu.eidas.engine.exceptions.EIDASSAMLEngineException;

/**
 * The base implementation of the {@link MetadataFetcherI} interface.
 * <p>
 * This default implementation only fetches the metadata from the URL and validates its digital signature using the
 * {@link MetadataSignerI#validateMetadataSignature(SignableXMLObject)}.
 *
 * @since 1.1
 */
public class BaseMetadataFetcher implements MetadataFetcherI {

    private static final Logger LOG = LoggerFactory.getLogger(BaseMetadataFetcher.class);

    private static final Pattern HTTP_OR_HTTPS_URL = Pattern.compile("^https?://.*$");

    protected EntityDescriptor fetchEntityDescriptor(@Nonnull String url) throws EIDASMetadataProviderException {
        if (!isAllowedMetadataUrl(url)) {
            throw new EIDASMetadataProviderException(EidasErrorKey.SAML_ENGINE_INVALID_METADATA_SOURCE.errorCode(),
                    EidasErrorKey.SAML_ENGINE_INVALID_METADATA_SOURCE.errorMessage(),
                    "Metadata URL is not secure: \"" + url + "\"");
        }
        URL metadataUrl = null;
        try {
            metadataUrl = new URL(url);
        } catch (MalformedURLException e) {
            throw new EIDASMetadataProviderException("Invalid URL : " + url);
        }
        HttpClientBuilder httpClientBuilder = new HttpClientBuilder();

        // This registers a socket factory for the https scheme:
        // specifying a null X509KeyManager and a null X509TrustManager is going to use the default ones from the JVM:
        httpClientBuilder.setHttpsProtocolSocketFactory(newSslSocketFactory());

        try {
            HTTPMetadataProvider provider = new DomCachingHttpMetadataProvider(null,
                    httpClientBuilder.buildClient(), url);
            provider.setParserPool(AbstractProtocolEngine.getSecuredParserPool());
            provider.initialize();
            XMLObject metadata = provider.getMetadata();
            EntityDescriptor entityDescriptor;
            if (metadata instanceof EntityDescriptor) {
                entityDescriptor = (EntityDescriptor) metadata;
            } else {
                //CAVEAT: the entity descriptor should have its id equal to the url (issuer url)
                entityDescriptor = provider.getEntityDescriptor(url);
            }
            if (null == entityDescriptor) {
                throw new EIDASMetadataProviderException(EidasErrorKey.SAML_ENGINE_NO_METADATA.errorCode(),
                        EidasErrorKey.SAML_ENGINE_NO_METADATA.errorMessage(),
                        "No entity descriptor for URL \"" + url + "\"");
            }
            if (!entityDescriptor.isValid()) {
                throw new EIDASMetadataProviderException(EidasErrorKey.SAML_ENGINE_INVALID_METADATA.errorCode(),
                        EidasErrorKey.SAML_ENGINE_INVALID_METADATA.errorMessage(),
                        "Invalid entity descriptor for URL \"" + url + "\"");
            }
            return entityDescriptor;
        } catch (MetadataProviderException mpe) {
            LOG.error("Error fetching metadata from URL \"" + url + "\": " + mpe, mpe);
            throw new EIDASMetadataProviderException(EidasErrorKey.SAML_ENGINE_INVALID_METADATA.errorCode(),
                    EidasErrorKey.SAML_ENGINE_INVALID_METADATA.errorMessage(), mpe);
        }
    }

    @Nonnull
    @Override
    public EntityDescriptor getEntityDescriptor(@Nonnull String url, @Nonnull MetadataSignerI metadataSigner)
            throws EIDASSAMLEngineException {
        // 1) fetch
        EntityDescriptor entityDescriptor = fetchEntityDescriptor(url);
        // 2) validate the digital signature
        if (mustValidateSignature(url)) {
            metadataSigner.validateMetadataSignature(entityDescriptor);
        }
        // 3) release the DOM
        entityDescriptor.releaseDOM();
        return entityDescriptor;
    }

    @Nullable
    public IDPSSODescriptor getIDPSSODescriptor(@Nonnull String url, @Nonnull MetadataSignerI metadataSigner)
            throws EIDASSAMLEngineException {
        EntityDescriptor entityDescriptor = getEntityDescriptor(url, metadataSigner);
        return MetadataUtil.getIDPSSODescriptor(entityDescriptor);
    }

    @Nullable
    public SPSSODescriptor getSPSSODescriptor(@Nonnull String url, @Nonnull MetadataSignerI metadataSigner)
            throws EIDASSAMLEngineException {
        EntityDescriptor entityDescriptor = getEntityDescriptor(url, metadataSigner);
        return MetadataUtil.getSPSSODescriptor(entityDescriptor);
    }

    protected boolean isAllowedMetadataUrl(@Nonnull String url) {
        if (StringUtils.isNotBlank(url)) {
            String lowerCaseUrl = url.toLowerCase(Locale.ENGLISH);
            if (mustUseHttps()) {
                return lowerCaseUrl.startsWith("https://");
            } else {
                return HTTP_OR_HTTPS_URL.matcher(lowerCaseUrl).matches();
            }
        }
        return false;
    }

    protected boolean mustUseHttps() {
        return true;
    }

    protected boolean mustValidateSignature(@Nonnull String url) {
        return true;
    }

    /**
     * Override this method to plug your own SSLSocketFactory.
     * <p>
     * This default implementation relies on the default one from the JVM, i.e. using the default trustStore
     * ($JRE/lib/security/cacerts).
     *
     * @return the SecureProtocolSocketFactory instance to be used to connect to https metadata URLs.
     */
    @Nonnull
    protected SecureProtocolSocketFactory newSslSocketFactory() {

        HostnameVerifier hostnameVerifier;

        if (!Boolean.getBoolean(DefaultBootstrap.SYSPROP_HTTPCLIENT_HTTPS_DISABLE_HOSTNAME_VERIFICATION)) {
            hostnameVerifier = new StrictHostnameVerifier();
        } else {
            hostnameVerifier = org.apache.commons.ssl.HostnameVerifier.ALLOW_ALL;
        }

        TLSProtocolSocketFactory tlsProtocolSocketFactory = new TLSProtocolSocketFactory(null, null,
                hostnameVerifier) {
            @Override
            protected void verifyHostname(Socket socket) throws SSLException {
                if (socket instanceof SSLSocket) {
                    SSLSocket sslSocket = (SSLSocket) socket;
                    try {
                        sslSocket.startHandshake();
                    } catch (IOException e) {
                        throw new SSLException(e);
                    }
                    SSLSession sslSession = sslSocket.getSession();
                    if (!sslSession.isValid()) {
                        throw new SSLException("SSLSession was invalid: Likely implicit handshake failure: "
                                + "Set system property javax.net.debug=all for details");
                    }
                    super.verifyHostname(sslSocket);
                }
            }
        };

        Protocol.registerProtocol("https", new Protocol("https", tlsProtocolSocketFactory, 443));

        return tlsProtocolSocketFactory;
    }
}