[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