[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