[java-plugin-shibd-saml] branch dev/StateMgmtWIP updated: Check in ACR validation changes, testing TBD
Codeberg
noreply at shibboleth.net
Mon May 4 16:09:45 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/StateMgmtWIP
in repository java-plugin-shibd-saml.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/6f7c358f92e2124430897d4745061d11145b5d13
The following commit(s) were added to refs/heads/dev/StateMgmtWIP by this push:
new 6f7c358 Check in ACR validation changes, testing TBD
6f7c358 is described below
commit 6f7c358f92e2124430897d4745061d11145b5d13
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon May 4 12:09:27 2026 -0400
Check in ACR validation changes, testing TBD
---
.../shibboleth/sp/saml/saml2/SAMLStateData.java | 34 +-
.../config/BrowserSSOProfileConfiguration.java | 19 ++
.../idp/flows/sp/consumer/saml2/saml2-beans.xml | 2 +-
sp-saml-impl/pom.xml | 5 +
.../impl/BrowserSSOProfileConfiguration.java | 33 +-
.../saml/saml2/profile/impl/AddAuthnRequest.java | 11 +-
.../impl/ProcessAssertionsForAuthentication.java | 351 +++++++++++++++++++++
7 files changed, 449 insertions(+), 6 deletions(-)
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
index 09a351d..192db7f 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/SAMLStateData.java
@@ -19,9 +19,12 @@ import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;
+import net.shibboleth.shared.primitive.StringSupport;
import net.shibboleth.sp.state.StateData;
/**
@@ -31,6 +34,9 @@ public class SAMLStateData extends StateData {
/** SAML request ID to track. */
@Nullable String requestID;
+
+ /** SAML {@link RequestedAuthnContext} comparison operator. */
+ @Nullable String authnContextOperator;
/**
* Get the identifier of the request message.
@@ -53,11 +59,33 @@ public class SAMLStateData extends StateData {
requestID = id;
return this;
}
+
+ /**
+ * Get the {@link RequestedAuthnContext} comparison operator used in the request.
+ *
+ * @return comparison operator
+ */
+ @JsonProperty("operator")
+ @Nullable public String getAuthnContextOperator() {
+ return authnContextOperator;
+ }
+
+ /**
+ * Set the {@link RequestedAuthnContext} comparison operator used in the request.
+ *
+ * @param op comparison operator
+ *
+ * @return the updated object
+ */
+ public SAMLStateData setAuthnContextOperator(@Nullable final String op) {
+ authnContextOperator = StringSupport.trimOrNull(op);
+ return this;
+ }
/** {@inheritDoc} */
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), requestID);
+ return Objects.hash(super.hashCode(), requestID, authnContextOperator);
}
/** {@inheritDoc} */
@@ -68,7 +96,8 @@ public class SAMLStateData extends StateData {
}
final SAMLStateData other = (SAMLStateData) obj;
- return Objects.equals(requestID, other.requestID);
+ return Objects.equals(requestID, other.requestID)
+ && Objects.equals(authnContextOperator, other.authnContextOperator);
}
/** {@inheritDoc} */
@@ -76,6 +105,7 @@ public class SAMLStateData extends StateData {
public String toString() {
return MoreObjects.toStringHelper(this)
.add("requestID", requestID)
+ .add("authnContextOperator", authnContextOperator)
.toString();
}
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
index d1f7a47..a0c9bf2 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
@@ -27,12 +27,14 @@ import net.shibboleth.shared.annotation.ConfigurationSetting;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.sp.state.StateManager;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
/** Configuration support for SP SAML 2.0 Browser SSO. */
@@ -90,6 +92,23 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
@ConfigurationSetting(name="authnContextClassRefs")
@Nonnull @NonnullElements @NotLive @Unmodifiable List<String> getAuthnContextClassRefs(
@Nullable final ProfileRequestContext profileRequestContext);
+
+ /**
+ * Get whether to validate the incoming assertions' {@link AuthnContextClassRef} against
+ * any {@link RequestedAuthnContext} included in the original request.
+ *
+ * <p>This leverages both the Hub's {@link StateManager} to recover the requested values
+ * and the IdP's existing machibery for evaluating the information in the case of inexact
+ * comparison operators.</p>
+ *
+ * <p>Defaults to true.</p>
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return whether to cross check the resulting ACRs
+ */
+ @ConfigurationSetting(name="validateAuthnContextClassRefs")
+ boolean isValidateAuthnContextClassRefs(@Nullable final ProfileRequestContext profileRequestContext);
/**
* Get the name identifier format to require via the SAML request.
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
index ee4e6bf..bff40be 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
@@ -228,7 +228,7 @@
<bean id="SAMLStatementConsumer" class="net.shibboleth.sp.saml.saml2.profile.impl.SAMLTokenContextConsumer" />
<bean id="ProcessAssertionsForAuthentication"
- class="net.shibboleth.idp.saml.saml2.profile.impl.ProcessAssertionsForAuthentication" scope="prototype"
+ class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessAssertionsForAuthentication" scope="prototype"
p:sAMLConsumer-ref="SAMLStatementConsumer">
<property name="responseResolver">
<bean parent="shibboleth.Functions.Compose">
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 5af36b1..57d9d4c 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -44,6 +44,11 @@
<artifactId>idp-profile-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-saml-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>${shib-profile.groupId}</groupId>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
index 061b0b6..2383005 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
@@ -41,13 +41,15 @@ import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.saml2.core.AuthnContextClassRef;
import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml.saml2.core.SubjectConfirmationData;
import org.opensaml.saml.saml2.core.SubjectLocality;
import org.opensaml.saml.saml2.metadata.RequestedAttribute;
/** Configuration support for SP SAML 2.0 Browser SSO. */
public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsumerProfileConfiguration
- implements SAMLArtifactConsumerProfileConfiguration, net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
+ implements SAMLArtifactConsumerProfileConfiguration,
+ net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
/** Whether attributes should be resolved in the course of the profile. */
@Nonnull private Predicate<ProfileRequestContext> resolveAttributesPredicate;
@@ -88,6 +90,9 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
/** Lookup function to supply default authentication methods. */
@Nonnull private Function<ProfileRequestContext,Collection<String>> authnContextClassRefLookupStrategy;
+ /** Whether to validate incoming ACRs. */
+ @Nonnull private Predicate<ProfileRequestContext> validateAuthnContextClassRefsPredicate;
+
/** Lookup function to supply NameID format. */
@Nonnull private Function<ProfileRequestContext,String> nameIDFormatLookupStrategy;
@@ -139,6 +144,7 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
proxyCountLookupStrategy = FunctionSupport.constant(null);
authnContextComparisonLookupStrategy = FunctionSupport.constant(null);
authnContextClassRefLookupStrategy = FunctionSupport.constant(null);
+ validateAuthnContextClassRefsPredicate = PredicateSupport.alwaysTrue();
nameIDFormatLookupStrategy = FunctionSupport.constant(null);
nameQualifierLookupStrategy = FunctionSupport.constant(null);
attributeIndexLookupStrategy = FunctionSupport.constant(null);
@@ -513,6 +519,31 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
@Nonnull final Function<ProfileRequestContext,Collection<String>> strategy) {
authnContextClassRefLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /** {@inheritDoc} */
+ public boolean isValidateAuthnContextClassRefs(@Nullable final ProfileRequestContext profileRequestContext) {
+ return validateAuthnContextClassRefsPredicate.test(profileRequestContext);
+ }
+
+ /**
+ * Set whether to validate the incoming assertions' {@link AuthnContextClassRef} against any
+ * {@link RequestedAuthnContext} included in the original request.
+ *
+ * @param flag flag to set
+ */
+ public void setValidateAuthnContextClassRefs(final boolean flag) {
+ validateAuthnContextClassRefsPredicate = PredicateSupport.constant(flag);
+ }
+
+ /**
+ * Set a condition for whether to validate the incoming assertions' {@link AuthnContextClassRef} against any
+ * {@link RequestedAuthnContext} included in the original request.
+ *
+ * @param condition condition to set
+ */
+ public void setValidateAuthnContextClassRefsPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ validateAuthnContextClassRefsPredicate = Constraint.isNotNull(condition,
+ "Validate AuthnContextClassRefs predicate cannot be null");
+ }
/** {@inheritDoc} */
@Nullable public String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext) {
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
index 7c44674..ee666ba 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -538,6 +538,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
log.debug("{} Setting requested AuthnContextClassRef(s) {}", getLogPrefix(), classrefs);
+ // If we have to validate later, track what's being requested in the state record.
+ final boolean enforcing = profileConfiguration.isValidateAuthnContextClassRefs(profileRequestContext);
+
final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
final SAMLObjectBuilder<RequestedAuthnContext> builder =
@@ -554,7 +557,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
final AuthnContextClassRef obj = acBuilder.buildObject();
obj.setURI(ref);
rac.getAuthnContextClassRefs().add(obj);
- stateData.getAcrs().add(ref);
+ if (enforcing) {
+ stateData.getAcrs().add(ref);
+ }
});
String opstring = input.getmember(SAML2InitiatorConstants.AUTHN_CONTEXT_COMPARISON).string();
@@ -578,7 +583,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
if (operator != null) {
log.debug("{} Setting RequestedAuthnContext operator to {}", getLogPrefix(), operator);
rac.setComparison(operator);
- stateData.setAuthnContextOperator(operator.toString());
+ if (enforcing) {
+ stateData.setAuthnContextOperator(operator.toString());
+ }
}
return rac;
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java
new file mode 100644
index 0000000..ccda3f9
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessAssertionsForAuthentication.java
@@ -0,0 +1,351 @@
+/*
+ * 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 net.shibboleth.sp.saml.saml2.profile.impl;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+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.saml2.assertion.SAML2AssertionValidationParameters;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.RequestedPrincipalContext;
+import net.shibboleth.idp.authn.principal.PrincipalEvalPredicateFactoryRegistry;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+
+/**
+ * Perform processing of a SAML 2 Response's Assertions that have been validated by earlier actions
+ * for use in finalization of SAML-based authentication by later actions.
+ *
+ * <p>The result of this action is to strip any invalid assertions from the response, and to preserve
+ * the "best"/selected {@link AuthnStatement} and any other content required in a pluggable manner.</p>
+ *
+ * <p>This is a copy of an IdP action for the time being as there was no way to override the desired
+ * behavior.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link AuthnEventIds#INVALID_CREDENTIALS}
+ * @post the selected statement is passed into the supplied {@link BiConsumer}
+ */
+public class ProcessAssertionsForAuthentication extends AbstractProfileAction {
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessAssertionsForAuthentication.class);
+
+ /** The resolver for the response to be processed. */
+ @NonnullAfterInit private Function<ProfileRequestContext,Response> responseResolver;
+
+ /** "Sink" for preserving SAML objects. */
+ @NonnullAfterInit private BiConsumer<ProfileRequestContext,AuthnStatement> samlConsumer;
+
+ /** Strategy used to locate the {@link StateDataContext} to check. */
+ @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+
+ /** The registry of predicate factories for custom principal evaluation. */
+ @NonnullBeforeExec private PrincipalEvalPredicateFactoryRegistry evalRegistry;
+
+ /** The Response to process. */
+ @NonnullBeforeExec private Response response;
+
+ /** State data to validate. */
+ @NonnullBeforeExec private SAMLStateData stateData;
+
+ /**
+ * Constructor.
+ */
+ public ProcessAssertionsForAuthentication() {
+ stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
+ }
+
+ /**
+ * Set the strategy function which resolves the response to process.
+ *
+ * @param strategy the new strategy function
+ */
+ public void setResponseResolver(@Nonnull final Function<ProfileRequestContext, Response> strategy) {
+ checkSetterPreconditions();
+ responseResolver = Constraint.isNotNull(strategy, "Response resolver cannot be null");
+ }
+
+ /**
+ * Set the {@link BiConsumer} used to save off the SAML statemen and any related objects as a result of this action.
+ *
+ * <p>This insulates the actiion from the specific context in which it may be used. The supplied consumer
+ * <strong>MUST</strong> establish any non-successful event via the supplied context if it fails.</p>
+ *
+ * @param consumer consumer to set
+ */
+ public void setSAMLConsumer(@Nonnull final BiConsumer<ProfileRequestContext,AuthnStatement> consumer) {
+ checkSetterPreconditions();
+ samlConsumer = Constraint.isNotNull(consumer, "BiConsumer cannot be null");
+ }
+
+ /**
+ * Sets the strategy used to lookup the {@link StateDataContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setStateDataContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+ checkSetterPreconditions();
+ stateDataContextLookupStrategy =
+ Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set the registry of predicate factories for custom principal evaluation.
+ *
+ * @param registry predicate factory registry
+ */
+ public void setPrincipalEvalPredicateFactoryRegistry(
+ @Nonnull final PrincipalEvalPredicateFactoryRegistry registry) {
+
+ evalRegistry = Constraint.isNotNull(registry, "PrincipalEvalPredicateFactoryRegistry cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (responseResolver == null) {
+ throw new ComponentInitializationException("Response resolver cannot be null");
+ } else if (samlConsumer == null) {
+ throw new ComponentInitializationException("BiConsumer cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ response = responseResolver.apply(profileRequestContext);
+ if (response == null || response.getAssertions().isEmpty()) {
+ log.info("{} Profile context contained no candidate Assertions to process. Skipping further processing",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return false;
+ }
+
+ final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+ if (stateDataContext != null && stateDataContext.getStateData() instanceof SAMLStateData samlState) {
+ stateData = samlState;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ // Completely remove any non-valid Assertions from the Response
+ final List<Assertion> nonValid = response.getAssertions().stream()
+ .filter(new AssertionIsValid().negate())
+ .collect(Collectors.toList());
+ log.debug("{} Removing {} non-valid Assertions from Response", getLogPrefix(), nonValid.size());
+ response.getAssertions().removeAll(nonValid);
+
+ // For authn purposes, select only Assertions which contain at least 1 AuthnStatement and a confirmed Subject
+ final Predicate<Assertion> selector = new AssertionContainsAuthenticationStatement()
+ .and(new AssertionContainsConfirmedSubject());
+
+ final List<Assertion> assertions = response.getAssertions().stream()
+ .filter(selector)
+ .collect(Collectors.toList());
+ if (assertions.isEmpty()) {
+ log.info("{} No valid SAML Assertions suitable for authentication were found", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+
+ // Of the remaining, we need to find the {@link AuthnStatement} with the earliest
+ // {@link AuthnStatement#getSessionNotOnOrAfter()} value and that optionally supplies an
+ // acceptable {@link AuthnContext} in the event the original request has specific demands.
+
+ AuthnStatement authnStatement = null;
+ Assertion authnAssertion = null;
+
+ RequestedPrincipalContext helperContext = null;
+
+ if (stateData != null && !stateData.getAcrs().isEmpty()) {
+
+ // Using the RequestedPrincipalContext reuses a lot of low level IdP machinery for us,
+ // we just have to transform the request state into a list of Principals.
+
+ helperContext = new RequestedPrincipalContext();
+
+ final String operator = stateData.getAuthnContextOperator();
+
+ final List<Principal> accumulator = new ArrayList<>(stateData.getAcrs().size());
+ stateData.getAcrs().stream().map(AuthnContextClassRefPrincipal::new).forEach(accumulator::add);
+
+ helperContext
+ .setPrincipalEvalPredicateFactoryRegistry(evalRegistry)
+ .setOperator(operator != null ? operator : AuthnContextComparisonTypeEnumeration.EXACT.toString())
+ .setRequestedPrincipals(accumulator);
+ }
+
+ for (final Assertion assertion : assertions) {
+ for (final AuthnStatement statement : assertion.getAuthnStatements()) {
+ if (helperContext == null || isAcceptable(helperContext, statement.getAuthnContext())) {
+ if (authnStatement == null) {
+ authnStatement = statement;
+ authnAssertion = assertion;
+ } else {
+ final Instant newFence = statement.getSessionNotOnOrAfter();
+ final Instant oldFence = authnStatement.getSessionNotOnOrAfter();
+ if (newFence != null && (oldFence == null || newFence.isBefore(oldFence))) {
+ authnStatement = statement;
+ authnAssertion = assertion;
+ }
+ }
+ }
+ }
+ }
+
+ if (authnAssertion == null) {
+ log.info("{} Could not select a single valid SAML Assertion for authentication", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+ return;
+ }
+
+ log.debug("{} Selected statement from Assertion {} for authentication", getLogPrefix(), authnAssertion.getID());
+
+ samlConsumer.accept(profileRequestContext, authnStatement);
+ }
+
+ private boolean isAcceptable(@Nonnull final RequestedPrincipalContext helperContext,
+ @Nullable final AuthnContext authnContext) {
+
+ final AuthnContextClassRef acr = authnContext != null ? authnContext.getAuthnContextClassRef() : null;
+ final String classRef = acr != null ? acr.getURI() : null;
+ if (classRef == null) {
+ log.debug("Statement's AuthnContext did not contain an AuthnContextClassRef, not valid", getLogPrefix());
+ return false;
+ }
+
+ if (helperContext.isAcceptable(new AuthnContextClassRefPrincipal(classRef))) {
+ log.debug("{} AuthnContextClassRef {} satisfied request", getLogPrefix(), classRef);
+ return true;
+ } else {
+ log.warn("{} AuthnContextClassRef {} did not satisfy request", getLogPrefix(), classRef);
+ return false;
+ }
+ }
+
+ /**
+ * Predicate for valid assertions.
+ */
+ private final class AssertionIsValid implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ final Optional<ValidationProcessingData> validationData = assertion.getObjectMetadata()
+ .get(ValidationProcessingData.class).stream().findFirst();
+ if (validationData.isEmpty()) {
+ return false;
+ }
+
+ return validationData.get().getResult() == ValidationResult.VALID;
+ }
+
+ }
+
+ /**
+ * Predicate for assertions containing at least 1 AuthenticationStatement.
+ */
+ private final class AssertionContainsAuthenticationStatement implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ return ! assertion.getAuthnStatements().isEmpty();
+ }
+
+ }
+
+ /**
+ * Predicate for assertions which have been validated and have a confirmed Subject.
+ */
+ private final class AssertionContainsConfirmedSubject implements Predicate<Assertion> {
+
+ /** {@inheritDoc} */
+ @SuppressWarnings("unused")
+ public boolean test(@Nullable final Assertion assertion) {
+ if (assertion == null) {
+ return false;
+ }
+
+ final Optional<ValidationProcessingData> validationData = assertion.getObjectMetadata()
+ .get(ValidationProcessingData.class).stream().findFirst();
+ if (validationData.isEmpty()) {
+ return false;
+ }
+
+ final ValidationContext validationContext = validationData.get().getContext();
+ if (validationContext == null) {
+ return false;
+ }
+
+ return validationContext.getDynamicParameters()
+ .get(SAML2AssertionValidationParameters.CONFIRMED_SUBJECT_CONFIRMATION) != null;
+ }
+
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list