[java-opensaml] 08/11: Support required Conditions during SAML 2 Assertion validation.

Brent Putman putmanb at georgetown.edu
Thu Jan 30 02:17:33 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=b0d611117751ba230dcb64c3112f4dcfa603a8db

commit b0d611117751ba230dcb64c3112f4dcfa603a8db
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Wed Jan 29 23:10:43 2020 -0500

    Support required Conditions during SAML 2 Assertion validation.
---
 .../saml2/assertion/SAML20AssertionValidator.java  |  52 ++++++++
 .../SAML2AssertionValidationParameters.java        |   6 +
 .../DefaultAssertionValidationContextBuilder.java  |  47 +++++++-
 .../assertion/SAML20AssertionValidatorTest.java    | 133 ++++++++++++++++++++-
 4 files changed, 235 insertions(+), 3 deletions(-)

diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidator.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidator.java
index 815f68b..b3f9185 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidator.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidator.java
@@ -21,6 +21,7 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -482,6 +483,11 @@ public class SAML20AssertionValidator {
     @Nonnull protected ValidationResult validateConditions(@Nonnull final Assertion assertion, 
             @Nonnull final ValidationContext context) throws AssertionValidationException {
         
+        final ValidationResult requiredConditionsResult = validateRequiredConditions(assertion, context);
+        if (requiredConditionsResult != ValidationResult.VALID) {
+            return requiredConditionsResult;
+        }
+                
         final Conditions conditions = assertion.getConditions();
         if (conditions == null) {
             log.debug("Assertion contained no Conditions element");
@@ -525,6 +531,51 @@ public class SAML20AssertionValidator {
     }
     
     /**
+     * Validate that all conditions indicated to be required are present in the assertion.
+     * 
+     * @param assertion the assertion whose conditions will be evaluated
+     * @param context current validation context
+     * 
+     * @return the result of the validation evaluation
+     */
+    protected ValidationResult validateRequiredConditions(@Nonnull final Assertion assertion,
+            @Nonnull final ValidationContext context) {
+        
+        @SuppressWarnings("unchecked")
+        final Set<QName> requiredConditions = (Set<QName>) context.getStaticParameters()
+                .get(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS);
+        
+        if (requiredConditions == null || requiredConditions.isEmpty()) {
+            log.debug("No Conditions were indicated as required");
+            return ValidationResult.VALID;
+        }
+        
+        final Conditions conditions = assertion.getConditions();
+        if (conditions == null || conditions.getConditions().isEmpty()) {
+            log.warn("At least 1 Condition was indicated as required, but Assertion contained no Conditions");
+            context.setValidationFailureMessage(
+                    "At least 1 Condition was indicated as required, but Assertion contained no Conditions");
+            return ValidationResult.INVALID;
+        }
+        
+        for (final QName requiredCondition : requiredConditions) {
+            final List<Condition> found = conditions.getConditions(requiredCondition);
+            if (found == null || found.isEmpty()) {
+                String msg = String.format("Condition '%s' was required, but was not found in assertion '%s'",
+                        requiredCondition, assertion.getID());
+                if (context.getValidationFailureMessage() != null) {
+                    msg = msg + ": " + context.getValidationFailureMessage();
+                }
+                log.warn(msg);
+                context.setValidationFailureMessage(msg);
+                return ValidationResult.INVALID;
+            }
+        }
+        
+        return ValidationResult.VALID;
+    }
+
+    /**
      * Validates the NotBefore and NotOnOrAfter Conditions constraints on the assertion.
      * 
      * @param assertion the assertion whose conditions will be validated
@@ -655,4 +706,5 @@ public class SAML20AssertionValidator {
 
         return ValidationResult.VALID;
     }
+
 }
\ No newline at end of file
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML2AssertionValidationParameters.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML2AssertionValidationParameters.java
index 7166303..73ac07b 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML2AssertionValidationParameters.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/SAML2AssertionValidationParameters.java
@@ -145,6 +145,12 @@ public final class SAML2AssertionValidationParameters {
     public static final String SC_HOK_CONFIRMED_KEYINFO = STD_PREFIX + SC_INFIX + ".HoK.ConfirmedKeyInfo";
 
     /**
+     * Carries a {@link java.util.Set}<code><</code>{@link javax.xml.QName}<code>></code>
+     * of required conditions which must be present in the assertion.
+     */
+    public static final String COND_REQUIRED_CONDITIONS = STD_PREFIX + COND_INFIX + ".RequiredConditions";
+
+    /**
      * Carries a {@link java.util.Set}<code><</code>{@link java.lang.String}<code>></code>
      * whose values are the acceptable 
      * {@link org.opensaml.saml.saml2.core.AudienceRestriction} {@link org.opensaml.saml.saml2.core.Audience}
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
index 2e006e8..b8afff4 100644
--- 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
@@ -25,10 +25,12 @@ import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -116,7 +118,8 @@ public class DefaultAssertionValidationContextBuilder
     /** Predicate for determining whether an Assertion SubjectConfirmationData Address is required. */
     private Predicate<ProfileRequestContext> addressRequired;
     
-    
+    /** The set of required Conditions. */
+    private Set<QName> requiredConditions;
 
     /** Resolver for security parameters context. */
     private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
@@ -134,6 +137,7 @@ public class DefaultAssertionValidationContextBuilder
         notOnOrAfterRequired = Predicates.alwaysFalse();
         notBeforeRequired = Predicates.alwaysFalse();
         addressRequired = Predicates.alwaysFalse();
+        requiredConditions = Collections.emptySet();
 
         securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class)
                 .compose(new InboundMessageContextLookup());
@@ -160,6 +164,28 @@ public class DefaultAssertionValidationContextBuilder
     }
 
     /**
+     * Get the set of required Conditions.
+     * 
+     * @return the required conditions, may be null
+     */
+    @Nonnull public Set<QName> getRequiredConditions() {
+        return requiredConditions;
+    }
+
+    /**
+     * Set the set of required Conditions.
+     * 
+     * @param conditions the required conditions
+     */
+    public void setRequiredConditions(@Nullable final Set<QName> conditions) {
+        if (conditions != null) {
+            requiredConditions = conditions.stream().filter(Objects::nonNull).collect(Collectors.toUnmodifiableSet());
+        } else {
+            requiredConditions = Collections.emptySet();
+        }
+    }
+
+    /**
      * Get the predicate which determines whether to include the self entityID as a valid Recipient.
      * 
      * <p>
@@ -540,6 +566,9 @@ public class DefaultAssertionValidationContextBuilder
         staticParams.put(SAML2AssertionValidationParameters.SC_NOT_ON_OR_AFTER_REQUIRED,
                 Boolean.valueOf(getNotOnOrAfterRequired().test(input.getProfileRequestContext())));
         
+        // For general Conditions
+        staticParams.put(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS, getRequiredConditions(input));
+        
         // For Audience Condition
         staticParams.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, getValidAudiences(input));
         
@@ -557,6 +586,22 @@ public class DefaultAssertionValidationContextBuilder
     }
     
     /**
+     * Get the set of required Conditions.
+     * 
+     * <p>
+     * The default behavior is to return the locally-configured data via {@link #getRequiredConditions()}.
+     * </p>
+     * 
+     * @param input the assertion validation input
+     * 
+     * @return the set of required Condition names, may be null
+     */
+    @Nonnull protected Set<QName> getRequiredConditions(@Nonnull final AssertionValidationInput input) {
+        // Subclasses may override
+        return getRequiredConditions();
+    }
+
+    /**
      * Get the signature validation criteria set.
      * 
      * <p>
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidatorTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidatorTest.java
index 987904a..8e887e5 100644
--- a/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidatorTest.java
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/assertion/SAML20AssertionValidatorTest.java
@@ -37,8 +37,8 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.xml.namespace.QName;
 
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
-
+import org.opensaml.core.xml.AbstractXMLObject;
+import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.saml.common.SAMLVersion;
 import org.opensaml.saml.common.assertion.AssertionValidationException;
@@ -66,6 +66,10 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.Sets;
+
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
 
 public class SAML20AssertionValidatorTest extends BaseAssertionValidationTest {
     
@@ -371,6 +375,64 @@ public class SAML20AssertionValidatorTest extends BaseAssertionValidationTest {
     }
     
     @Test
+    public void testNoConditionsWithRequired() throws AssertionValidationException {
+        getAssertion().setConditions(null);
+        
+        validator = getCurrentValidator();
+        
+        Map<String,Object> staticParams = buildBasicStaticParameters();
+        staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false);
+        staticParams.put(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS, Sets.newHashSet(MockCondition.ELEMENT_NAME));
+        
+        ValidationContext validationContext = new ValidationContext(staticParams);
+        
+        Assertion assertion = getAssertion();
+        
+        Assert.assertEquals(validator.validate(assertion, validationContext), ValidationResult.INVALID);
+    }
+    
+    @Test
+    public void testConditionsWithRequiredPresent() throws AssertionValidationException {
+        getAssertion().getConditions().getConditions().add(new MockCondition());
+        getAssertion().getConditions().getConditions().add(new MockCondition2());
+        
+        conditionValidators.add(new MockConditionValidator());
+        conditionValidators.add(new MockCondition2Validator());
+        
+        validator = getCurrentValidator();
+        
+        Map<String,Object> staticParams = buildBasicStaticParameters();
+        staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false);
+        staticParams.put(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS, Sets.newHashSet(MockCondition.ELEMENT_NAME));
+        
+        ValidationContext validationContext = new ValidationContext(staticParams);
+        
+        Assertion assertion = getAssertion();
+        
+        Assert.assertEquals(validator.validate(assertion, validationContext), ValidationResult.VALID);
+    }
+    
+    @Test
+    public void testConditionsWithRequiredMissing() throws AssertionValidationException {
+        getAssertion().getConditions().getConditions().add(new MockCondition2());
+        
+        conditionValidators.add(new MockConditionValidator());
+        conditionValidators.add(new MockCondition2Validator());
+        
+        validator = getCurrentValidator();
+        
+        Map<String,Object> staticParams = buildBasicStaticParameters();
+        staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED, false);
+        staticParams.put(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS, Sets.newHashSet(MockCondition.ELEMENT_NAME));
+        
+        ValidationContext validationContext = new ValidationContext(staticParams);
+        
+        Assertion assertion = getAssertion();
+        
+        Assert.assertEquals(validator.validate(assertion, validationContext), ValidationResult.INVALID);
+    }
+    
+    @Test
     public void testInvalidConditionsNotBefore() throws AssertionValidationException {
         getAssertion().getConditions().setNotBefore(Instant.now().plus(30, ChronoUnit.MINUTES));
         getAssertion().getConditions().setNotOnOrAfter(Instant.now().plus(60, ChronoUnit.MINUTES));
@@ -496,6 +558,73 @@ public class SAML20AssertionValidatorTest extends BaseAssertionValidationTest {
     private SAML20AssertionValidator getCurrentValidator() {
         return new SAML20AssertionValidator(conditionValidators, subjectConfirmationValidators, statementValidators, signatureTrustEngine, signaturePrevalidator);
     }
+    
+    public static class MockCondition extends AbstractXMLObject implements Condition {
+        
+        public static final QName ELEMENT_NAME = new QName("urn:test:conditions", "MockCondition", "mock");
+        
+        public MockCondition() {
+            this(ELEMENT_NAME.getNamespaceURI(), ELEMENT_NAME.getLocalPart(), ELEMENT_NAME.getPrefix());
+        }
+
+        protected MockCondition(String namespaceURI, String elementLocalName, String namespacePrefix) {
+            super(namespaceURI, elementLocalName, namespacePrefix);
+        }
+
+        /** {@inheritDoc} */
+        public List<XMLObject> getOrderedChildren() {
+            return null;
+        }
+        
+    }
+    
+    public static class MockCondition2 extends AbstractXMLObject implements Condition {
+        
+        public static final QName ELEMENT_NAME = new QName("urn:test:conditions", "MockCondition2", "mock");
+        
+        public MockCondition2() {
+            this(ELEMENT_NAME.getNamespaceURI(), ELEMENT_NAME.getLocalPart(), ELEMENT_NAME.getPrefix());
+        }
+
+        protected MockCondition2(String namespaceURI, String elementLocalName, String namespacePrefix) {
+            super(namespaceURI, elementLocalName, namespacePrefix);
+        }
+
+        /** {@inheritDoc} */
+        public List<XMLObject> getOrderedChildren() {
+            return null;
+        }
+        
+    }
+    
+    public static class MockConditionValidator implements ConditionValidator {
+        
+        /** {@inheritDoc} */
+        public QName getServicedCondition() {
+            return MockCondition.ELEMENT_NAME;
+        }
+
+        /** {@inheritDoc} */
+        public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context)
+                throws AssertionValidationException {
+            return ValidationResult.VALID;
+        }
+        
+    }
 
+    public static class MockCondition2Validator implements ConditionValidator {
+        
+        /** {@inheritDoc} */
+        public QName getServicedCondition() {
+            return MockCondition2.ELEMENT_NAME;
+        }
+
+        /** {@inheritDoc} */
+        public ValidationResult validate(Condition condition, Assertion assertion, ValidationContext context)
+                throws AssertionValidationException {
+            return ValidationResult.VALID;
+        }
+        
+    }
 
 }

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


More information about the commits mailing list