[java-plugin-shibd-saml] branch main updated: WIP on SAML ACS flow, up to extracting attributes.

Scott Cantor cantor.2 at osu.edu
Thu Sep 12 16:11:26 UTC 2024


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch main
in repository java-plugin-shibd-saml.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-saml.git;a=commit;h=fe11a3fb2be01cf4914121ad60c6723b98450a3d

The following commit(s) were added to refs/heads/main by this push:
     new fe11a3f  WIP on SAML ACS flow, up to extracting attributes.
fe11a3f is described below

commit fe11a3fb2be01cf4914121ad60c6723b98450a3d
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Sep 12 12:11:23 2024 -0400

    WIP on SAML ACS flow, up to extracting attributes.
---
 .../sp/saml/saml2/context/SAMLTokenContext.java    |  79 +++++
 .../sp/saml/saml2/context/package-info.java        |  18 +
 .../config/BrowserSSOProfileConfiguration.java     |  18 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   2 +-
 .../sp/consumer/saml2/saml2-abstract-beans.xml     | 135 +++++++-
 .../sp/consumer/saml2/saml2-abstract-flow.xml      |   2 +-
 .../idp/flows/sp/initiator/saml2/saml2-beans.xml   |   7 +-
 sp-saml-impl/pom.xml                               |  11 +
 .../impl/BrowserSSOProfileConfiguration.java       |  34 ++
 .../saml2/profile/impl/ExtractSAMLAttributes.java  | 374 +++++++++++++++++++++
 .../profile/impl/SAMLTokenContextConsumer.java     |  56 +++
 11 files changed, 726 insertions(+), 10 deletions(-)

diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
new file mode 100644
index 0000000..ffad84d
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
@@ -0,0 +1,79 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Subject;
+
+/**
+ * Manages state for SAML token consumer flow during final assertion processing.
+ */
+public class SAMLTokenContext extends BaseContext {
+    
+    /** Subject of assertion used to authenticate. */
+    @Nullable private Subject subject;
+    
+    /** Authentication statement. */
+    @Nullable private AuthnStatement authnStatement;
+        
+    /**
+     * Get the SAML {@link Subject} from the authentication.
+     * 
+     * @return SAML {@link Subject}
+     */
+    @Nullable public Subject getSubject() {
+        return subject;
+    }
+ 
+    /**
+     * Set the SAML {@link Subject} from the authentication.
+     * 
+     * @param sub the SAML {@link Subject}
+     * 
+     * @return this context
+     */
+    @Nonnull public SAMLTokenContext setSubject(@Nullable final Subject sub) {
+        subject = sub;
+        
+        return this;
+    }
+    
+    /**
+     * Get the SAML {@link AuthnStatement} from the authentication.
+     * 
+     * @return SAML {@link AuthnStatement}
+     */
+    @Nullable public AuthnStatement getAuthnStatement() {
+        return authnStatement;
+    }
+ 
+    /**
+     * Set the SAML {@link AuthnStatement} from the authentication.
+     * 
+     * @param statement the SAML {@link AuthnStatement}
+     * 
+     * @return this context
+     */
+    @Nonnull public SAMLTokenContext setAuthnStatement(@Nullable final AuthnStatement statement) {
+        authnStatement = statement;
+        
+        return this;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java
new file mode 100644
index 0000000..0abea49
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Context classes for SAML SP support.
+ */
+package net.shibboleth.sp.saml.saml2.context;
\ No newline at end of file
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 391a354..407b0ef 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
@@ -14,11 +14,14 @@
 
 package net.shibboleth.sp.saml.saml2.profile.config;
 
+import java.util.Collection;
 import java.util.List;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import net.shibboleth.idp.attribute.IdPAttribute;
 import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotLive;
@@ -51,7 +54,6 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
      * @return required format
      */
     @Nullable String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext);
-
     
     /**
      * Get the SAML binding to insert into an {@link AuthnRequest} to control the response binding.
@@ -64,4 +66,18 @@ public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProf
      */
     @Nullable String getResponseBinding(@Nullable final ProfileRequestContext profileRequestContext);
 
+    /**
+     * Get a strategy function to apply to SAML responses to extract additional {@link IdPAttribute}
+     * objects from the data.
+     * 
+     * <p>This supplements the built-in behavior that decodes any SAML {@Attribute} objects in the
+     * validated assertion(s).</p> 
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return extraction strategy
+     */
+    @Nullable Function<ProfileRequestContext,Collection<IdPAttribute>> getAttributeExtractionStrategy(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
 }
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index c2a9c08..092b011 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -29,7 +29,7 @@
             <bean class="net.shibboleth.sp.profile.context.logic.HttpSeevletRequestPredicate"
                 p:allowedMethods="POST"
                 p:allowedContentTypes="application/x-www-form-urlencoded"
-                p:requiredParameters="SAMLResponse" />
+                p:requiredParameters="#{{ 'SAMLResponse', 'Signature' }}" />
         </property>
     </bean>
 
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
index a54ad5f..452bdf5 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-beans.xml
@@ -7,6 +7,23 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
+    <bean id="InboundEntityIDLookup" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLEntityIDFunction" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+                          c:type="#{ T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext) }" />
+                </constructor-arg>
+                <constructor-arg name="f">
+                    <bean class="org.opensaml.messaging.context.navigate.MessageContextLookup" c:direction="INBOUND" />
+                </constructor-arg>
+            </bean>
+        </constructor-arg>
+    </bean>
+
     <bean id="HandleResponse" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
         <constructor-arg>
             <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -28,11 +45,11 @@
                             p:keyInfoResolver-ref="shibboleth.KeyInfoCredentialResolver" />
                         <bean class="org.opensaml.messaging.handler.impl.CheckMandatoryIssuer" scope="prototype"
                             p:issuerLookupStrategy-ref="InboundEntityIDLookup" />
-                        <bean class="org.opensaml.messaging.handler.impl.CheckExpectedIssuer" scope="prototype"
-                            p:issuerLookupStrategy-ref="InboundEntityIDLookup"
-                            p:expectedIssuerLookupStrategy-ref="OutboundEntityIDLookup" />
-                        <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
-                            p:function="#{getObject('%{sp.saml.inboundMessageHandlerFunction:}'.trim())}" />
+                        <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                            <property name="functionLookupStrategy">
+                                <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+                            </property>
+                        </bean>
                     </list>
                 </property>
              </bean>
@@ -42,4 +59,112 @@
         </property>
     </bean>
 
+    <bean id="AppAwareIssuerLookupFunction" class="net.shibboleth.sp.profile.context.navigate.IssuerLookupFunction" />
+
+    <bean id="PopulateDecryptionParameters"
+        class="org.opensaml.profile.action.impl.PopulateDecryptionParameters" scope="prototype"
+        p:recipientLookupStrategy-ref="AppAwareIssuerLookupFunction"
+        p:configurationLookupStrategy-ref="shibboleth.DecryptionConfigurationLookup"
+        p:decryptionParametersResolver-ref="shibboleth.DecryptionParametersResolver" />
+
+    <bean id="DecryptAssertions" class="org.opensaml.saml.saml2.profile.impl.DecryptAssertions" scope="prototype" />
+
+    <bean id="AssertionValidator" class="org.opensaml.saml.saml2.assertion.SAML20AssertionValidator">
+        <!-- Condition validators. -->
+        <constructor-arg index="0">
+            <util:list>
+                <bean class="org.opensaml.saml.saml2.assertion.impl.AudienceRestrictionConditionValidator" />
+                <bean class="org.opensaml.saml.saml2.assertion.impl.DelegationRestrictionConditionValidator" />
+                <bean class="org.opensaml.saml.saml2.assertion.impl.OneTimeUseConditionValidator">
+                    <constructor-arg ref="shibboleth.ReplayCache" />
+                    <constructor-arg value="#{null}" />
+                </bean>
+                <bean class="org.opensaml.saml.saml2.assertion.impl.ProxyRestrictionConditionValidator" />
+            </util:list>
+        </constructor-arg>
+        <!-- SubjectConfirmation validators. -->
+        <constructor-arg index="1">
+            <util:list>
+                <bean class="org.opensaml.saml.saml2.assertion.impl.BearerSubjectConfirmationValidator" />
+                <bean class="org.opensaml.saml.saml2.assertion.impl.HolderOfKeySubjectConfirmationValidator" />
+            </util:list>
+        </constructor-arg>
+        <!-- Statement validators. -->
+        <constructor-arg index="2">
+            <util:list>
+                <bean class="org.opensaml.saml.saml2.assertion.impl.AuthnStatementValidator" />
+            </util:list>
+        </constructor-arg>
+        <constructor-arg index="3" value="#{getObject('%{sp.saml.assertionValidator:}'.trim())}" />
+        <!-- This is null b/c in this case we use a dynamically-resolved engine in the ValidationContext -->
+        <constructor-arg index="4" value="#{null}" />
+        <constructor-arg index="5">
+            <bean class="org.opensaml.saml.security.impl.SAMLSignatureProfileValidator" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="AssertionValidationContextBuilder" class="org.opensaml.saml.saml2.profile.impl.DefaultAssertionValidationContextBuilder">
+        <property name="clockSkew" value="%{sp.policy.clockSkew:PT3M}" />
+        <property name="lifetime" value="%{sp.policy.assertionLifetime:PT3M}" />
+        <property name="checkAddress">
+            <bean class="net.shibboleth.saml.saml2.profile.config.logic.CheckAddressPredicate" />
+        </property>
+        <property name="maximumTimeSinceAuthn">
+            <bean class="net.shibboleth.saml.saml2.profile.config.navigate.MaximumTimeSinceAuthnLookupFunction" />
+        </property>
+        <property name="additionalAudiences">
+            <bean class="net.shibboleth.saml.profile.config.navigate.AssertionAudiencesLookupFunction" />
+        </property>
+        <property name="signatureRequired">
+            <bean parent="shibboleth.Conditions.NOT">
+                <constructor-arg>
+                    <bean class="org.opensaml.saml.common.profile.logic.InboundMessageSignedPredicate" p:presenceSatisfies="true" />
+                </constructor-arg>
+            </bean>
+        </property>
+        <property name="requiredConditions">
+            <set>
+                <util:constant static-field="org.opensaml.saml.saml2.core.AudienceRestriction.DEFAULT_ELEMENT_NAME" />
+            </set>
+        </property>
+        <!-- TODO: wire up to unsolicited response control point. -->
+        <property name="inResponseToRequired" value="false" />
+        <property name="recipientRequired" value="true" />
+        <property name="notOnOrAfterRequired" value="true" />
+    </bean>
+
+    <bean id="ValidateAssertions"
+        class="org.opensaml.saml.saml2.profile.impl.ValidateAssertions" scope="prototype"
+        p:invalidFatal="false"
+        p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
+        p:validationContextBuilder-ref="AssertionValidationContextBuilder"
+        p:assertionValidator-ref="AssertionValidator" />
+
+    <bean id="DecryptNameIDs" class="org.opensaml.saml.saml2.profile.impl.DecryptNameIDs" scope="prototype" />
+
+    <bean id="DecryptAttributes" class="org.opensaml.saml.saml2.profile.impl.DecryptAttributes" scope="prototype" />
+
+    <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"
+            p:sAMLConsumer-ref="SAMLStatementConsumer">
+        <property name="responseResolver">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <beanclass="org.opensaml.messaging.context.navigate.MessageLookup"
+                        c:type="#{ T(org.opensaml.saml.saml2.core.Response) }" />
+                </constructor-arg>
+                <constructor-arg name="f">
+                    <ref bean="shibboleth.MessageContextLookup.Inbound" />
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="ExtractSAMLAttributes"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ExtractSAMLAttributes" scope="prototype"
+        p:responderLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
+        p:requesterLookupStrategy-ref="AppAwareIssuerLookupFunction" />
+
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
index c1fbc84..aea98b9 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-abstract-flow.xml
@@ -21,7 +21,7 @@
         <evaluate expression="DecryptNameIDs" />
         <evaluate expression="DecryptAttributes" />
         <evaluate expression="ProcessAssertionsForAuthentication" />
-        <evaluate expression="ValidateSAMLAuthentication" />
+        <evaluate expression="ExtractSAMLAttributes" />
 <!--        <evaluate expression="PostAssertionPopulateAuditContext" />-->
 <!--        <evaluate expression="PostResponsePopulateAuditContext" />-->
         
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
index 2caa461..cab72f7 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -139,10 +139,13 @@
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
         <property name="handlers">
             <list>
+                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                    <property name="functionLookupStrategy">
+                        <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+                    </property>
+                </bean>
                 <bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
                 <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
-                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
-                    p:function="#{getObject('%{sp.SAML.outboundMessageHandlerFunction:}'.trim())}" />
                 <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
                     <property name="activationCondition">
                         <bean parent="shibboleth.Conditions.NOT">
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 2e05e23..63b4b34 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -34,6 +34,11 @@
             <scope>provided</scope>
         </dependency>
 
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-profile-api</artifactId>
@@ -56,6 +61,12 @@
             <artifactId>shib-attribute-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-filter-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
         <dependency>
             <groupId>${shib-metadata.groupId}</groupId>
             <artifactId>shib-metadata-api</artifactId>
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 31fed31..baa3049 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
@@ -23,6 +23,7 @@ import java.util.function.Predicate;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import net.shibboleth.idp.attribute.IdPAttribute;
 import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
 import net.shibboleth.shared.annotation.constraint.NonNegative;
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
@@ -78,6 +79,10 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
     
     /** Lookup function for response binding. */
     @Nonnull private Function<ProfileRequestContext,String> responseBindingLookupStrategy;
+   
+    /** Lookup function for attribute extraction strategy. */
+    @Nonnull Function<ProfileRequestContext,Function<ProfileRequestContext,Collection<IdPAttribute>>>
+    attributeExtractionStrategyLookupStrategy;
     
     /** Constructor. */
     public BrowserSSOProfileConfiguration() {
@@ -103,6 +108,7 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
         attributeIndexLookupStrategy = FunctionSupport.constant(null);
         requestedAttributesLookupStrategy = FunctionSupport.constant(null);
         responseBindingLookupStrategy = FunctionSupport.constant(SAMLConstants.SAML2_POST_BINDING_URI);
+        attributeExtractionStrategyLookupStrategy = FunctionSupport.constant(null);
     }
 
     /** {@inheritDoc} */
@@ -411,5 +417,33 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsum
         responseBindingLookupStrategy =
                 Constraint.isNotNull(strategy, "Response binding lookup strategy cannot be null");
     }
+
+    /** {@inheritDoc} */
+    @Nullable public Function<ProfileRequestContext,Collection<IdPAttribute>> getAttributeExtractionStrategy(
+            @Nullable ProfileRequestContext profileRequestContext) {
+        return attributeExtractionStrategyLookupStrategy.apply(profileRequestContext);
+    }
+    
+    /**
+     * Set the attribute extraction strategy.
+     * 
+     * @param strategy strategy function
+     */
+    public void setAttributeExtractionStrategy(
+            @Nonnull Function<ProfileRequestContext,Collection<IdPAttribute>> strategy) {
+        attributeExtractionStrategyLookupStrategy = FunctionSupport.constant(strategy);
+    }
+    
+    /**
+     * Set the lookup strategy for the attribute extraction strategy.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAttributeExtractionStrategyLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Function<ProfileRequestContext,Collection<IdPAttribute>>>
+            strategy) {
+        attributeExtractionStrategyLookupStrategy = Constraint.isNotNull(strategy,
+                "Attribute extraction strategy lookup strategy cannot be null");
+    }
     
 }
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
new file mode 100644
index 0000000..8bbbec0
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -0,0 +1,374 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.RecursiveTypedParentContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import net.shibboleth.idp.attribute.AttributeDecodingException;
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
+import net.shibboleth.idp.attribute.filter.AttributeFilterException;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext;
+import net.shibboleth.idp.attribute.filter.context.AttributeFilterContext.Direction;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
+import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
+import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
+import net.shibboleth.idp.attribute.transcoding.TranscodingRule;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.saml.profile.context.navigate.SAMLMetadataContextLookupFunction;
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.service.ServiceException;
+import net.shibboleth.shared.service.ServiceableComponent;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
+import net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
+
+/**
+ * An action that extracts {@link IdPAttribute} objects from an inbound SAML 2.0 SSO response.
+ *
+ * <p>Attributes decoded from the assertion(s) are in an unfiltered state and subject to the filtering
+ * service. Any other data extracted is stored directly in the filtered set.</p>
+ *  
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ */
+public class ExtractSAMLAttributes extends AbstractApplicationAction {
+
+    /** Default prefix for metrics. */
+    @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.saml"; 
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractSAMLAttributes.class);
+
+    /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+    /** Strategy used to look up {@link SAMLTokenContext} to operate on. */
+    @Nonnull private Function<ProfileRequestContext,SAMLTokenContext> samlTokenContextLookupStrategy;
+
+    /** Function used to obtain the requester ID. */
+    @Nonnull private Function<ProfileRequestContext,String> requesterLookupStrategy;
+
+    /** Function used to obtain the issuer ID. */
+    @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
+    
+    /** Strategy used to create {@link AttributeContext} to hold results. */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextCreationStrategy;
+
+    /** Context containing the token(s) to process. */
+    @NonnullBeforeExec private SAMLTokenContext samlTokenContext;
+    
+    /** Store off profile config. */
+    @NonnullBeforeExec private BrowserSSOProfileConfiguration profileConfiguration;
+    
+    /** Context for externally supplied inbound attributes. */
+    @NonnullBeforeExec private AttributeContext attributeContext;
+        
+    /** Constructor. */
+    public ExtractSAMLAttributes() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        samlTokenContextLookupStrategy = new ChildContextLookup<>(SAMLTokenContext.class);
+
+        requesterLookupStrategy = new RelyingPartyIdLookupFunction();
+        issuerLookupStrategy = new IssuerLookupFunction();
+
+        // PRC -> SAMLTokenContext -> AttributeContext
+        attributeContextCreationStrategy = new ChildContextLookup<>(AttributeContext.class, true).compose( 
+                new ChildContextLookup<>(SAMLTokenContext.class));
+    }
+
+    /**
+     * Set the strategy used to return the {@link RelyingPartyContext} for configuration options.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to return the {@link SAMLTokenContext} for input.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSAMLTokenContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SAMLTokenContext> strategy) {
+        checkSetterPreconditions();
+        samlTokenContextLookupStrategy =
+                Constraint.isNotNull(strategy, "SAMLTokenContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the requester ID for filtering.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequesterLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        requesterLookupStrategy = Constraint.isNotNull(strategy, "Requester lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the issuer ID for filtering.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to create the {@link AttributeContext} to hold results.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setAttributeContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,AttributeContext> strategy) {
+        checkSetterPreconditions();
+        attributeContextCreationStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        samlTokenContext = this.samlTokenContextLookupStrategy.apply(profileRequestContext);
+        if (samlTokenContext == null) {
+            log.debug("{} No SAMLAuthnContext available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+
+        final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            log.error("{} Unable to locate RelyingPartyContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        } else if (rpContext.getProfileConfig() == null) {
+            log.error("{} Unable to locate profile configuration", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        } else if (!(rpContext.getProfileConfig() instanceof BrowserSSOProfileConfiguration)) {
+            log.error("{} Not a SAML 2 profile configuration", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        attributeContext = attributeContextCreationStrategy.apply(profileRequestContext);
+        if (attributeContext == null) {
+            log.debug("{} Unable to create AttributeContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        profileConfiguration = (BrowserSSOProfileConfiguration) rpContext.getProfileConfig();
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        processAttributes(profileRequestContext);
+        
+        final Function<ProfileRequestContext,Collection<IdPAttribute>> aes =
+                profileConfiguration.getAttributeExtractionStrategy(profileRequestContext);
+        if (aes != null) {
+            log.debug("{} Applying custom attribute extraction strategy", getLogPrefix());
+            final Collection<IdPAttribute> attributes = new ArrayList<>(attributeContext.getIdPAttributes().values());
+            final Collection<IdPAttribute> newAttributes = aes.apply(profileRequestContext);
+            if (newAttributes != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("{} Extracted attributes with custom strategy: {}", getLogPrefix(),
+                            newAttributes.stream().map(IdPAttribute::getId).collect(Collectors.toUnmodifiableList()));
+                }
+                attributes.addAll(newAttributes);
+                attributeContext.setIdPAttributes(attributes);
+            }
+        }
+        
+        // TODO: NameID handling
+        
+        // TODO: built-in variables if handled here
+    }
+    
+    /**
+     * Process the inbound SAML Attributes.
+     * 
+     * @param profileRequestContext current profile request context
+     */
+    private void processAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        log.debug("{} Decoding incoming SAML Attributes", getLogPrefix());
+        
+        final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
+        assert mapped != null;
+
+        try (final ServiceableComponent<AttributeTranscoderRegistry> component =
+                ensureApplication().getAttributeTranscoderRegistry().getServiceableComponent()) {
+            final MessageContext imc = profileRequestContext.getInboundMessageContext();
+            assert imc != null;
+            final Response response = (Response) imc.getMessage();
+            assert response != null;
+            for (final Assertion assertion : response.getAssertions()) {
+                for (final AttributeStatement statement : assertion.getAttributeStatements()) {
+                    for (final Attribute designator : statement.getAttributes()) {
+                        assert designator!=null;
+                        try {
+                            decodeAttribute(component.getComponent(), profileRequestContext, designator, mapped);
+                        } catch (final AttributeDecodingException e) {
+                            log.error("{} Error decoding inbound Attribute", getLogPrefix(), e);
+                        }
+                    }
+                }
+            }
+        } catch (final ServiceException e) {
+            log.error("Attribute transcoder service unavailable", e);
+            return;
+        }
+
+        log.debug("{} Incoming SAML Attributes mapped to attribute IDs: {}", getLogPrefix(), mapped.keySet());
+
+        if (!mapped.isEmpty()) {
+            attributeContext.setUnfilteredIdPAttributes(mapped.values());
+            attributeContext.setIdPAttributes(null);
+            filterAttributes(profileRequestContext);
+        }
+    }
+    
+    /**
+     * Access the registry of transcoding rules to decode the input {@link Attribute}.
+     * 
+     * @param registry  registry of transcoding rules
+     * @param profileRequestContext current profile request context
+     * @param input input object
+     * @param results collection to add results to
+     * 
+     * @throws AttributeDecodingException if an error occurs or no results were obtained
+     */
+    private void decodeAttribute(@Nonnull final AttributeTranscoderRegistry registry,
+            @Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final Attribute input,
+            @Nonnull @Live final Multimap<String,IdPAttribute> results) throws AttributeDecodingException {
+        
+        final Collection<TranscodingRule> transcodingRules = registry.getTranscodingRules(input);
+        if (transcodingRules.isEmpty()) {
+            log.info("{} No transcoding rule for Attribute (Name '{}', NameFormat: '{}')", getLogPrefix(),
+                    input.getName(), input.getNameFormat() != null ? input.getNameFormat() : Attribute.UNSPECIFIED);
+            return;
+        }
+        
+        for (final TranscodingRule rules : transcodingRules) {
+            assert rules != null;
+            final AttributeTranscoder<Attribute> transcoder = TranscoderSupport.getTranscoder(rules);
+            final IdPAttribute decodedAttribute = transcoder.decode(profileRequestContext, input, rules);
+            if (decodedAttribute != null) {
+                results.put(decodedAttribute.getId(), decodedAttribute);
+            }
+        }
+    }
+    
+    /**
+     * Check for inbound attributes and apply filtering.
+     * 
+     * @param profileRequestContext current profile request context
+     */
+    private void filterAttributes(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final AttributeFilterContext filterContext =
+                samlTokenContext.ensureSubcontext(AttributeFilterContext.class);
+
+        populateFilterContext(profileRequestContext, filterContext);
+        
+        try (final ServiceableComponent<AttributeFilter> filterComponent =
+                ensureApplication().getAttributeFilter().getServiceableComponent();
+                final ServiceableComponent<MetadataResolver> metadataResolverComponent =
+                        ensureApplication().getMetadataResolver().getServiceableComponent()) {
+
+            // Populate here for locking scope.
+            filterContext.setMetadataResolver(metadataResolverComponent.getComponent());
+            
+            final AttributeFilter filter = filterComponent.getComponent();
+            filter.filterAttributes(filterContext);
+            filterContext.removeFromParent();
+            attributeContext.setIdPAttributes(filterContext.getFilteredIdPAttributes().values());
+        } catch (final AttributeFilterException e) {
+            log.error("{} Error while filtering inbound attributes", getLogPrefix(), e);
+        } catch (final ServiceException e) {
+            log.error("{} Invalid AttributeFilter configuration", getLogPrefix(), e);
+        }
+    }
+    
+    /**
+     * Fill in the filter context data.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param filterContext context to populate
+     */
+    private void populateFilterContext(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AttributeFilterContext filterContext) {
+        
+        filterContext.setDirection(Direction.INBOUND)
+            .setPrefilteredIdPAttributes(attributeContext.getUnfilteredIdPAttributes().values())
+            .setRequesterMetadataContextLookupStrategy(null)
+            .setIssuerMetadataContextLookupStrategy(
+                    new SAMLMetadataContextLookupFunction().compose(
+                            new RecursiveTypedParentContextLookup<>(ProfileRequestContext.class)))
+            .setProxiedRequesterContextLookupStrategy(null)
+            .setAttributeIssuerID(issuerLookupStrategy.apply(profileRequestContext))
+            .setAttributeRecipientID(requesterLookupStrategy.apply(profileRequestContext));
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
new file mode 100644
index 0000000..a91a014
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
@@ -0,0 +1,56 @@
+/*
+ * 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.util.function.BiConsumer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
+
+/**
+ * Bridge class to adapt IdP action for saving off SAML token defails for SP use.
+ */
+public class SAMLTokenContextConsumer implements BiConsumer<ProfileRequestContext,AuthnStatement> {
+
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SAMLTokenContextConsumer.class);
+    
+    /** {@inheritDoc} */
+    public void accept(@Nullable final ProfileRequestContext profileRequestContext,
+            @Nullable final AuthnStatement statement) {
+        
+        if (profileRequestContext == null || statement == null) {
+            log.error("ProfileRequestContext or statement were null");
+            return;
+        }
+        
+        final SAMLTokenContext tokenContext = profileRequestContext.ensureSubcontext(SAMLTokenContext.class);
+        tokenContext.setAuthnStatement(statement);
+
+        final XMLObject parent = statement.getParent();
+        if (parent instanceof Assertion assertion) {
+            tokenContext.setSubject(assertion.getSubject());
+        }
+    }
+
+}
\ 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