net.link.util.saml.Saml2Utils.java Source code

Java tutorial

Introduction

Here is the source code for net.link.util.saml.Saml2Utils.java

Source

/*
 * Lisu project.
 *
 * Copyright 2010 Lin.k N.V. All rights reserved.
 * Lin.k N.V. proprietary/confidential. Use is subject to license terms.
 */

package net.link.util.saml;

import static com.google.common.base.Preconditions.*;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import java.security.cert.X509Certificate;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import net.link.util.common.CertificateChain;
import net.link.util.exception.ValidationFailedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.ReadableDateTime;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.binding.BasicSAMLMessageContext;
import org.opensaml.saml2.binding.decoding.HTTPRedirectDeflateDecoder;
import org.opensaml.saml2.core.*;
import org.opensaml.saml2.metadata.SPSSODescriptor;
import org.opensaml.ws.message.MessageContext;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.security.SecurityPolicy;
import org.opensaml.ws.security.SecurityPolicyResolver;
import org.opensaml.ws.security.provider.BasicSecurityPolicy;
import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
import org.opensaml.xml.Configuration;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.schema.XSAny;
import org.opensaml.xml.security.SecurityHelper;
import org.opensaml.xml.security.credential.Credential;
import org.opensaml.xml.security.credential.StaticCredentialResolver;
import org.opensaml.xml.signature.*;
import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine;

/**
 * SAML v2.0 utility class
 */
@SuppressWarnings("UnusedDeclaration")
public abstract class Saml2Utils extends SamlUtils {

    /**
     * Checks whether the assertion is well formed and validates against some given data.
     *
     * @param assertion        the assertion to validate
     * @param now              Check whether the assertion is valid at this instant.
     * @param expectedAudience expected audience matching the optional audience restriction
     *
     * @throws ValidationFailedException validation failed.
     */
    public static void validateAssertion(@NotNull Assertion assertion, @Nullable ReadableDateTime now,
            @Nullable String expectedAudience) throws ValidationFailedException {

        Conditions conditions = assertion.getConditions();
        DateTime notBefore = conditions.getNotBefore();
        DateTime notOnOrAfter = conditions.getNotOnOrAfter();

        logger.dbg("now: %s", now);
        logger.dbg("notBefore: %s", notBefore);
        logger.dbg("notOnOrAfter : %s", notOnOrAfter);

        if (now != null) {
            if (now.isBefore(notBefore)) {
                // time skew
                DateTime nowDt = now.toDateTime();
                if (nowDt.plusMinutes(5).isBefore(notBefore) || nowDt.minusMinutes(5).isAfter(notOnOrAfter))
                    throw new ValidationFailedException("SAML2 assertion validation audience=" + expectedAudience
                            + " : invalid SAML message timeframe");
            } else if (now.isBefore(notBefore) || now.isAfter(notOnOrAfter))
                throw new ValidationFailedException("SAML2 assertion validation audience=" + expectedAudience
                        + " : invalid SAML message timeframe");
        }

        Subject subject = assertion.getSubject();
        if (null == subject)
            throw new ValidationFailedException(
                    "SAML2 assertion validation audience=" + expectedAudience + " : missing Assertion Subject");

        if (subject.getSubjectConfirmations().isEmpty())
            throw new ValidationFailedException(
                    "SAML2 assertion validation audience=" + expectedAudience + " : missing SubjectConfirmation");

        SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmations().get(0);
        SubjectConfirmationData subjectConfirmationData = subjectConfirmation.getSubjectConfirmationData();
        if (!subjectConfirmationData.getUnknownXMLObjects(KeyInfo.DEFAULT_ELEMENT_NAME).isEmpty()) {
            // meaning a PublicKey is attached
            if (subjectConfirmationData.getUnknownXMLObjects(KeyInfo.DEFAULT_ELEMENT_NAME).size() != 1)
                throw new ValidationFailedException("SAML2 assertion validation audience=" + expectedAudience
                        + " : more then 1 KeyInfo element in SubjectConfirmationData");
        }

        if (assertion.getAuthnStatements().isEmpty())
            throw new ValidationFailedException(
                    "SAML2 assertion validation audience=" + expectedAudience + " : missing AuthnStatement");

        AuthnStatement authnStatement = assertion.getAuthnStatements().get(0);
        if (null == authnStatement.getAuthnContext())
            throw new ValidationFailedException(
                    "SAML2 assertion validation audience=" + expectedAudience + " : missing AuthnContext");

        if (null == authnStatement.getAuthnContext().getAuthnContextClassRef())
            throw new ValidationFailedException(
                    "SAML2 assertion validation audience=" + expectedAudience + " : missing AuthnContextClassRef");

        if (expectedAudience != null)
            // Check whether the audience of the response corresponds to the original audience restriction
            validateAudienceRestriction(conditions, expectedAudience);
    }

    /**
     * Validates the audience restriction in specified {@link Conditions} against the specified audience..
     *
     * @param conditions       conditions to validate
     * @param expectedAudience audience expected to be found.
     *
     * @throws ValidationFailedException validation failed.
     */
    public static void validateAudienceRestriction(@NotNull Conditions conditions, @NotNull String expectedAudience)
            throws ValidationFailedException {

        List<AudienceRestriction> audienceRestrictions = conditions.getAudienceRestrictions();
        if (audienceRestrictions.isEmpty())
            throw new ValidationFailedException("SAML2 assertion validation audience=" + expectedAudience
                    + " : no Audience Restrictions found in response assertion");

        AudienceRestriction audienceRestriction = audienceRestrictions.get(0);
        List<Audience> audiences = audienceRestriction.getAudiences();
        if (audiences.isEmpty())
            throw new ValidationFailedException("SAML2 assertion validation audience=" + expectedAudience
                    + " : no Audiences found in AudienceRestriction");

        Audience audience = audiences.get(0);
        if (!expectedAudience.equals(audience.getAudienceURI()))
            throw new ValidationFailedException("SAML2 assertion validation: audience name not correct, expected: "
                    + expectedAudience + " was: " + audience.getAudienceURI());
    }

    public static void validateRedirectSignature(HttpServletRequest request,
            Collection<X509Certificate> trustedCertificates) throws ValidationFailedException {

        logger.dbg("validate[HTTP Redirect], Query:\n%s", request.getQueryString());
        if (trustedCertificates == null || trustedCertificates.isEmpty())
            throw new ValidationFailedException("There are no credentials to validate against.");

        Collection<Credential> trustedCredentials = Collections2.transform(trustedCertificates,
                new Function<X509Certificate, Credential>() {
                    @Override
                    public Credential apply(final X509Certificate from) {

                        return SecurityHelper.getSimpleCredential(from, null);
                    }
                });
        StaticCredentialResolver credResolver = new StaticCredentialResolver(
                ImmutableList.copyOf(trustedCredentials));
        final SignatureTrustEngine engine = new ExplicitKeySignatureTrustEngine(credResolver,
                Configuration.getGlobalSecurityConfiguration().getDefaultKeyInfoCredentialResolver());

        BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject> messageContext = new BasicSAMLMessageContext<SAMLObject, SAMLObject, SAMLObject>();
        messageContext.setInboundMessageTransport(new HttpServletRequestAdapter(request));
        messageContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
        messageContext.setSecurityPolicyResolver(new SecurityPolicyResolver() {

            @Override
            public Iterable<SecurityPolicy> resolve(MessageContext criteria)
                    throws org.opensaml.xml.security.SecurityException {

                return Collections.singleton(resolveSingle(criteria));
            }

            @Override
            public SecurityPolicy resolveSingle(MessageContext criteria) throws SecurityException {

                SecurityPolicy securityPolicy = new BasicSecurityPolicy();
                securityPolicy.getPolicyRules().add(new HTTPRedirectForceSignedRule(engine));

                return securityPolicy;
            }
        });

        try {
            new HTTPRedirectDeflateDecoder().decode(messageContext);
        } catch (MessageDecodingException e) {
            throw new ValidationFailedException("Signature validation failed.", e);
        } catch (org.opensaml.xml.security.SecurityException e) {
            throw new ValidationFailedException("Signature validation failed.", e);
        }
    }

    /**
     * Validate the signature on specified {@link Signature}.
     *
     * @param signature           Signature to validate if not using HTTP Redirect binding.
     * @param request             Request to obtain signature from if using HTTP Redirect binding.
     * @param trustedCertificates Certificates that are trusted.  The signature must validate against one of these.
     *
     * @return The certificate chain provided by the request or <code>null</code> if the binding does not support passing certificate
     * information. The chain can be empty if the binding supports passing certificate information but none are provided.
     *
     * @throws ValidationFailedException The signature could not be trusted.
     */
    @Nullable
    public static CertificateChain validateSignature(Signature signature, HttpServletRequest request,
            Collection<X509Certificate> trustedCertificates) throws ValidationFailedException {

        if (signature != null)
            return validatePostSignature(signature, trustedCertificates);

        // No signature inside the SAML object, must be in the queryString (SAML HTTP Redirect Binding)
        validateRedirectSignature(
                checkNotNull(request,
                        "No signature found in SAML object and no query data provided to search for a signature."),
                trustedCertificates);

        return null;
    }

    /**
     * Generates an XML {@link XSAny} object filled with specified value.
     *
     * @param attributeValue attribute value to convert
     *
     * @return converted {@link XMLObject}
     */
    public static XMLObject toAttributeValue(Object attributeValue) {

        return toAttributeValue(AttributeValue.DEFAULT_ELEMENT_NAME, attributeValue);
    }
}