[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