[java-opensaml] 01/02: JSATTR-6: SAML AttributeQuery DataConnector
Brent Putman
putmanb at georgetown.edu
Thu May 29 23:47:57 UTC 2025
This is an automated email from the git hooks/post-receive script.
putmanb pushed a commit to branch main
in repository java-opensaml.
View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=34cf236b26d27e0ae35f6889a367c7579ccdaf5e
commit 34cf236b26d27e0ae35f6889a367c7579ccdaf5e
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Mon Mar 10 14:11:54 2025 -0400
JSATTR-6: SAML AttributeQuery DataConnector
Add variant of DefaultAssertionValidationContextBuilder and related
classes which is based on InOutOperationContext rather than
ProfileRequestContext.
Add new methods to a couple of support classes.
---
.../saml/common/binding/SAMLBindingSupport.java | 24 +
.../messaging/AssertionValidationInput.java | 80 ++
...ertionValidationNetworkInformationSupplier.java | 51 +
.../messaging/BasicNetworkInformationSupplier.java | 83 ++
...tpServletRequestNetworkInformationSupplier.java | 74 ++
.../saml2/assertion/messaging/package-info.java | 21 +
.../saml/saml2/profile/SAML2ObjectSupport.java | 60 +
.../DefaultAssertionValidationContextBuilder.java | 1239 ++++++++++++++++++++
.../assertion/messaging/impl/package-info.java | 22 +
.../saml/saml2/profile/SAML2ObjectSupportTest.java | 143 +++
10 files changed, 1797 insertions(+)
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/common/binding/SAMLBindingSupport.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/binding/SAMLBindingSupport.java
index 6cf020a08..dae771756 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/common/binding/SAMLBindingSupport.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/common/binding/SAMLBindingSupport.java
@@ -366,6 +366,30 @@ public final class SAMLBindingSupport {
return request.getRequestURL().toString();
}
+ /**
+ * Extract the transport endpoint URI at which this message was received.
+ *
+ * @param messageContext current message context
+ * @return string representing the transport endpoint URI at which the current message was received
+ * @throws MessageException thrown if the endpoint can not be looked up from the message
+ * context and converted to a string representation
+ *
+ * @since 5.2.0
+ */
+ @Nullable public static String getActualReceiverEndpointURI(@Nonnull final MessageContext messageContext)
+ throws MessageException {
+
+ final SAMLMessageReceivedEndpointContext receivedEnpointContext =
+ messageContext.getSubcontext(SAMLMessageReceivedEndpointContext.class);
+ if (receivedEnpointContext != null) {
+ final String url = receivedEnpointContext.getRequestURL();
+ if (url != null) {
+ return url;
+ }
+ }
+ return null;
+ }
+
/**
* Convert a 2-byte artifact endpoint index byte[] as typically used by SAML 2 artifact types to an integer,
* appropriate for use with {@link org.opensaml.saml.saml2.metadata.IndexedEndpoint} impls.
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationInput.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationInput.java
new file mode 100644
index 000000000..02e1a3387
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationInput.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed 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.assertion.messaging;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.saml.saml2.core.Assertion;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Class which holds data relevant to validating a SAML 2.0 Assertion.
+ */
+public class AssertionValidationInput {
+
+ /** The profile request context input. */
+ @Nonnull private InOutOperationContext operationContext;
+
+ /** The HTTP request input. */
+ @Nonnull private AssertionValidationNetworkInformationSupplier networkInformationSupplier;
+
+ /** The Assertion being evaluated. */
+ @Nonnull private Assertion assertion;
+
+ /**
+ * Constructor.
+ * @param samlAssertion the assertion being evaluated
+ * @param context the profile request context being evaluated
+ * @param networkInformation the supplier of network information
+ */
+ public AssertionValidationInput(@Nonnull final Assertion samlAssertion,
+ @Nonnull final InOutOperationContext context,
+ @Nonnull final AssertionValidationNetworkInformationSupplier networkInformation) {
+ operationContext = Constraint.isNotNull(context, "InOutOperationContext may not be null");
+ networkInformationSupplier = Constraint.isNotNull(networkInformation, "HttpServletRequest may not be null");
+ assertion = Constraint.isNotNull(samlAssertion, "Assertion may not be null");
+ }
+
+ /**
+ * Get the {@link InOutOperationContext} input.
+ *
+ * @return the message context input
+ */
+ @Nonnull public InOutOperationContext getOperationContext() {
+ return operationContext;
+ }
+
+ /**
+ * Get the {@link HttpServletRequest} input.
+ *
+ * @return the HTTP servlet request input
+ */
+ @Nonnull public AssertionValidationNetworkInformationSupplier getNetworkInformationSupplier() {
+ return networkInformationSupplier;
+ }
+
+ /**
+ * Get the {@link Assertion} being evaluated.
+ *
+ * @return the Assertion being validated
+ */
+ @Nonnull public Assertion getAssertion() {
+ return assertion;
+ }
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationNetworkInformationSupplier.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationNetworkInformationSupplier.java
new file mode 100644
index 000000000..71227d3ac
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/AssertionValidationNetworkInformationSupplier.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed 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.assertion.messaging;
+
+import java.security.cert.X509Certificate;
+
+import javax.annotation.Nullable;
+
+/**
+ * Interface for a component that supplies network-related information for assertion validation.
+ */
+public interface AssertionValidationNetworkInformationSupplier {
+
+ /**
+ * The X.509 certificate presented by the attesting party, may be null.
+ *
+ * @return the attesting party's certificate, or null
+ */
+ @Nullable X509Certificate getAttesterCertificate();
+
+ /**
+ * The attesting party's IP address, may be null.
+ *
+ * <p>
+ * For IPv6 addresses this should be in "sanitized" form, without enclosing square brackets.
+ * </p>
+ *
+ * @return the attesting party's IP address, or null if not known
+ */
+ @Nullable String getAttesterIPAddress();
+
+ /**
+ * The endpoint URI at which the assertion being validated was received, may be null.
+ *
+ * @return the endpoint URI, or null if not applicable
+ */
+ @Nullable String getReceiverEndpointURI();
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/BasicNetworkInformationSupplier.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/BasicNetworkInformationSupplier.java
new file mode 100644
index 000000000..2df1ba3eb
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/BasicNetworkInformationSupplier.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed 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.assertion.messaging;
+
+import java.security.cert.X509Certificate;
+
+import javax.annotation.Nullable;
+
+/**
+ * Basic property-based implementation of {@link AssertionValidationNetworkInformationSupplier}.
+ */
+public class BasicNetworkInformationSupplier implements AssertionValidationNetworkInformationSupplier {
+
+ /** The attesting party's certificate. */
+ @Nullable X509Certificate attesterCertificate;
+
+ /** The attesting party's IP address. */
+ @Nullable String attesterIPAddress;
+
+ /** The endpoint URI at which the assertion for validation was received. */
+ @Nullable String receiverEndpointURI;
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public X509Certificate getAttesterCertificate() {
+ return attesterCertificate;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String getAttesterIPAddress() {
+ return attesterIPAddress;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String getReceiverEndpointURI() {
+ return receiverEndpointURI;
+ }
+
+ /**
+ * Set the attesting party's certificate.
+ *
+ * @param certificate the attesting party's certificate
+ */
+ public void setAttesterCertificate(@Nullable final X509Certificate certificate) {
+ attesterCertificate = certificate;
+ }
+
+ /**
+ * Set the attesting party's IP address.
+ *
+ * @param address the attesting party's IP address
+ */
+ public void setAttesterIPAddress(@Nullable final String address) {
+ attesterIPAddress = address;
+ }
+
+ /**
+ * Set the endpoint URI at which the assertion being validated was received.
+ *
+ * @param endpointURI the endpoint URI
+ */
+ public void setReceiverEndpointURI(@Nullable final String endpointURI) {
+ receiverEndpointURI = endpointURI;
+ }
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/HttpServletRequestNetworkInformationSupplier.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/HttpServletRequestNetworkInformationSupplier.java
new file mode 100644
index 000000000..72f578dd6
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/HttpServletRequestNetworkInformationSupplier.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed 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.assertion.messaging;
+
+import java.security.cert.X509Certificate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.SecurityException;
+import org.opensaml.security.messaging.ServletRequestX509CredentialAdapter;
+import org.opensaml.security.x509.X509Credential;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.servlet.HttpServletSupport;
+
+/**
+ * Implementation of {@link AssertionValidationNetworkInformationSupplier} which wraps an
+ * instance of {@link HttpServletRequest}.
+ */
+public class HttpServletRequestNetworkInformationSupplier implements AssertionValidationNetworkInformationSupplier {
+
+ /** The wrapped servlet request. */
+ @Nonnull private HttpServletRequest servletRequest;
+
+ /**
+ * Constructor.
+ *
+ * @param request the servlet request instanc3
+ */
+ public HttpServletRequestNetworkInformationSupplier(@Nonnull final HttpServletRequest request) {
+ servletRequest = Constraint.isNotNull(request, "HttpServletRequest was null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public X509Certificate getAttesterCertificate() {
+ try {
+ final X509Credential credential = new ServletRequestX509CredentialAdapter(servletRequest);
+ return credential.getEntityCertificate();
+ } catch (final SecurityException e) {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String getAttesterIPAddress() {
+ return HttpServletSupport.getRemoteAddr(servletRequest);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String getReceiverEndpointURI() {
+ return servletRequest.getRequestURL().toString();
+ }
+
+}
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/package-info.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/package-info.java
new file mode 100644
index 000000000..36426db36
--- /dev/null
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/assertion/messaging/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed 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.
+ */
+
+/**
+ * Interfaces and API classes related to networking and messaging APIs that support validating SAML 2 Assertions.
+ */
+ at NonnullElements
+package org.opensaml.saml.saml2.assertion.messaging;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupport.java b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupport.java
index 4b09d4dfa..bedda0014 100644
--- a/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupport.java
+++ b/opensaml-saml-api/src/main/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupport.java
@@ -19,13 +19,23 @@ import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.opensaml.saml.saml2.core.BaseID;
+import org.opensaml.saml.saml2.core.EncryptedID;
import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Subject;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
/**
* A helper class for working with SAMLObjects.
*/
public final class SAML2ObjectSupport {
+ /** Logger. */
+ @Nonnull private static final Logger LOG = LoggerFactory.getLogger(SAML2ObjectSupport.class);
+
/** Constructor. */
private SAML2ObjectSupport() {
@@ -107,5 +117,55 @@ public final class SAML2ObjectSupport {
}
return Objects.equals(name1qual, name2qual);
}
+
+ /**
+ * Match a target {@link Subject} against a control instance according to the requirements specified
+ * in SAML Core 3.3.4.
+ *
+ * <p>
+ * Any {@link EncryptedID} instances which were originally present must have already been decrypted
+ * and stored in-place on the Subject. {@link BaseID} is currently unsupported. Presence of either
+ * in either target or control subject will throw {@link IllegalArgumentException}.
+ * </p>
+ *
+ * @param target the target subject to evaluate
+ * @param control the control subject against which to evaluate the target
+ *
+ * @return true if target matches the control, otherwise false
+ *
+ * @throws IllegalArgumentException if EncryptedID or BaseID is present in either Subject instance
+ */
+ public static boolean matchSubject(@Nonnull final Subject target, @Nonnull final Subject control) {
+ Constraint.isNotNull(target, "Target Subject was null");
+ Constraint.isNotNull(control, "Control Subject was null");
+
+ //TODO implement SubjectConfirmation support. Need registry of method URI -> SC matchers
+ if (!target.getSubjectConfirmations().isEmpty()) {
+ LOG.warn("Target Subject contains SubjectConfirmation, currently not supported and eval is skipped");
+ }
+
+ if (target.getEncryptedID() != null || control.getEncryptedID() != null) {
+ throw new IllegalArgumentException("Saw EncryptedID in Subject, matching not supported");
+ }
+
+ if (target.getBaseID() != null || control.getBaseID() != null) {
+ throw new IllegalArgumentException("Saw BaseID in Subject, matching not supported");
+ }
+
+ final NameID targetNameID = target.getNameID();
+ final NameID controlNameID = control.getNameID();
+
+ if (targetNameID == null && controlNameID == null) {
+ LOG.debug("Both target and control NameIDs are null, trivially match");
+ return true;
+ }
+ if (targetNameID == null || controlNameID == null) {
+ LOG.debug("One NameID is null ({}), the other is not, trivially do not match",
+ targetNameID == null ? "target" : "control");
+ return false;
+ }
+
+ return areNameIDsEquivalent(targetNameID, controlNameID);
+ }
}
\ No newline at end of file
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/DefaultAssertionValidationContextBuilder.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/DefaultAssertionValidationContextBuilder.java
new file mode 100644
index 000000000..a2a305a4b
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/DefaultAssertionValidationContextBuilder.java
@@ -0,0 +1,1239 @@
+/*
+ * Licensed 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.assertion.messaging.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.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;
+import javax.xml.namespace.QName;
+
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.messaging.MessageException;
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.messaging.context.navigate.MessageContextLookup;
+import org.opensaml.messaging.context.navigate.MessageContextLookup.Direction;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.assertion.ValidationContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.common.messaging.context.SAMLMessageInfoContext;
+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.common.messaging.context.navigate.SAMLEntityIDFunction;
+import org.opensaml.saml.common.messaging.context.navigate.SAMLMessageInfoContextIDFunction;
+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.assertion.messaging.AssertionValidationInput;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.metadata.RoleDescriptor;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.opensaml.xmlsec.SignatureValidationParameters;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.opensaml.xmlsec.signature.support.SignatureValidationParametersCriterion;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.LazySet;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+
+/**
+ * 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. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultAssertionValidationContextBuilder.class);
+
+ /** A function for resolving the clock skew to apply. */
+ @Nullable private Function<InOutOperationContext, Duration> clockSkew;
+
+ /** A function for resolving the lifetime to apply. */
+ @Nullable private Function<InOutOperationContext, Duration> lifetime;
+
+ /** A function for resolving the signature validation CriteriaSet for a particular function. */
+ @Nullable private Function<Pair<InOutOperationContext, Assertion>, CriteriaSet> signatureCriteriaSetFunction;
+
+ /** Predicate for determining whether an Assertion signature is required. */
+ @Nonnull private Predicate<InOutOperationContext> signatureRequired;
+
+ /** Predicate for determining whether an Assertion's network address(es) should be checked. */
+ @Nonnull private Predicate<InOutOperationContext> checkAddress;
+
+ /** Function for determining the max allowed time since authentication. */
+ @Nullable private Function<InOutOperationContext, Duration> maximumTimeSinceAuthn;
+
+ /** Predicate for determining whether to include the self entityID as a valid Recipient. */
+ @Nonnull private Predicate<InOutOperationContext> includeSelfEntityIDAsRecipient;
+
+ /** Function for determining additional valid audience values. */
+ @Nullable private Function<InOutOperationContext, Set<String>> additionalAudiences;
+
+ /** Function for determining additional valid Issuer values. */
+ @Nonnull private Function<InOutOperationContext, Set<String>> validIssuers;
+
+ /** Predicate for determining whether to require issuer be of the {@link NameIDType#ENTITY} format. */
+ @Nonnull private Predicate<InOutOperationContext> requireEntityIssuer;
+
+ /** Function for determining the valid InResponseTo value. */
+ @Nullable private Function<InOutOperationContext, String> inResponseTo;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData InResponseTo is ignored. */
+ @Nonnull private Predicate<InOutOperationContext> inResponseToIgnored;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData InResponseTo is required. */
+ @Nonnull private Predicate<InOutOperationContext> inResponseToRequired;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData Recipient is required. */
+ @Nonnull private Predicate<InOutOperationContext> recipientRequired;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData NotBefore is required. */
+ @Nonnull private Predicate<InOutOperationContext> notBeforeRequired;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData NotOnOrAfter is required. */
+ @Nonnull private Predicate<InOutOperationContext> notOnOrAfterRequired;
+
+ /** Predicate for determining whether an Assertion SubjectConfirmationData Address is required. */
+ @Nonnull private Predicate<InOutOperationContext> addressRequired;
+
+ /** The set of required Conditions. */
+ @Nonnull private Set<QName> requiredConditions;
+
+ /** Resolver for security parameters context. */
+ private Function<InOutOperationContext, SecurityParametersContext> securityParametersLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultAssertionValidationContextBuilder() {
+ signatureRequired = PredicateSupport.alwaysTrue();
+ includeSelfEntityIDAsRecipient = PredicateSupport.alwaysFalse();
+ checkAddress = PredicateSupport.alwaysTrue();
+ inResponseTo = new DefaultValidInResponseToLookupFunction();
+ inResponseToIgnored = PredicateSupport.alwaysFalse();
+ inResponseToRequired = PredicateSupport.alwaysFalse();
+ recipientRequired = PredicateSupport.alwaysFalse();
+ notOnOrAfterRequired = PredicateSupport.alwaysFalse();
+ notBeforeRequired = PredicateSupport.alwaysFalse();
+ addressRequired = PredicateSupport.alwaysFalse();
+ requiredConditions = CollectionSupport.emptySet();
+ validIssuers = new DefaultValidIssuersLookupFunction();
+ requireEntityIssuer = PredicateSupport.alwaysFalse();
+
+ securityParametersLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class)
+ .compose(new ContextDataLookupFunction<InOutOperationContext, MessageContext>() {
+ public MessageContext apply(@Nullable InOutOperationContext t) {
+ return t != null ? t.getInboundMessageContext() : null;
+ }
+ });
+ }
+
+ /**
+ * Get the strategy by which to resolve the clock skew.
+ *
+ * @return lookup strategy
+ *
+ * @since 4.1.0
+ */
+ @Nullable public Function<InOutOperationContext, Duration> getClockSkew() {
+ return clockSkew;
+ }
+
+ /**
+ * Set the clock skew.
+ *
+ * @param skew clock skew
+ *
+ * @since 4.1.0
+ */
+ public void setClockSkew(@Nullable final Duration skew) {
+ clockSkew = FunctionSupport.constant(skew);
+ }
+
+ /**
+ * Set the strategy by which to resolve the clock skew.
+ *
+ * @param strategy lookup strategy
+ *
+ * @since 4.1.0
+ */
+ public void setClockSkewLookupStrategy(@Nullable final Function<InOutOperationContext, Duration> strategy) {
+ clockSkew = strategy;
+ }
+
+ /**
+ * Get the strategy by which to resolve the lifetime.
+ *
+ * @return lookup strategy
+ *
+ * @since 4.2.0
+ */
+ @Nullable public Function<InOutOperationContext, Duration> getLifetime() {
+ return lifetime;
+ }
+
+ /**
+ * Set the lifetime.
+ *
+ * @param duration lifetime
+ *
+ * @since 4.2.0
+ */
+ public void setLifetime(@Nullable final Duration duration) {
+ lifetime = FunctionSupport.constant(duration);
+ }
+
+ /**
+ * Set the strategy by which to resolve the lifetime.
+ *
+ * @param strategy lookup strategy
+ *
+ * @since 4.2.0
+ */
+ public void setLifetimeLookupStrategy(@Nullable final Function<InOutOperationContext, Duration> strategy) {
+ lifetime = strategy;
+ }
+
+ /**
+ * Get the strategy by which to resolve a {@link SecurityParametersContext}.
+ *
+ * @return the lookup strategy
+ */
+ @Nonnull public Function<InOutOperationContext, 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<InOutOperationContext, SecurityParametersContext> strategy) {
+ securityParametersLookupStrategy =
+ Constraint.isNotNull(strategy, "SecurityParametersContext lookup strategy was null") ;
+ }
+
+ /**
+ * 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(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get();
+ } else {
+ requiredConditions = CollectionSupport.emptySet();
+ }
+ }
+
+ /**
+ * Get the predicate which determines whether to include the self entityID as a valid Recipient.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getIncludeSelfEntityIDAsRecipient() {
+ return includeSelfEntityIDAsRecipient;
+ }
+
+ /**
+ * Set the predicate which determines whether to include the self entityID as a valid Recipient.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setIncludeSelfEntityIDAsRecipient(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ includeSelfEntityIDAsRecipient = Constraint.isNotNull(predicate, "Signature required predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion signature is required.
+ *
+ * <p>
+ * Defaults to an always true predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> 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(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ signatureRequired = Constraint.isNotNull(predicate, "Signature required predicate was null");
+ }
+
+ /**
+ * Set the function for determining the valid InResponseTo.
+ *
+ * <p>
+ * Defaults to null.
+ * </p>
+ *
+ * @param function the function, may be null
+ */
+ public void setInResponseTo(final @Nullable Function<InOutOperationContext,String> function) {
+ inResponseTo = function;
+ }
+
+ /**
+ * Get the function for determining the valid InResponseTo.
+ *
+ * <p>
+ * Defaults to null.
+ * </p>
+ *
+ * @return the function
+ */
+ @Nullable public Function<InOutOperationContext,String> getInResponseTo() {
+ return inResponseTo;
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData InResponseTo is required.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getInResponseToRequired() {
+ return inResponseToRequired;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData InResponseTo is required.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setInResponseToRequired(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ inResponseToRequired = Constraint.isNotNull(predicate, "InResponseTo required predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData InResponseTo is ignored.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ *
+ * @since 5.2.0
+ */
+ @Nonnull public Predicate<InOutOperationContext> getInResponseToIgnored() {
+ return inResponseToIgnored;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData InResponseTo is ignored.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ *
+ * @since 5.2.0
+ */
+ public void setInResponseToIgnored(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ inResponseToIgnored = Constraint.isNotNull(predicate, "InResponseTo ignored predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData Recipient is required.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getRecipientRequired() {
+ return recipientRequired;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData Recipient is required.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setRecipientRequired(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ recipientRequired = Constraint.isNotNull(predicate, "Recipient required predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData NotBefore is required.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getNotBeforeRequired() {
+ return notBeforeRequired;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData NotBefore is required.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setNotBeforeRequired(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ notBeforeRequired = Constraint.isNotNull(predicate, "NotBefore required predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData NotOnOrAfter is required.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getNotOnOrAfterRequired() {
+ return notOnOrAfterRequired;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData NotOnOrAfter is required.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setNotOnOrAfterRequired(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ notOnOrAfterRequired = Constraint.isNotNull(predicate, "NotOnOrAfter required predicate was null");
+ }
+
+ /**
+ * Get the predicate which determines whether an Assertion SubjectConfirmationData Address is required.
+ *
+ * <p>
+ * Defaults to an always false predicate;
+ * </p>
+ *
+ * @return the predicate
+ */
+ @Nonnull public Predicate<InOutOperationContext> getAddressRequired() {
+ return addressRequired;
+ }
+
+ /**
+ * Set the predicate which determines whether an Assertion SubjectConfirmationData Address is required.
+ *
+ * <p>
+ * Defaults to an always false predicate.
+ * </p>
+ *
+ * @param predicate the predicate, must be non-null
+ */
+ public void setAddressRequired(final @Nonnull Predicate<InOutOperationContext> predicate) {
+ addressRequired = Constraint.isNotNull(predicate, "Address 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
+ */
+ @Nonnull public Predicate<InOutOperationContext> 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(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ checkAddress = Constraint.isNotNull(predicate, "Check address predicate was null");
+ }
+
+ /**
+ * Get the function for determining additional audience values.
+ *
+ * <p>
+ * Defaults to null.
+ * </p>
+ *
+ * @return the function
+ */
+ @Nullable public Function<InOutOperationContext,Set<String>> getAdditionalAudiences() {
+ return additionalAudiences;
+ }
+
+ /**
+ * Set the function for determining additional audience values.
+ *
+ * <p>
+ * Defaults to null.
+ * </p>
+ *
+ * @param function the function, may be null
+ */
+ public void setAdditionalAudiences(@Nullable final Function<InOutOperationContext,Set<String>> function) {
+ additionalAudiences = function;
+ }
+
+ /**
+ * Get the function for determining the valid Issuer values
+ *
+ * <p>
+ * Defaults to an implementation which resolves the outbound SAML peer entityID.
+ * </p>
+ *
+ * @return the function
+ */
+ @Nonnull public Function<InOutOperationContext,Set<String>> getValidIssuers() {
+ return validIssuers;
+ }
+
+ /**
+ * Set the function for determining the valid Issuer values
+ *
+ * <p>
+ * Defaults to an implementation which resolves the outbound SAML peer entityID.
+ * </p>
+ *
+ * @param function the function, may be null
+ */
+ public void setValidIssuers(@Nonnull final Function<InOutOperationContext,Set<String>> function) {
+ validIssuers = Constraint.isNotNull(function, "Valied Issuers function was null");
+ }
+
+ /**
+ * Get the predicate which determines whether to require the Issuer contain the {@link NameIDType#ENTITY} Format.
+ *
+ * @return predicate
+ *
+ * @since 5.2.0
+ */
+ @Nonnull public Predicate<InOutOperationContext> getRequireEntityIssuer() {
+ return requireEntityIssuer;
+ }
+
+ /**
+ * Get the predicate which determines whether to require the Issuer contain the {@link NameIDType#ENTITY} Format.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param predicate the condition to set
+ *
+ * @since 5.2.0
+ */
+ public void setRequireEntityIssuer(@Nonnull final Predicate<InOutOperationContext> predicate) {
+ requireEntityIssuer = Constraint.isNotNull(predicate, "Entity issuer predicate was null");
+ }
+
+ /**
+ * Get the function for determining the max allowed time since authentication.
+ *
+ * <p>
+ * Defaults to null.
+ * </p>
+ *
+ * @return the function
+ */
+ @Nullable public Function<InOutOperationContext,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(@Nullable final Function<InOutOperationContext,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<InOutOperationContext, 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<InOutOperationContext, 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 TreeMap<String, Object> staticParams = new TreeMap<>();
+
+ // Clock skew
+ final var skewFunc = getClockSkew();
+ if (skewFunc != null) {
+ staticParams.put(SAML2AssertionValidationParameters.CLOCK_SKEW,
+ skewFunc.apply(input.getOperationContext()));
+ }
+
+ // Lifetime (for IssueInstant)
+ final var lifetimeFunc = getLifetime();
+ if (lifetimeFunc != null) {
+ staticParams.put(SAML2AssertionValidationParameters.LIFETIME,
+ lifetimeFunc.apply(input.getOperationContext()));
+ }
+
+ // Issuer
+ staticParams.put(SAML2AssertionValidationParameters.VALID_ISSUERS,
+ getValidIssuers().apply(input.getOperationContext()));
+
+ staticParams.put(SAML2AssertionValidationParameters.REQUIRE_ENTITY_ISSUER,
+ Boolean.valueOf(getRequireEntityIssuer().test(input.getOperationContext())));
+
+ // Signature
+ populateSignatureParameters(staticParams, input);
+
+ // Conditions
+ populateConditionsParameters(staticParams, input);
+
+ final Set<InetAddress> validAddresses = getValidAddresses(input);
+ final Boolean checkAddressEnabled = Boolean.valueOf(getCheckAddress().test(input.getOperationContext()));
+ assert checkAddressEnabled != null;
+
+ // SubjectConfirmation
+ populateSubjectConfirmationParameters(staticParams, input, validAddresses, checkAddressEnabled);
+
+ // Statements
+ populateStatementParams(staticParams, input, validAddresses, checkAddressEnabled);
+
+ log.trace("Built static parameters map: {}", staticParams);
+
+ return staticParams;
+ }
+
+
+ /**
+ * Populate the static signature parameters.
+ * @param staticParams the parameters being populated
+ * @param input validation input
+ */
+ private void populateSignatureParameters(@Nonnull final Map<String, Object> staticParams,
+ @Nonnull final AssertionValidationInput input) {
+
+ staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_REQUIRED,
+ Boolean.valueOf(getSignatureRequired().test(input.getOperationContext())));
+ staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_VALIDATION_CRITERIA_SET,
+ getSignatureCriteriaSet(input));
+
+ final SecurityParametersContext securityParameters = getSecurityParametersLookupStrategy()
+ .apply(input.getOperationContext());
+
+ final SignatureValidationParameters valParams = securityParameters != null
+ ? securityParameters.getSignatureValidationParameters() : null;
+ if (valParams != null) {
+ staticParams.put(SAML2AssertionValidationParameters.SIGNATURE_VALIDATION_TRUST_ENGINE,
+ valParams.getSignatureTrustEngine());
+ }
+ }
+
+ /**
+ * Populate the static Conditions parameters.
+ * @param staticParams the parameters being populated
+ * @param input validation input
+ */
+ private void populateConditionsParameters(@Nonnull final Map<String, Object> staticParams,
+ @Nonnull final AssertionValidationInput input) {
+
+ // For general Conditions
+ staticParams.put(SAML2AssertionValidationParameters.COND_REQUIRED_CONDITIONS, getRequiredConditions(input));
+
+ // For Audience Condition
+ staticParams.put(SAML2AssertionValidationParameters.COND_VALID_AUDIENCES, getValidAudiences(input));
+ }
+
+ /**
+ * Populate the static SubjectConfirmation parameters.
+ *
+ * @param staticParams the parameters being populated
+ * @param input validation input
+ * @param validAddresses the valid addresses
+ * @param checkAddressEnabled whether address checking is enabled
+ */
+ private void populateSubjectConfirmationParameters(@Nonnull final Map<String, Object> staticParams,
+ @Nonnull final AssertionValidationInput input, @Nonnull final Set<InetAddress> validAddresses,
+ @Nonnull final Boolean checkAddressEnabled) {
+
+ // 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);
+ }
+
+ // For SubjectConfirmationData
+ staticParams.put(SAML2AssertionValidationParameters.SC_RECIPIENT_REQUIRED,
+ Boolean.valueOf(getRecipientRequired().test(input.getOperationContext())));
+ staticParams.put(SAML2AssertionValidationParameters.SC_VALID_RECIPIENTS, getValidRecipients(input));
+
+ staticParams.put(SAML2AssertionValidationParameters.SC_ADDRESS_REQUIRED,
+ Boolean.valueOf(getAddressRequired().test(input.getOperationContext())));
+ staticParams.put(SAML2AssertionValidationParameters.SC_VALID_ADDRESSES, validAddresses);
+ staticParams.put(SAML2AssertionValidationParameters.SC_CHECK_ADDRESS, checkAddressEnabled);
+
+ staticParams.put(SAML2AssertionValidationParameters.SC_IN_RESPONSE_TO_IGNORED,
+ Boolean.valueOf(getInResponseToIgnored().test(input.getOperationContext())));
+ staticParams.put(SAML2AssertionValidationParameters.SC_IN_RESPONSE_TO_REQUIRED,
+ Boolean.valueOf(getInResponseToRequired().test(input.getOperationContext())));
+
+ final var irtFunc = getInResponseTo();
+ if (irtFunc != null) {
+ staticParams.put(SAML2AssertionValidationParameters.SC_VALID_IN_RESPONSE_TO,
+ irtFunc.apply(input.getOperationContext()));
+ }
+
+ staticParams.put(SAML2AssertionValidationParameters.SC_NOT_BEFORE_REQUIRED,
+ Boolean.valueOf(getNotBeforeRequired().test(input.getOperationContext())));
+ staticParams.put(SAML2AssertionValidationParameters.SC_NOT_ON_OR_AFTER_REQUIRED,
+ Boolean.valueOf(getNotOnOrAfterRequired().test(input.getOperationContext())));
+ }
+
+ /**
+ * Populate the static Statement params.
+ * @param staticParams the parameters being populated
+ * @param input validation input
+ * @param validAddresses the valid addresses
+ * @param checkAddressEnabled whether address checking is enabled
+ */
+ private void populateStatementParams(@Nonnull final Map<String, Object> staticParams,
+ @Nonnull final AssertionValidationInput input, @Nonnull final Set<InetAddress> validAddresses,
+ @Nonnull final Boolean checkAddressEnabled) {
+
+ // For AuthnStatement
+ staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_VALID_ADDRESSES, validAddresses);
+ staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_CHECK_ADDRESS, checkAddressEnabled);
+
+ final var maxTimeFunc = getMaximumTimeSinceAuthn();
+ if (maxTimeFunc != null) {
+ staticParams.put(SAML2AssertionValidationParameters.STMT_AUTHN_MAX_TIME,
+ maxTimeFunc.apply(input.getOperationContext()));
+ }
+ }
+
+ /**
+ * 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>
+ * 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();
+
+ final var sigCritFunc = getSignatureCriteriaSetFunction();
+ if (sigCritFunc != null) {
+ final CriteriaSet dynamicCriteria = sigCritFunc.apply(
+ new Pair<>(input.getOperationContext(), input.getAssertion()));
+ if (dynamicCriteria != null) {
+ criteriaSet.addAll(dynamicCriteria);
+ }
+ }
+
+ if (!criteriaSet.contains(EntityIdCriterion.class)) {
+ final Issuer issuerObj = input.getAssertion().getIssuer();
+ if (issuerObj != null) {
+ final String issuer = StringSupport.trimOrNull(issuerObj.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.getOperationContext().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
+ */
+ // Checkstyle: CyclomaticComplexity OFF
+ 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) {
+ final RoleDescriptor role = metadataContext.getRoleDescriptor();
+ if (role != null) {
+ criteriaSet.add(new RoleDescriptorCriterion(role));
+ }
+ }
+ }
+ 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) {
+ final String protocol = protocolContext.getProtocol();
+ if (protocol != null) {
+ criteriaSet.add(new ProtocolCriterion(protocol));
+ }
+ }
+
+ if (!criteriaSet.contains(SignatureValidationParametersCriterion.class)) {
+ final SecurityParametersContext secParamsContext =
+ inboundContext.getSubcontext(SecurityParametersContext.class);
+ if (secParamsContext != null) {
+ final SignatureValidationParameters valParams = secParamsContext.getSignatureValidationParameters();
+ if (valParams != null) {
+ criteriaSet.add(new SignatureValidationParametersCriterion(valParams));
+ }
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Get the attesting entity's {@link X509Certificate}.
+ *
+ * <p>
+ * This implementation returns the client TLS certificate present in the
+ * {@link jakarta.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) {
+ final X509Certificate cert = input.getNetworkInformationSupplier().getAttesterCertificate();
+ if (cert != null) {
+ return cert;
+ }
+
+ 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;
+ * </p>
+ * <ol>
+ * <li>
+ * the result of evaluating
+ * {@link SAMLBindingSupport#getActualReceiverEndpointURI(MessageContext, HttpServletRequest)}
+ * </li>
+ * <li>
+ * if enabled via the eval of {@link #getIncludeSelfEntityIDAsRecipient()}, the value from evaluating
+ * {@link #getSelfEntityID(AssertionValidationInput)} if non-null
+ *
+ * </li>
+ * </ol>
+ *
+ * @param input the assertion validation input
+ *
+ * @return set of recipient endpoint URI's
+ */
+ @Nonnull @Unmodifiable @NotLive protected Set<String> getValidRecipients(
+ @Nonnull final AssertionValidationInput input) {
+ final LazySet<String> validRecipients = new LazySet<>();
+
+ try {
+ final String contextEndpoint = SAMLBindingSupport.getActualReceiverEndpointURI(
+ input.getOperationContext().ensureInboundMessageContext());
+ final String endpoint = contextEndpoint != null ? contextEndpoint
+ : input.getNetworkInformationSupplier().getReceiverEndpointURI();
+ if (endpoint != null) {
+ validRecipients.add(endpoint);
+ }
+ } catch (final MessageException e) {
+ log.warn("Attempt to resolve recipient endpoint failed", e);
+ }
+
+ if (getIncludeSelfEntityIDAsRecipient().test(input.getOperationContext())) {
+ 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 @Unmodifiable @NotLive 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 CollectionSupport.emptySet();
+ } catch (final UnknownHostException e) {
+ log.warn("Processing of attester IP address failed. Validation of Assertion may or may not succeed", e);
+ return CollectionSupport.emptySet();
+ }
+ }
+
+ /**
+ * Get the attester's IP address.
+ *
+ * <p>
+ * This implementation returns the value of {@link jakarta.servlet.http.HttpServletRequest#getRemoteAddr()}.
+ * </p>
+ *
+ * @param input the assertion validation input
+ *
+ * @return the IP address of the attester
+ */
+ @Nullable protected String getAttesterIPAddress(@Nonnull final AssertionValidationInput input) {
+ return input.getNetworkInformationSupplier().getAttesterIPAddress();
+ }
+
+ /**
+ * Get the valid audiences for attestation.
+ *
+ * <p>
+ * This implementation returns a set containing the union of:
+ * </p>
+ * <ol>
+ * <li>the result of {@link #getSelfEntityID(AssertionValidationInput)}, if non-null</li>
+ * <li>the result of evaluating {@link #getAdditionalAudiences()}, if non-null</li>
+ * </ol>
+ *
+ * @param input the assertion validation input
+ *
+ * @return set of audience URI's
+ */
+ @Nonnull @Unmodifiable @NotLive 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);
+ }
+
+ final var audFunc = getAdditionalAudiences();
+ if (audFunc != null) {
+ final Set<String> additional = audFunc.apply(input.getOperationContext());
+ if (additional != null) {
+ validAudiences.addAll(additional);
+ }
+ }
+
+ 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.getOperationContext()
+ .ensureInboundMessageContext()
+ .getSubcontext(SAMLSelfEntityContext.class);
+
+ if (selfContext != null) {
+ return selfContext.getEntityId();
+ }
+
+ return null;
+ }
+
+ /** Default strategy for resolving the valid InResponseTo value. */
+ public static class DefaultValidInResponseToLookupFunction implements Function<InOutOperationContext, String> {
+
+ /** The lookup delegate. */
+ @Nonnull private Function<MessageContext, String> delegate;
+
+ /** Constructor. */
+ public DefaultValidInResponseToLookupFunction() {
+ delegate = new SAMLMessageInfoContextIDFunction().compose(
+ new ChildContextLookup<>(SAMLMessageInfoContext.class, true).compose(
+ new MessageContextLookup<>(Direction.OUTBOUND)));
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public String apply(@Nullable final InOutOperationContext prc) {
+ if (prc == null || prc.getInboundMessageContext() == null) {
+ return null;
+ }
+
+ //Note: Doesn't matter whether we apply to inbound or outbound
+ return delegate.apply(prc.getInboundMessageContext());
+ }
+
+ }
+
+ /**
+ * Default strategy for resolving the valid Issuers.
+ *
+ * <p>
+ * Resolves the entityID from the {@link SAMLPeerEntityContext} child of the outbound {@link MessageContext}.
+ * </p>
+ * */
+ public static class DefaultValidIssuersLookupFunction implements Function<InOutOperationContext, Set<String>> {
+
+ /** The lookup delegate. */
+ @Nonnull private Function<MessageContext, String> delegate;
+
+ /** Constructor. */
+ public DefaultValidIssuersLookupFunction() {
+ delegate = new SAMLEntityIDFunction().compose(
+ new ChildContextLookup<>(SAMLPeerEntityContext.class).compose(
+ new MessageContextLookup<>(Direction.OUTBOUND)));
+ }
+
+ /** {@inheritDoc} */
+ @Nullable @Unmodifiable @NotLive public Set<String> apply(@Nullable final InOutOperationContext prc) {
+ if (prc == null || prc.getInboundMessageContext() == null) {
+ return null;
+ }
+
+ // Note: Doesn't matter whether we apply to inbound or outbound
+ final String entityID = delegate.apply(prc.getInboundMessageContext());
+ if (entityID != null) {
+ return CollectionSupport.singleton(entityID);
+ }
+ return CollectionSupport.emptySet();
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/package-info.java b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/package-info.java
new file mode 100644
index 000000000..8cf3bdc3d
--- /dev/null
+++ b/opensaml-saml-impl/src/main/java/org/opensaml/saml/saml2/assertion/messaging/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed 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.
+ */
+
+/**
+ * Implementation classes related to networking and messaging APIs that support validating SAML 2 Assertions.
+
+ */
+ at NonnullElements
+package org.opensaml.saml.saml2.assertion.messaging.impl;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupportTest.java b/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupportTest.java
new file mode 100644
index 000000000..6dee4b8e9
--- /dev/null
+++ b/opensaml-saml-impl/src/test/java/org/opensaml/saml/saml2/profile/SAML2ObjectSupportTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed 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;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.testing.XMLObjectBaseTestCase;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.saml.saml2.core.EncryptedID;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.core.tests.MockBaseID;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ *
+ */
+public class SAML2ObjectSupportTest extends XMLObjectBaseTestCase {
+
+ @DataProvider
+ public Object[][] subjects () {
+ return new Object[][] {
+ new Object[] {
+ "luke", NameID.UNSPECIFIED, null, null,
+ "luke", NameID.UNSPECIFIED, null, null,
+ Boolean.TRUE},
+ new Object[] {
+ "luke", null, null, null,
+ "luke", NameID.UNSPECIFIED, null, null,
+ Boolean.TRUE},
+ new Object[] {
+ "luke", NameID.UNSPECIFIED, null, null,
+ "luke", null, null, null,
+ Boolean.TRUE},
+ new Object[] {
+ "luke", null, null, null,
+ "luke", null, null, null,
+ Boolean.TRUE},
+ new Object[] {
+ "luke", null, "QualFoo", "SPQualFoo",
+ "luke", null, "QualFoo", "SPQualFoo",
+ Boolean.TRUE},
+
+ new Object[] {
+ "luke", null, null, null,
+ "han", null, null, null,
+ Boolean.FALSE},
+ new Object[] {
+ "luke", "FormatFoo", null, null,
+ "luke", "FormatBar", null, null,
+ Boolean.FALSE},
+ new Object[] {
+ "luke", null, "QualFoo", null,
+ "luke", null, "QualBar", null,
+ Boolean.FALSE},
+ new Object[] {
+ "luke", null, null, "SPQualFoo",
+ "luke", null, null, "SPQualBar",
+ Boolean.FALSE},
+ };
+ }
+
+ @Test(dataProvider="subjects")
+ public void matchSubject(String targetValue, String targetFormat, String targetNameQualifer, String targetSPNameQualifer,
+ String controlValue, String controlFormat, String controlNameQualifer, String controlSPNameQualifer,
+ Boolean matches) {
+
+ Assert.assertEquals(SAML2ObjectSupport.matchSubject(
+ buildSubject(targetValue, targetFormat, targetNameQualifer, targetSPNameQualifer),
+ buildSubject(controlValue, controlFormat, controlNameQualifer, controlSPNameQualifer)),
+ matches);
+ }
+
+ @Test
+ public void matchSubjectSpecialCases() {
+ Subject subj1 = buildSubject("luke", null, null, null);
+ Subject subj2 = buildSubject("luke", null, null, null);
+
+ Assert.assertEquals(SAML2ObjectSupport.matchSubject(subj1, subj2), Boolean.TRUE);
+
+ subj1.setNameID(null);
+ subj2 = buildSubject("luke", null, null, null);
+ Assert.assertEquals(SAML2ObjectSupport.matchSubject(subj1, subj2), Boolean.FALSE);
+
+ subj1 = buildSubject("luke", null, null, null);
+ subj2.setNameID(null);
+ Assert.assertEquals(SAML2ObjectSupport.matchSubject(subj1, subj2), Boolean.FALSE);
+
+ subj1.setNameID(null);
+ subj2.setNameID(null);
+ Assert.assertEquals(SAML2ObjectSupport.matchSubject(subj1, subj2), Boolean.TRUE);
+
+ subj2 = buildSubject("luke", null, null, null);
+
+ subj1 = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+ subj1.setEncryptedID((EncryptedID) XMLObjectSupport.buildXMLObject(EncryptedID.DEFAULT_ELEMENT_NAME));
+ try {
+ SAML2ObjectSupport.matchSubject(subj1, subj2);
+ Assert.fail("Subject match did not fail on presence of EncryptedID");
+ } catch (IllegalArgumentException e) {
+ //expected
+ }
+
+ subj1 = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+ subj1.setBaseID(new MockBaseID());
+ try {
+ SAML2ObjectSupport.matchSubject(subj1, subj2);
+ Assert.fail("Subject match did not fail on presence of BaseID");
+ } catch (IllegalArgumentException e) {
+ //expected
+ }
+
+ }
+
+ @Nonnull
+ private Subject buildSubject(String value, String format, String nameQualifer, String spNameQualifer) {
+
+ final NameID nameID = (NameID) XMLObjectSupport.buildXMLObject(NameID.DEFAULT_ELEMENT_NAME);
+ nameID.setValue(value);
+ nameID.setFormat(format);
+ nameID.setNameQualifier(nameQualifer);
+ nameID.setSPNameQualifier(spNameQualifer);
+
+ final Subject subject = (Subject) XMLObjectSupport.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME);
+ subject.setNameID(nameID);
+ return subject;
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list