[java-opensaml] 06/06: Profile support for SAML 2 Assertion validation

Brent Putman putmanb at georgetown.edu
Fri Jan 24 23:06:11 EST 2020


This is an automated email from the git hooks/post-receive script.

putmanb pushed a commit to branch master
in repository java-opensaml.

View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=976b1eae09226e8eb7a3943d0acd0d1e239249c5

commit 976b1eae09226e8eb7a3943d0acd0d1e239249c5
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Tue Dec 17 15:20:13 2019 -0500

    Profile support for SAML 2 Assertion validation
---
 .../common/assertion/ValidationProcessingData.java |  70 +++
 .../opensaml/saml/common/profile/SAMLEventIds.java |   6 +
 .../DefaultAssertionValidationContextBuilder.java  | 572 +++++++++++++++++++++
 .../saml2/profile/impl/ValidateAssertions.java     | 460 +++++++++++++++++
 4 files changed, 1108 insertions(+)

diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/common/assertion/ValidationProcessingData.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/assertion/ValidationProcessingData.java
new file mode 100644
index 0000000..a077411
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/assertion/ValidationProcessingData.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.opensaml.saml.common.assertion;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Convenience class for holding the {@link ValidationContext} used to validate
+ * an assertion, along with the final {@link ValidationResult}.
+ * 
+ * <p>
+ * This is useful for storing the pair of post-validation data items on the object metadata of an assertion.
+ * </p>
+ */
+public class ValidationProcessingData {
+
+    /** The validation context. */
+    private ValidationContext context;
+
+    /** The validation result. */
+    private ValidationResult result;
+
+    /**
+     * Constructor.
+     *
+     * @param validationContext the validation context
+     * @param validationResult the validation result
+     */
+    public ValidationProcessingData(@Nonnull final ValidationContext validationContext, 
+            @Nonnull final ValidationResult validationResult) {
+        context = Constraint.isNotNull(validationContext, "ValidationContext was null");
+        result = Constraint.isNotNull(validationResult, "ValidationResult was null");
+    }
+
+    /**
+     * Get the validation context.
+     * 
+     * @return the validation context
+     */
+    @Nonnull public ValidationContext getContext() {
+        return context;
+    }
+
+    /**
+     * Get the validation result.
+     * 
+     * @return the validation result
+     */
+    @Nonnull public ValidationResult getResult() {
+        return result;
+    }
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/common/profile/SAMLEventIds.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/profile/SAMLEventIds.java
index e3daac9..d2cb54c 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/common/profile/SAMLEventIds.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/profile/SAMLEventIds.java
@@ -57,6 +57,12 @@ public final class SAMLEventIds {
     /** ID of event returned if SAML subject-id requirement is not met. */
     @Nonnull @NotEmpty public static final String SUBJECT_ID_REQ_FAILED = "SubjectIDReqFailed";
 
+    /** ID of event returned if there was a fatal error attempting to validate a SAML Assertion. */
+    @Nonnull @NotEmpty public static final String UNABLE_VALIDATE_ASSERTION = "UnableToValidateAssertion";
+
+    /** ID of event returned if a SAML Assertion was invalid. */
+    @Nonnull @NotEmpty public static final String ASSERTION_INVALID = "AssertionInvalid";
+
     /** Constructor. */
     private SAMLEventIds() {
         
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/DefaultAssertionValidationContextBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/DefaultAssertionValidationContextBuilder.java
new file mode 100644
index 0000000..e47887c
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/DefaultAssertionValidationContextBuilder.java
@@ -0,0 +1,572 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.opensaml.saml.saml2.profile.impl;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.namespace.QName;
+
+import net.shibboleth.utilities.java.support.collection.LazySet;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.messaging.MessageException;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.messaging.context.SAMLProtocolContext;
+import org.opensaml.saml.common.messaging.context.SAMLSelfEntityContext;
+import org.opensaml.saml.criterion.EntityRoleCriterion;
+import org.opensaml.saml.criterion.ProtocolCriterion;
+import org.opensaml.saml.criterion.RoleDescriptorCriterion;
+import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.profile.impl.ValidateAssertions.AssertionValidationInput;
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.opensaml.security.messaging.ServletRequestX509CredentialAdapter;
+import org.opensaml.security.x509.X509Credential;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+
+/**
+ *  Function which implements default behavior for building an instance of {@link ValidationContext}
+ *  from an instance of {@link AssertionValidationInput}.
+ */
+public class DefaultAssertionValidationContextBuilder 
+        implements Function<AssertionValidationInput, ValidationContext> {
+    
+    /** Logger. */
+    @Nullable private Logger log = LoggerFactory.getLogger(DefaultAssertionValidationContextBuilder.class);
+    
+    /** A function for resolving the signature validation CriteriaSet for a particular function. */
+    private Function<Pair<ProfileRequestContext, Assertion>, CriteriaSet> signatureCriteriaSetFunction;
+    
+    /** Predicate for determining whether an Assertion signature is required. */
+    private Predicate<ProfileRequestContext> signatureRequired;
+    
+    /** Predicate for determining whether an Assertion's network address(es) should be checked. */
+    private Predicate<ProfileRequestContext> checkAddress;
+    
+    /** Function for determining the max allowed time since authentication. */
+    private Function<ProfileRequestContext, Duration> maximumTimeSinceAuthn;
+
+    /** Resolver for security parameters context. */
+    private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultAssertionValidationContextBuilder() {
+        signatureRequired = Predicates.alwaysTrue();
+        checkAddress = Predicates.alwaysTrue();
+
+        securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class)
+                .compose(new InboundMessageContextLookup());
+    }
+
+    /**
+     * Get the strategy by which to resolve a {@link SecurityParametersContext}.
+     *
+     * @return the lookup strategy
+     */
+    @Nonnull public Function<ProfileRequestContext, SecurityParametersContext> getSecurityParametersLookupStrategy() {
+        return securityParametersLookupStrategy;
+    }
+
+    /**
+     * Set the strategy by which to resolve a {@link SecurityParametersContext}.
+     *
+     * @param strategy the strategy function
+     */
+    public void setSecurityParametersLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
+        securityParametersLookupStrategy =
+                Constraint.isNotNull(strategy, "SecurityParametersContext lookup strategy was null") ;
+    }
+
+    /**
+     * Get the predicate which determines whether an Assertion signature is required.
+     * 
+     * <p>
+     * Defaults to an always true predicate;
+     * </p>
+     * 
+     * @return the predicate
+     */
+    public Predicate<ProfileRequestContext> getSignatureRequired() {
+        return signatureRequired;
+    }
+
+    /**
+     * Set the predicate which determines whether an Assertion signature is required.
+     * 
+     * <p>
+     * Defaults to an always true predicate.
+     * </p>
+     * 
+     * @param predicate the predicate, must be non-null
+     */
+    public void setSignatureRequired(final @Nonnull Predicate<ProfileRequestContext> predicate) {
+        signatureRequired = Constraint.isNotNull(predicate, "Signature required predicate was null");
+    }
+
+    /**
+     * Get the predicate which determines whether an Assertion's network address(es) should be checked.
+     *
+     * <p>
+     * Defaults to an always true predicate;
+     * </p>
+     *
+     * @return the predicate
+     */
+    public Predicate<ProfileRequestContext> getCheckAddress() {
+        return checkAddress;
+    }
+
+    /**
+     * Set the predicate which determines whether an Assertion's network address(es) should be checked.
+     *
+     * <p>
+     * Defaults to an always true predicate.
+     * </p>
+     *
+     * @param predicate the predicate, must be non-null
+     */
+    public void setCheckAddress(final @Nonnull Predicate<ProfileRequestContext> predicate) {
+        checkAddress = Constraint.isNotNull(predicate, "Check address predicate was null");
+    }
+
+    /**
+     * Get the function for determining the max allowed time since authentication.
+     *
+     * <p>
+     * Defaults to null.
+     * </p>
+     *
+     * @return the function
+     */
+    public Function<ProfileRequestContext,Duration> getMaximumTimeSinceAuthn() {
+        return maximumTimeSinceAuthn;
+    }
+
+    /**
+     * Set the function for determining the max allowed time since authentication.
+     *
+     * <p>
+     * Defaults to null.
+     * </p>
+     *
+     * @param function the function, may be null
+     */
+    public void setMaximumTimeSinceAuthn(final @Nonnull Function<ProfileRequestContext,Duration> function) {
+        maximumTimeSinceAuthn = function;
+    }
+
+    /**
+     * Get the function for resolving the signature validation CriteriaSet for a particular function.
+     * 
+     * <p>
+     * Defaults to: {@code null}.
+     * </p>
+     * 
+     * @return a criteria set instance, or null
+     */
+    @Nullable public Function<Pair<ProfileRequestContext, Assertion>, CriteriaSet> getSignatureCriteriaSetFunction() {
+        return signatureCriteriaSetFunction;
+    }
+
+    /**
+     * Set the function for resolving the signature validation CriteriaSet for a particular function.
+     * 
+     * <p>
+     * Defaults to: {@code null}.
+     * </p>
+     * 
+     * @param function the resolving function, may be null
+     */
+    public void setSignatureCriteriaSetFunction(
+            @Nullable final Function<Pair<ProfileRequestContext, Assertion>, CriteriaSet> function) {
+        signatureCriteriaSetFunction = function;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public ValidationContext apply(@Nullable final AssertionValidationInput input) {
+        if (input == null) {
+            return null;
+        }
+        
+        return new ValidationContext(buildStaticParameters(input));
+    }
+    
+    /**
+     * Build the static parameters map for input to the {@link ValidationContext}.
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the static parameters map
+     */
+    @Nonnull protected Map<String,Object> buildStaticParameters(
+            @Nonnull final AssertionValidationInput input) {
+        
+        final HashMap<String, Object> staticParams = new HashMap<>();
+        
+        //For signature validation
+        staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, 
+                Boolean.valueOf(getSignatureRequired().test(input.getProfileRequestContext())));
+        staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_VALIDATION_CRITERIA_SET, 
+                getSignatureCriteriaSet(input));
+        final SecurityParametersContext securityParameters = getSecurityParametersLookupStrategy()
+                .apply(input.getProfileRequestContext());
+        if (securityParameters != null && securityParameters.getSignatureValidationParameters() != null) {
+            staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_VALIDATION_TRUST_ENGINE,
+                    securityParameters.getSignatureValidationParameters().getSignatureTrustEngine());
+        }
+        
+        // For HoK subject confirmation
+        final X509Certificate attesterCertificate = getAttesterCertificate(input);
+        if (attesterCertificate != null) {
+            staticParams.put(SAML2AssertionValidationParameters.SC_HOK_PRESENTER_CERT, attesterCertificate);
+        }
+        final PublicKey attesterPublicKey = getAttesterPublicKey(input);
+        if (attesterPublicKey != null) {
+            staticParams.put(SAML2AssertionValidationParameters.SC_HOK_PRESENTER_KEY, attesterPublicKey);
+        }
+        
+        final Set<InetAddress> validAddresses = getValidAddresses(input);
+        final Boolean checkAddressEnabled = Boolean.valueOf(getCheckAddress().test(input.getProfileRequestContext()));
+        
+        // For SubjectConfirmationData
+        staticParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, getValidRecipients(input));
+        staticParams.put(SAML2AssertionValidationParameters.SC_VALID_ADDRESSES, validAddresses);
+        staticParams.put(SAML2AssertionValidationParameters.SC_CHECK_ADDRESS, checkAddressEnabled);
+        
+        // For Audience Condition
+        staticParams.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, getValidAudiences(input));
+        
+        // For AuthnStatement
+        staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_VALID_ADDRESSES, validAddresses);
+        staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_CHECK_ADDRESS, checkAddressEnabled);
+        if (getMaximumTimeSinceAuthn() != null) {
+            staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_MAX_TIME, 
+                    getMaximumTimeSinceAuthn().apply(input.getProfileRequestContext()));
+        }
+        
+        log.trace("Built static parameters map: {}", staticParams);
+        
+        return staticParams;
+    }
+    
+    /**
+     * Get the signature validation criteria set.
+     * 
+     * <p>
+     * This implementation first evaluates the result of applying the function 
+     * {@link #getSignatureCriteriaSetFunction()}, if configured. If that evaluation did not
+     * produce an {@link EntityIdCriterion}, one is added based on the issuer of the {@link Assertion}.
+     * If that evaluation did not produce an instance of {@link UsageCriterion}, one is added with
+     * the value of {@link UsageType#SIGNING}.
+     * </p>
+     * 
+     * <p>
+     * Finally the following criteria are added if not already present and if the corresponding data
+     * is available in the inbound {@link MessageContext}:
+     * </p>
+     * <ul>
+     *  <li>{@link RoleDescriptorCriterion}</li>
+     *  <li>{@link EntityRoleCriterion}</li>
+     *  <li>{@link ProtocolCriterion}</li>
+     * </ul>
+     * 
+     * @param input the assertion validation input
+     *
+     * @return the criteria set based on the message context data
+     */
+    @Nonnull protected CriteriaSet getSignatureCriteriaSet(@Nonnull final AssertionValidationInput input) {
+        final CriteriaSet criteriaSet = new CriteriaSet();
+        
+        if (getSignatureCriteriaSetFunction() != null) {
+            final CriteriaSet dynamicCriteria = getSignatureCriteriaSetFunction().apply(
+                    new Pair<>(input.getProfileRequestContext(), input.getAssertion()));
+            if (dynamicCriteria != null) {
+                criteriaSet.addAll(dynamicCriteria);
+            }
+        }
+        
+        if (!criteriaSet.contains(EntityIdCriterion.class)) {
+            String issuer = null;
+            if (input.getAssertion().getIssuer() != null) {
+                issuer = StringSupport.trimOrNull(input.getAssertion().getIssuer().getValue());
+            }
+            if (issuer != null) {
+                log.debug("Adding internally-generated EntityIdCriterion with value of: {}", issuer);
+                criteriaSet.add(new EntityIdCriterion(issuer));
+            }
+        }
+        
+        if (!criteriaSet.contains(UsageCriterion.class)) {
+            log.debug("Adding internally-generated UsageCriterion with value of: {}", UsageType.SIGNING);
+            criteriaSet.add(new UsageCriterion(UsageType.SIGNING));
+        }
+        
+        final MessageContext inboundContext = input.getProfileRequestContext().getInboundMessageContext();
+        if (inboundContext != null) {
+            populateSignatureCriteriaFromInboundContext(criteriaSet, inboundContext);
+        }
+
+        log.debug("Resolved Signature validation CriteriaSet: {}", criteriaSet);
+        
+        return criteriaSet;
+    }
+
+    /**
+     * Populate signature criteria from the specified {@link MessageContext}.
+     *
+     * <ul>
+     *  <li>{@link RoleDescriptorCriterion}</li>
+     *  <li>{@link EntityRoleCriterion}</li>
+     *  <li>{@link ProtocolCriterion}</li>
+     * </ul>
+     *
+     * @param criteriaSet the criteria set to populate
+     * @param inboundContext the inbound message context
+     */
+    protected void populateSignatureCriteriaFromInboundContext(@Nonnull final CriteriaSet criteriaSet,
+            @Nonnull final MessageContext inboundContext) {
+
+        final SAMLPeerEntityContext peerContext = inboundContext.getSubcontext(SAMLPeerEntityContext.class);
+        if (peerContext != null) {
+            if (!criteriaSet.contains(RoleDescriptorCriterion.class)) {
+                final SAMLMetadataContext metadataContext = peerContext.getSubcontext(SAMLMetadataContext.class);
+                if (metadataContext != null && metadataContext.getRoleDescriptor() != null) {
+                    criteriaSet.add(new RoleDescriptorCriterion(metadataContext.getRoleDescriptor()));
+                }
+            }
+            if (!criteriaSet.contains(EntityRoleCriterion.class)) {
+                final QName role = peerContext.getRole();
+                if (role != null) {
+                    criteriaSet.add(new EntityRoleCriterion(role));
+                }
+            }
+        }
+
+        final SAMLProtocolContext protocolContext = inboundContext.getSubcontext(SAMLProtocolContext.class);
+        if (!criteriaSet.contains(ProtocolCriterion.class)
+                && protocolContext != null && protocolContext.getProtocol() != null) {
+            criteriaSet.add(new ProtocolCriterion(protocolContext.getProtocol()));
+        }
+    }
+
+    /**
+     * Get the attesting entity's {@link X509Certificate}.
+     * 
+     * <p>
+     * This implementation returns the client TLS certificate present in the 
+     * {@link javax.servlet.http.HttpServletRequest}, or null if one is not present.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the entity certificate, or null
+     */
+    @Nullable protected X509Certificate getAttesterCertificate(
+            @Nonnull final AssertionValidationInput input) {
+        try {
+            final X509Credential credential = new ServletRequestX509CredentialAdapter(input.getHttpServletRequest());
+            return credential.getEntityCertificate();
+        } catch (final SecurityException e) {
+            log.debug("Peer TLS X.509 certificate was not present. " 
+                    + "Holder-of-key proof-of-possession via client TLS cert will not be possible");
+            return null;
+        }
+    }
+
+    /**
+     * Get the attesting entity's {@link PublicKey}.
+     * 
+     * <p>
+     * This implementation returns null. Subclasses should override to implement specific logic.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the entity public key, or null
+     */
+    @Nullable protected PublicKey getAttesterPublicKey(@Nonnull final AssertionValidationInput input) {
+        return null;
+    }
+    
+    /**
+     * Get the valid recipient endpoints for attestation.
+     * 
+     * <p>
+     * This implementation returns a set containing the 2 values;
+     * <ol>
+     * <li>
+     * {@link javax.servlet.http.HttpServletRequest#getRequestURL()}
+     * </li>
+     * <li>
+     * if present, {@link SAMLSelfEntityContext#getEntityId()}
+     * </li>
+     * </ol>
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return set of recipient endpoint URI's
+     */
+    @Nonnull protected Set<String> getValidRecipients(@Nonnull final AssertionValidationInput input) {
+        final LazySet<String> validRecipients = new LazySet<>();
+        
+        try {
+            final String endpoint = SAMLBindingSupport.getActualReceiverEndpointURI(
+                    input.getProfileRequestContext().getInboundMessageContext(), input.getHttpServletRequest());
+            if (endpoint != null) {
+                validRecipients.add(endpoint);
+            }
+        } catch (final MessageException e) {
+            log.warn("Attempt to resolve recipient endpoint failed", e);
+        }
+        
+        final String selfEntityID = getSelfEntityID(input);
+        if (selfEntityID != null) {
+            validRecipients.add(selfEntityID);
+        }
+        
+        log.debug("Resolved valid subject confirmation recipients set: {}", validRecipients);
+        return validRecipients;
+    }
+
+    /**
+     * Get the set of addresses which are valid for subject confirmation.
+     * 
+     * <p>
+     * This implementation simply returns the set based on 
+     * {@link #getAttesterIPAddress(AssertionValidationInput)}, if that produces a value.
+     * Otherwise an empty set is returned.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the set of valid addresses
+     */
+    @Nonnull protected Set<InetAddress> getValidAddresses(@Nonnull final AssertionValidationInput input) {
+        try {
+            final LazySet<InetAddress> validAddresses = new LazySet<>();
+            InetAddress[] addresses = null;
+            final String attesterIPAddress = getAttesterIPAddress(input);
+            log.debug("Saw attester IP address: {}", attesterIPAddress);
+            if (attesterIPAddress != null) {
+                addresses = InetAddress.getAllByName(attesterIPAddress);
+                validAddresses.addAll(Arrays.asList(addresses));
+                log.debug("Resolved valid subject confirmation InetAddress set: {}", validAddresses);
+                return validAddresses;
+            }
+            log.warn("Could not determine attester IP address. Validation of Assertion may or may not succeed");
+            return Collections.emptySet();
+        } catch (final UnknownHostException e) {
+            log.warn("Processing of attester IP address failed. Validation of Assertion may or may not succeed", e);
+            return Collections.emptySet();
+        }
+    }
+    
+    /**
+     * Get the attester's IP address.
+     * 
+     * <p>
+     * This implementation returns the value of {@link javax.servlet.http.HttpServletRequest#getRemoteAddr()}.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the IP address of the attester
+     */
+    @Nonnull protected String getAttesterIPAddress(@Nonnull final AssertionValidationInput input) {
+        //TODO support indirection via SAMLBindingSupport and use of SAMLMessageReceivedEndpointContext?
+        return input.getHttpServletRequest().getRemoteAddr();
+    }
+    
+    /**
+     * Get the valid audiences for attestation.
+     * 
+     * <p>
+     * This implementation returns a set containing the single entityID held by the message context's 
+     * {@link SAMLSelfEntityContext#getEntityId()}, if present.  Otherwise an empty set is returned.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return set of audience URI's
+     */
+    @Nonnull protected Set<String> getValidAudiences(@Nonnull final AssertionValidationInput input) {
+        final LazySet<String> validAudiences = new LazySet<>();
+        
+        final String selfEntityID = getSelfEntityID(input);
+        if (selfEntityID != null) {
+            validAudiences.add(selfEntityID);
+        }
+        
+        log.debug("Resolved valid audiences set: {}", validAudiences);
+        return validAudiences;
+    }
+    
+    /**
+     * Get the self entityID.
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the self entityID, or null if could not be resolved
+     */
+    @Nullable protected String getSelfEntityID(@Nonnull final AssertionValidationInput input) {
+        final SAMLSelfEntityContext selfContext = input.getProfileRequestContext()
+                .getInboundMessageContext()
+                .getSubcontext(SAMLSelfEntityContext.class);
+        
+        if (selfContext != null) {
+            return selfContext.getEntityId();
+        }
+        
+        return null;
+    }
+
+}
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/ValidateAssertions.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/ValidateAssertions.java
new file mode 100644
index 0000000..ca320c7
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/profile/impl/ValidateAssertions.java
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID 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.opensaml.saml.saml2.profile.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.action.AbstractProfileAction;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.assertion.AssertionValidationException;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.assertion.ValidationProcessingData;
+import org.opensaml.saml.common.assertion.ValidationResult;
+import org.opensaml.saml.common.profile.SAMLEventIds;
+import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A profile action which resolves SAML 2.0 Assertions from the profile request context
+ * and validates them using a resolved or configured instance of {@link SAML20AssertionValidator}.
+ * 
+ * <p>
+ * The {@link ValidationResult} along with the {@link ValidationContext} used are stored in the assertion's
+ * {@link XMLObject#getObjectMetadata()} as instance of {@link ValidationProcessingData}.
+ * 
+ * </p>
+ */
+public class ValidateAssertions extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateAssertions.class);
+    
+    /** The HttpServletRequest being processed. */
+    @NonnullAfterInit private HttpServletRequest httpServletRequest;
+    
+    /** Flag which indicates whether a failure of Assertion validation should be considered fatal. */
+    private boolean invalidFatal;
+    
+    /** The SAML 2.0 Assertion validator, may be null.*/
+    @Nullable private SAML20AssertionValidator assertionValidator;
+    
+    /** The SAML 2.0 Assertion validator lookup function, may be null.*/
+    @Nullable
+    private Function<Pair<ProfileRequestContext, Assertion>, SAML20AssertionValidator> assertionValidatorLookup;
+    
+    /** Function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance. */
+    @NonnullAfterInit private Function<AssertionValidationInput, ValidationContext> validationContextBuilder;
+    
+    /** The resolver for the list of assertions to be validated. */
+    @Nonnull private Function<ProfileRequestContext, List<Assertion>> assertionResolver;
+    
+    /** The resolved assertions to be validated. */
+    private List<Assertion> assertions;
+    
+    /** Constructor. */
+    public ValidateAssertions() {
+        super();
+        setInvalidFatal(true);
+        setValidationContextBuilder(new DefaultAssertionValidationContextBuilder());
+        setAssertionResolver(new DefaultAssertionResolver());
+    }
+
+    /**
+     * Get the function which resolves the list of assertions to validate.
+     * 
+     * @return the assertion resolver function
+     */
+    @Nonnull public Function<ProfileRequestContext, List<Assertion>> getAssertionResolver() {
+        return assertionResolver;
+    }
+
+    /**
+     * Set the function which resolves the list of assertions to validate.
+     * 
+     * @param function the new assertion resolver function
+     */
+    public void setAssertionResolver(@Nonnull final Function<ProfileRequestContext, List<Assertion>> function) {
+        assertionResolver = Constraint.isNotNull(function, "The Assertion resolver function may not be null");
+    }
+
+    /**
+     * Get the function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance.
+     * 
+     * <p>
+     * Defaults to an instance of {@link DefaultAssertionValidationContextBuilder}.
+     * </p>
+     * 
+     * @return the builder function
+     */
+    @NonnullAfterInit
+    public Function<AssertionValidationInput, ValidationContext> getValidationContextBuilder() {
+        return validationContextBuilder;
+    }
+
+    /**
+     * Set the function that builds a {@link ValidationContext} instance based on a 
+     * {@link AssertionValidationInput} instance.
+     * 
+     * <p>
+     * Defaults to an instance of {@link DefaultAssertionValidationContextBuilder}.
+     * </p>
+     * 
+     * @param builder the builder function
+     */
+    public void setValidationContextBuilder(
+            @Nonnull final Function<AssertionValidationInput, ValidationContext> builder) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        validationContextBuilder = Constraint.isNotNull(builder, "Validation context builder may not be null");
+    }
+
+    /**
+     * Get the HTTP servlet request being processed.
+     * 
+     * @return the HTTP servlet request
+     */
+    @NonnullAfterInit public HttpServletRequest getHttpServletRequest() {
+        return httpServletRequest;
+    }
+
+    /**
+     * Set the HTTP servlet request being processed.
+     * 
+     * @param request The HTTP servlet request
+     */
+    public void setHttpServletRequest(@Nonnull final HttpServletRequest request) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        httpServletRequest = Constraint.isNotNull(request, "HttpServletRequest cannot be null");
+    }
+    
+    /**
+     * Get flag which indicates whether a failure of Assertion validation should be considered a fatal processing error.
+     * 
+     * <p>
+     * Defaults to: {@code true}.
+     * </p>
+     * 
+     * @return Returns the invalidFatal.
+     */
+    public boolean isInvalidFatal() {
+        return invalidFatal;
+    }
+
+    /**
+     * Set flag which indicates whether a failure of Assertion validation should be considered a fatal processing error.
+     * 
+     * <p>
+     * Defaults to: {@code true}.
+     * </p>
+     * 
+     * @param flag The invalidFatal to set.
+     */
+    public void setInvalidFatal(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        invalidFatal = flag;
+    }
+    
+    /**
+     * Get the locally-configured Assertion validator.
+     * 
+     * @return the local Assertion validator, or null
+     */
+    @Nullable public SAML20AssertionValidator getAssertionValidator() {
+        return assertionValidator;
+    }
+
+    /**
+     * Set the locally-configured Assertion validator.
+     * 
+     * @param validator the local Assertion validator, may be null
+     */
+    public void setAssertionValidator(@Nullable final SAML20AssertionValidator validator) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        assertionValidator = validator;
+    }
+    
+    /**
+     * Get the Assertion validator lookup function.
+     * 
+     * @return the Assertion validator lookup function, or null
+     */
+    @Nullable
+    public Function<Pair<ProfileRequestContext, Assertion>, SAML20AssertionValidator> getAssertionValidatorLookup() {
+        return assertionValidatorLookup;
+    }
+
+    /**
+     * Set the Assertion validator lookup function.
+     * 
+     * @param function the Assertion validator lookup function, may be null
+     */
+    public void setAssertionValidatorLookup(
+            @Nullable final Function<Pair<ProfileRequestContext, Assertion>, SAML20AssertionValidator> function) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        assertionValidatorLookup = function;
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (getValidationContextBuilder() == null) {
+            throw new ComponentInitializationException("ValidationContext builder cannot be null");
+        }
+        
+        if (getHttpServletRequest() == null) {
+            throw new ComponentInitializationException("HttpServletRequest cannot be null");
+        }
+        
+        if (getAssertionValidator() == null) {
+            if (getAssertionValidatorLookup() == null) {
+                throw new ComponentInitializationException("Both Assertion validator and lookup function were null");
+            }
+            log.info("{} Assertion validator is null, must be resovleable via the lookup function", getLogPrefix());
+        }
+    }
+
+    /** {@inheritDoc} */
+    protected void doDestroy() {
+        httpServletRequest = null;
+        
+        super.doDestroy();
+    }
+
+    /** {@inheritDoc} */
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        assertions = getAssertionResolver().apply(profileRequestContext);
+        if (assertions == null || assertions.isEmpty()) {
+            log.info("{} Profile context contained no Assertions to validate. Skipping further processing",
+                    getLogPrefix());
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    protected void doExecute(@Nonnull final ProfileRequestContext profileContext) {
+        boolean sawNonValid = false;
+        for (final Assertion assertion : assertions) {
+            final SAML20AssertionValidator validator = resolveValidator(profileContext, assertion);
+            if (validator == null) {
+                log.warn("{} No SAML20AssertionValidator was available, terminating", getLogPrefix());
+                ActionSupport.buildEvent(profileContext, SAMLEventIds.UNABLE_VALIDATE_ASSERTION);
+                return;
+            }
+        
+            try { 
+                final ValidationContext validationContext = buildValidationContext(profileContext, assertion);
+            
+                final ValidationResult validationResult = validator.validate(assertion, validationContext);
+                if (validationResult != ValidationResult.VALID) {
+                    sawNonValid = true;
+                }
+                processResult(validationContext, validationResult, assertion, profileContext);
+            } catch (final AssertionValidationException e) {
+                log.warn("{} There was a problem determining Assertion validity: {}", getLogPrefix(), e.getMessage());
+                ActionSupport.buildEvent(profileContext, SAMLEventIds.UNABLE_VALIDATE_ASSERTION);
+                return;
+            }
+        }
+        
+        if (sawNonValid && isInvalidFatal()) {
+            ActionSupport.buildEvent(profileContext, SAMLEventIds.ASSERTION_INVALID);
+        } else {
+            ActionSupport.buildProceedEvent(profileContext);
+        }
+    }
+
+    /**
+     * Process the result of the assertion validation.
+     * 
+     * @param validationContext the Assertion validation context
+     * @param validationResult the Assertion validation result
+     * @param assertion the assertion being evaluated produced
+     * @param profileContext the current profile request context
+     */
+    protected void processResult(@Nonnull final ValidationContext validationContext, 
+            @Nonnull final ValidationResult validationResult, @Nonnull final Assertion assertion, 
+            @Nonnull final ProfileRequestContext profileContext) {
+
+        log.debug("{} Assertion validation result was: {}", getLogPrefix(), validationResult);
+        if (validationResult != ValidationResult.VALID) {
+            log.debug("{} Assertion validation failure msg was: {}",
+                    getLogPrefix(), validationContext.getValidationFailureMessage());
+        }
+
+        assertion.getObjectMetadata().put(new ValidationProcessingData(validationContext, validationResult));
+    }
+
+    /**
+     * Resolve the Assertion token validator to use with the specified Assertion.
+     * 
+     * @param profileContext the current profile context
+     * @param assertion the assertion being evaluated
+     * 
+     * @return the token validator
+     */
+    @Nullable protected SAML20AssertionValidator resolveValidator(@Nonnull final ProfileRequestContext profileContext, 
+            @Nonnull final Assertion assertion) {
+        
+        if (getAssertionValidatorLookup() != null) {
+            log.debug("{} Attempting to resolve SAML 2 Assertion validator via lookup function", getLogPrefix());
+            final SAML20AssertionValidator validator = getAssertionValidatorLookup().apply(
+                    new Pair<>(profileContext, assertion));
+            if (validator != null) {
+                log.debug("{} Resolved SAML 2 Assertion validator via lookup function", getLogPrefix());
+                return validator;
+            }
+        }
+        
+        if (getAssertionValidator() != null) {
+            log.debug("{} Resolved locally configured SAML 2 Assertion validator", getLogPrefix());
+            return getAssertionValidator();
+        }
+        
+        log.debug("{} No SAML 2 Assertion validator could be resolved", getLogPrefix());
+        return null;
+    }
+
+    /**
+     * Build the Assertion ValidationContext.
+     * 
+     * @param profileContext the current profile context
+     * @param assertion the assertion which is to be validated
+     * 
+     * @return the new Assertion validation context to use
+     * 
+     * @throws AssertionValidationException if no validation context instance could be built
+     */
+    @Nonnull protected ValidationContext buildValidationContext(@Nonnull final ProfileRequestContext profileContext, 
+            @Nonnull final Assertion assertion) throws AssertionValidationException {
+        
+        final ValidationContext validationContext = getValidationContextBuilder().apply(
+                new AssertionValidationInput(profileContext, getHttpServletRequest(), assertion));
+        
+        if (validationContext == null) {
+            log.warn("{} ValidationContext produced was null", getLogPrefix());
+            throw new AssertionValidationException("Assertion ValidationContext was null");
+        }
+        
+        return validationContext;
+    }
+    
+    /**
+     * The default assertion resolver function.
+     */
+    public class DefaultAssertionResolver implements Function<ProfileRequestContext, List<Assertion>> {
+
+        /** {@inheritDoc} */
+        public List<Assertion> apply(@Nonnull final ProfileRequestContext profileContext) {
+            final SAMLObject message = (SAMLObject) profileContext.getInboundMessageContext().getMessage();
+            if (message instanceof Response) {
+                return ((Response) message).getAssertions();
+            }
+            
+            return null;
+        }
+        
+    }
+    
+    /**
+     * Class which holds data relevant to validating a SAML 2.0 Assertion.
+     */
+    public class AssertionValidationInput {
+        
+        /** The profile request context input. */
+        private ProfileRequestContext profileContext;
+        
+        /** The HTTP request input. */
+        private HttpServletRequest httpServletRequest;
+        
+        /** The Assertion being evaluated. */
+        private Assertion assertion;
+
+        /**
+         * Constructor.
+         *
+         * @param context the profile request context being evaluated
+         * @param request the HTTP request being evaluated
+         * @param samlAssertion the assertion being evaluated
+         */
+        public AssertionValidationInput(@Nonnull final ProfileRequestContext context,
+                @Nonnull final HttpServletRequest request, @Nonnull final Assertion samlAssertion) {
+            profileContext = Constraint.isNotNull(context, "ProfileRequestContext may not be null");
+            httpServletRequest = Constraint.isNotNull(request, "HttpServletRequest may not be null");
+            assertion = Constraint.isNotNull(samlAssertion, "Assertion may not be null");
+        }
+
+        /**
+         * Get the {@link ProfileRequestContext} input.
+         * 
+         * @return the message context input
+         */
+        @Nonnull public ProfileRequestContext getProfileRequestContext() {
+            return profileContext;
+        }
+
+        /**
+         * Get the {@link HttpServletRequest} input.
+         * 
+         * @return the HTTP servlet request input
+         */
+        @Nonnull public HttpServletRequest getHttpServletRequest() {
+            return httpServletRequest;
+        }
+        
+        /**
+         * Get the {@link Assertion} being evaluated.
+         * 
+         * @return the Assertion being validated
+         */
+        @Nonnull public Assertion getAssertion() {
+            return assertion;
+        }
+
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list