[java-plugin-shibd-saml] branch main updated: Lightly tested ECP flow.

Codeberg noreply at shibboleth.net
Thu Aug 6 20:17:37 UTC 2026


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

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

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/3d698b7efbe1cad2c09fa1bd07469c4608efa93b

The following commit(s) were added to refs/heads/main by this push:
     new 3d698b7  Lightly tested ECP flow.
3d698b7 is described below

commit 3d698b7efbe1cad2c09fa1bd07469c4608efa93b
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Thu Aug 6 16:17:22 2026 -0400

    Lightly tested ECP flow.
---
 .../logic/WantECPRequestsSignedPredicate.java      |  43 +++
 .../sp/saml/saml2/context/logic/package-info.java  |  18 ++
 .../idp/flows/sp/initiator/ecp/ecp-beans.xml       | 118 ++++++++
 .../idp/flows/sp/initiator/ecp/ecp-flow.xml        |  62 ++++
 .../shibboleth/idp/flows/sp/saml2-common-beans.xml |  30 +-
 .../flows/saml2/ECPSessionInitiatorFlowTest.java   | 328 +++++++++++++++++++++
 ...PEnvironmentApplicationContextInitializer.java} |   2 +-
 ...MLEnvironmentApplicationContextInitializer.java |   1 +
 .../shibboleth/idp/module/conf/sp/sp.properties    |   2 +-
 sp-saml-impl/pom.xml                               |   5 +
 .../config/impl/ECPProfileConfiguration.java       |   8 +-
 .../saml/saml2/profile/impl/AddAuthnRequest.java   |  19 ++
 .../impl/InitializeOutboundMessageContext.java     | 170 +++++++++++
 .../profile/impl/InitializePAOSBindingContext.java | 140 +++++++++
 .../saml/saml2/profile/impl/ParseECPOptions.java   |   2 +-
 .../profile/impl/PrepareInboundMessageContext.java |  97 ++++++
 .../services/org.opensaml.core.config.Initializer  |   1 +
 17 files changed, 1026 insertions(+), 20 deletions(-)

diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/WantECPRequestsSignedPredicate.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/WantECPRequestsSignedPredicate.java
new file mode 100644
index 0000000..52849b4
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/WantECPRequestsSignedPredicate.java
@@ -0,0 +1,43 @@
+/*
+ * 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.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.sp.saml.saml2.context.ECPOptionsContext;
+
+/**
+ * Condition evaluating {@link ECPOptionsContext#isWantAuthnRequestsSigned()}.
+ */
+public class WantECPRequestsSignedPredicate implements Predicate<ProfileRequestContext> {
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        
+        if (input != null) {
+            final ECPOptionsContext options = input.getSubcontext(ECPOptionsContext.class);
+            if (options != null) {
+                return options.isWantAuthnRequestsSigned();
+            }
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/package-info.java
new file mode 100644
index 0000000..eddc173
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/logic/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.
+ */
+
+/**
+ * Conditions for SAML-specific context information.
+ */
+package net.shibboleth.sp.saml.saml2.context.logic;
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-beans.xml
new file mode 100644
index 0000000..53f0b38
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-beans.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <util:constant id="shibboleth.sp.profileId"
+        static-field="net.shibboleth.saml.saml2.profile.config.ECPProfileConfiguration.PROFILE_ID" />
+        
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.saml.service.logging.session-initiator.ecp:SessionInitiator.ECP}" />
+
+    <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml" />
+
+    <bean id="ValidateSessionInitiatorRequest"
+        class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest" scope="prototype"
+        p:flowId="ecp"
+        p:requireDiscoveryURL="false"
+        p:requireRelyingPartyId="false" />
+
+    <bean id="ParseECPOptions"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ParseECPOptions" scope="prototype" />
+
+    <bean id="SelectECPRelyingPartyConfiguration"
+        class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:treatAnonymousAsVerified="true" />
+
+    <bean id="InitializeECPOutboundMessageContext"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple"
+        p:requirePeerIdentity="false" />
+
+    <bean id="InitializePAOSBindingContext"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.InitializePAOSBindingContext" scope="prototype"
+        p:bindingDescriptor-ref="shibboleth.Binding.SAML2PAOS" />
+
+    <bean id="PopulateECPRequestSignatureSigningParameters" parent="PopulateRequestSignatureSigningParameters">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.OR">
+                <constructor-arg>
+                    <list>
+                        <bean class="net.shibboleth.sp.saml.saml2.context.logic.WantECPRequestsSignedPredicate" />
+                        <bean class="net.shibboleth.saml.profile.config.logic.SignRequestsPredicate"
+                            p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
+                    </list>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <util:map id="InboundSAML2BindingMap">
+        <entry key="PAOS">
+            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_PAOS_BINDING_URI" />
+        </entry>
+    </util:map>
+
+    <bean id="AddAuthnRequest"
+            class="net.shibboleth.sp.saml.saml2.profile.impl.AddAuthnRequest" scope="prototype"
+            p:overwriteExisting="true"
+            p:nameIDLookupStrategy="#{getObject('%{sp.saml.NameIDLookupStrategy:}'.trim())}"
+            p:inboundBindingMap-ref="InboundSAML2BindingMap"
+            p:responseLocationRequired="true">
+        <property name="identifierGeneratorLookupStrategy">
+            <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+        </property>
+    </bean>
+
+    <bean id="HandleOutboundECPMessage"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:messageHandler-ref="PreEncodeECPMessageHandler"
+            c:executionDirection="OUTBOUND">
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+        
+    <bean id="PreEncodeECPMessageHandler"
+            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.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype" />
+            </list>
+        </property>
+    </bean>
+
+    <!-- Handlers to run during message encoding to add SOAP headers. -->
+
+    <bean id="shibboleth.sp.BindingSpecificMessageHandler"
+            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
+        <property name="handlers">
+            <util:list>
+                <bean class="net.shibboleth.sp.saml.saml2.binding.impl.AddPAOSRequestHeaderHandler" scope="prototype" />
+                <bean class="net.shibboleth.sp.saml.saml2.binding.impl.AddECPRequestHeaderHandler" scope="prototype" />
+                <bean class="org.opensaml.saml.saml2.binding.impl.AddRelayStateHeaderHandler" scope="prototype" />
+            </util:list>
+        </property>
+    </bean>
+
+    <!-- Binding objects (not in IdP at present). -->
+
+    <bean parent="shibboleth.BindingDescriptor" id="shibboleth.Binding.SAML2PAOS"
+        p:id="urn:oasis:names:tc:SAML:2.0:bindings:PAOS"
+        p:shortName="PAOS"
+        p:encoderBeanId="shibboleth.Encoders.SAML2PAOSEncoder" />
+
+    <bean id="shibboleth.Encoders.SAML2PAOSEncoder"
+          class="net.shibboleth.sp.saml.saml2.binding.encoding.impl.HTTPPAOSEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
+
+</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-flow.xml
new file mode 100644
index 0000000..bd91e52
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-flow.xml
@@ -0,0 +1,62 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/initiator">
+
+    <action-state id="InitializeECPRequest">
+        <on-entry>
+            <evaluate expression="opensamlProfileRequestContext.setProfileId(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.sp.profileId'))" />
+        </on-entry>
+        
+        <evaluate expression="ValidateSessionInitiatorRequest" />
+        <evaluate expression="ParseECPOptions" />
+        <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckForRelyingParty" />
+    </action-state>
+    
+    <decision-state id="CheckForRelyingParty">
+        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))?.getRelyingPartyId() != null"
+            then="MetadataLookup" else="ContinueECPRequest" />
+    </decision-state>
+    
+    <action-state id="MetadataLookup">
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="ContinueECPRequest" />
+    </action-state>
+    
+    <action-state id="ContinueECPRequest">
+        <evaluate expression="SelectECPRelyingPartyConfiguration" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="SelectProfileConfiguration" />
+        
+        <evaluate expression="InitializeECPOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <evaluate expression="InitializePAOSBindingContext" />
+
+        <evaluate expression="PopulateECPRequestSignatureSigningParameters" />
+        <evaluate expression="PopulateEncryptionParameters" />
+        
+        <evaluate expression="AddAuthnRequest" />
+        <evaluate expression="OutboundNameIDPopulateAuditContext" />
+        <evaluate expression="EncryptNameIDs" />
+        
+        <evaluate expression="HandleOutboundECPMessage" />
+        <evaluate expression="PreserveRelayState" />
+        <evaluate expression="OutboundRequestPopulateAuditContext" />
+        <evaluate expression="EncodeMessage" />
+        
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="proceed" />
+    </action-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/initiator/ecp/ecp-beans.xml" />
+
+</flow>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
index c29bb3d..b564d24 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
@@ -63,8 +63,7 @@
     </bean>
 
     <bean id="PrepareInboundMessageContext"
-        class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
-        p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+        class="net.shibboleth.sp.saml.saml2.profile.impl.PrepareInboundMessageContext" scope="prototype" />
 
     <util:constant id="shibboleth.MetadataLookup.Protocol"
         static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS" />
@@ -93,15 +92,6 @@
     <bean id="InitializeRelyingPartyContextFromSAMLPeer"
         class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
 
-    <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
-        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
-
-    <bean id="InitializeMessageChannelSecurityContext" 
-        class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
-        p:confidentialityActive="false"
-        p:integrityActive="false" />
-
     <bean id="SelectRelyingPartyConfiguration"
         class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
 
@@ -112,6 +102,15 @@
         class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
         p:profileId-ref="shibboleth.sp.profileId" />
 
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
+
+    <bean id="InitializeMessageChannelSecurityContext" 
+        class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
+        p:confidentialityActive="false"
+        p:integrityActive="false" />
+
     <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
         c:_0-ref="OutgoingSAML2SPRequestBindings" />
 
@@ -184,9 +183,6 @@
         </property>
     </bean>
     
-    <bean id="OutboundRequestPopulateAuditContext" parent="shibboleth.sp.AbstractPopulateAuditContext"
-        p:fieldExtractors="#{getObject('shibboleth.sp.saml.OutboundRequestAuditExtractors') ?: getObject('shibboleth.sp.saml.DefaultOutboundRequestAuditExtractors')}" />
-    
     <bean id="PreEncodeMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
         <property name="handlers">
@@ -211,6 +207,9 @@
         </property>
     </bean>
 
+    <bean id="OutboundRequestPopulateAuditContext" parent="shibboleth.sp.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.sp.saml.OutboundRequestAuditExtractors') ?: getObject('shibboleth.sp.saml.DefaultOutboundRequestAuditExtractors')}" />
+    
     <bean id="PreserveRelayState"
         class="net.shibboleth.sp.saml.saml2.profile.impl.PreserveRelayState" scope="prototype"
         p:errorFatal="%{sp.stateToken.errorsFatal:false}" />
@@ -218,7 +217,8 @@
     <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
 
     <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
-        p:messageEncoderFactory-ref="messageEncoderFactory" />
+        p:messageEncoderFactory-ref="messageEncoderFactory"
+        p:messageHandler="#{getObject('shibboleth.sp.BindingSpecificMessageHandler')}" />
 
     <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
 
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/ECPSessionInitiatorFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/ECPSessionInitiatorFlowTest.java
new file mode 100644
index 0000000..4b7074f
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/ECPSessionInitiatorFlowTest.java
@@ -0,0 +1,328 @@
+/*
+ * 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.flows.saml2;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.soap.soap11.Envelope;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
+import net.shibboleth.idp.test.PreferFileSystemApplicationContextInitializer;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.flows.AbstractSPFlowTest;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.profile.InitiatorConstants;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.saml.saml2.profile.SAML2InitiatorConstants;
+
+/**
+ * Unit test for the SP session-initiator flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath:/net/shibboleth/sp/saml-test-beans.xml",
+                },
+        initializers = {
+                TestSPECPEnvironmentApplicationContextInitializer.class,
+                PreferFileSystemApplicationContextInitializer.class,
+                IdPPropertiesApplicationContextInitializer.class
+                },
+        inheritInitializers = false
+        )
+ at WebAppConfiguration
+public class ECPSessionInitiatorFlowTest extends AbstractSPFlowTest {
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/session-initiator";
+
+    /** Issuer value. */
+    @Nonnull public static final String ISSUER = "https://testsp.example.org";
+    
+    /** Resource URL. */
+    @Nonnull public static final byte[] RESOURCE_URL = "https://sp.example.org/secure".getBytes(StandardCharsets.UTF_8);
+
+    /** ACS URL. */
+    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/ACS";
+
+    /** PAOS ACS URL. */
+    @Nonnull public static final String PAOS_RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/SAML2/ECP";
+
+    /** Constructor. */
+    protected ECPSessionInitiatorFlowTest() {
+        super(FLOW_ID);
+    }
+
+    /**
+     * Test flow with profile disallowed.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testProfileDisabled() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        addPAOSHeaders(input, false);
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        setApplicationRequest("no-profile", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, IdPEventIds.INVALID_PROFILE_CONFIG);
+    }
+
+    /**
+     * Test flow without response URL.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testNoResponseURL() throws IOException {
+        setDefaultAuth();
+
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        addPAOSHeaders(input, false);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+    
+    /**
+     * Test legacy response URL success case picking first URL.
+     * 
+     * @throws Exception 
+     */
+    @Test
+    public void testFirstResponseURL() throws Exception {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        addPAOSHeaders(input, false);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        final DDF endpoints = input.addmember(InitiatorConstants.RESPONSE_URL).list();
+        endpoints.add(new DDF("PAOS").string(PAOS_RESPONSE_URL));
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageSuccess(result);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        Assert.assertEquals(req.getAssertionConsumerServiceURL(), PAOS_RESPONSE_URL);
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test simple success case with computed relay state.
+     * 
+     * @throws Exception 
+     */
+    @Test
+    public void testSimpleWithoutState() throws Exception {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        addPAOSHeaders(input, false);
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(SPConstants.TARGET).unsafe_string("https://sp.example.org/cgi-bin/test.cgi?foo=bar%20baz&frobnitz=zorkmid".getBytes(StandardCharsets.UTF_8));
+        
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageSuccess(result);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertEquals(req.getAssertionConsumerServiceURL(), RESPONSE_URL);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test success supplying IsPassive from agent.
+     * 
+     * @throws Exception 
+     */
+    @Test
+    public void testIsPassiveFromAgent() throws Exception {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        addPAOSHeaders(input, false);
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        input.addmember(SAML2InitiatorConstants.IS_PASSIVE).integer(1);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageSuccess(result);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertTrue(req.isPassive());
+    }
+
+
+    /**
+     * Add the PAOS headers.
+     * 
+     * @param input the top level input message containing the renoted request
+     * @param signed whether to include the request signing option
+     */
+    private void addPAOSHeaders(@Nonnull final DDF input, final boolean signed) {
+        final StringBuilder builder = new StringBuilder(SAML2InitiatorConstants.PAOS_HEADER_VERSION_INDICATOR + ' ' + SAMLConstants.SAML20ECP_NS);
+        if (signed) {
+            builder.append(", ").append(SAML2InitiatorConstants.ECP20_OPTION_WANT_AUTHN_REQUESTS_SIGNED);
+        }
+        
+        final DDF headers = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+                .addmember(RemotedHttpServletRequest.HEADERS).structure();
+        
+        headers.addmember("Accept").unsafe_string(SAML2InitiatorConstants.PAOS_MEDIA_TYPE.getBytes(StandardCharsets.UTF_8));
+        headers.addmember("PAOS").unsafe_string(builder.toString().getBytes(StandardCharsets.UTF_8));
+    }
+    
+    /**
+     * Decode an encoded response and run sanity checks against it.
+     * 
+     * @param result flow execution result
+     * @param format NameID format to check for in policy element
+     * 
+     * @return the request object
+     * 
+     * @throws Exception 
+     */
+    @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result,
+            @Nullable final String format) throws Exception {
+        final ProfileRequestContext prc = retrieveProfileRequestContext(result);
+        assert prc != null;
+        final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
+        final DDF input = arc.getInput();
+        final DDF output = arc.getOutput();
+        assert input != null;
+        assert output != null;
+        Assert.assertTrue(output.isstruct());
+        final DDF http = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
+        Assert.assertTrue(http.isstruct());
+        
+        final byte[] body = http.getmember(RemotedHttpServletResponse.RESPONSE).getmember(RemotedHttpServletResponse.DATA).unsafe_string();
+        assert body != null;
+        final Envelope env = decodeSOAPEnvelope(body);
+        final AuthnRequest authnRequest = (AuthnRequest) Constraint.isNotNull(env.getBody(), "Body was null").getUnknownXMLObjects().get(0);
+        
+        Assert.assertNotNull(authnRequest.getID());
+        Assert.assertEquals(authnRequest.getProtocolBinding(), SAMLConstants.SAML2_PAOS_BINDING_URI);
+        Assert.assertTrue(Instant.now().isAfter(authnRequest.getIssueInstant()));
+        final Issuer issuer = authnRequest.getIssuer();
+        assert issuer != null;
+        Assert.assertEquals(issuer.getValue(), ISSUER);
+        
+        boolean foundCorrelationCookie = false;
+        for (final DDF header : http.getmember(RemotedHttpServletResponse.HEADERS)) {
+            if ("Set-Cookie".equals(header.name())) {
+                final String cookie = header.string();
+                assert cookie != null;
+                // TODO: Ideally we would extract the cookie value from the header and be able to
+                // test against a base64'd JSON string, but that's a fair bit of work.
+                if (cookie.startsWith("__Host-" + "shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_')) {
+                    Assert.assertTrue(
+                            cookie.startsWith("__Host-shibsp_state_" + input.getmember(SPConstants.APPLICATION).string() + '_'
+                                    + SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()) + '='));
+                    foundCorrelationCookie = true;
+                }
+            }
+        }
+        Assert.assertTrue(foundCorrelationCookie);
+        
+        final NameIDPolicy pol = authnRequest.getNameIDPolicy();
+        assert pol != null;
+        assertTrue(pol.getAllowCreate());
+        Assert.assertEquals(pol.getFormat(), format);
+        
+        return authnRequest;
+    }
+
+    
+    /**
+     * Decodes a SAML message encoded via HTTP-PAOS binding.
+     * 
+     * @param body the message body
+     * 
+     * @return decoded message
+     * 
+     * @throws XMLParserException 
+     * @throws IOException 
+     * @throws UnmarshallingException 
+     */
+    @Nonnull protected Envelope decodeSOAPEnvelope(@Nonnull final byte[] body)
+            throws IOException, UnmarshallingException, XMLParserException {
+        
+        
+        try (final ByteArrayInputStream in = new ByteArrayInputStream(body)) {
+            return (Envelope) XMLObjectSupport.unmarshallFromInputStream(parserPool, in);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPECPEnvironmentApplicationContextInitializer.java
similarity index 93%
copy from sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
copy to sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPECPEnvironmentApplicationContextInitializer.java
index 2c54daf..4f82dcb 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPECPEnvironmentApplicationContextInitializer.java
@@ -24,7 +24,7 @@ import net.shibboleth.sp.flows.TestSPEnvironmentApplicationContextInitializer;
 /**
  * An {@link ApplicationContextInitializer} which extends the parent plugin's test initializer.
  */
-public class TestSPSAMLEnvironmentApplicationContextInitializer extends TestSPEnvironmentApplicationContextInitializer {
+public class TestSPECPEnvironmentApplicationContextInitializer extends TestSPEnvironmentApplicationContextInitializer {
 
     /** {@inheritDoc} */
     @Override
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
index 2c54daf..dfb3f40 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
@@ -33,6 +33,7 @@ public class TestSPSAMLEnvironmentApplicationContextInitializer extends TestSPEn
         mock.setProperty("sp.service.agents.resources", "test.sp.saml.AgentResolverResources");
         mock.setProperty("sp.agent.authn.method", "basic");
         mock.setProperty("sp.stateToken.sealed", "false");
+        mock.setProperty("sp.defaultAuthority", "https://idp.example.org");
         mock.setProperty("idp.additionalProperties",
                 "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/saml.properties");
     }
diff --git a/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 6537428..2b33649 100644
--- a/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -2,7 +2,7 @@
 sp.issuer = https://sp.example.org
 
 # Default authentication authority (IdP, OP, etc.) to use if any
-sp.defaultAuthority = https://idp.example.org
+#sp.defaultAuthority = https://idp.example.org
 
 # Configuration of service to resolve SP agents and applications
 # Uncomment and define to override service resources
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
index 00b7dfa..fb70342 100644
--- a/sp-saml-impl/pom.xml
+++ b/sp-saml-impl/pom.xml
@@ -49,6 +49,11 @@
             <artifactId>idp-saml-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-saml-impl</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/ECPProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java
index a4b8deb..be0d2c0 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java
@@ -40,12 +40,16 @@ public class ECPProfileConfiguration extends BrowserSSOProfileConfiguration
     /** Constructor. */
     public ECPProfileConfiguration() {
         super(net.shibboleth.saml.saml2.profile.config.ECPProfileConfiguration.PROFILE_ID);
-        
-        setResponseBinding(SAMLConstants.SAML2_PAOS_BINDING_URI);
 
         providerNameLookupStrategy = FunctionSupport.constant(null);
         idpListLookupStrategy = FunctionSupport.constant(null);
     }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public String getResponseBinding(@Nullable ProfileRequestContext profileRequestContext) {
+        return SAMLConstants.SAML2_PAOS_BINDING_URI;
+    }
 
     /** {@inheritDoc} */
     @Nullable public String getProviderName(@Nullable 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 7da0a00..456295f 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
@@ -118,6 +118,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
     /** Map of agent binding tokens to SAML binding constants. */
     @Nonnull private Map<String,String> inboundBindingMap;
     
+    /** Whether tp fail if no response location is available. */
+    private boolean responseLocationRequired;
+    
     /** The generator to use. */
     @NonnullBeforeExec private IdentifierGenerationStrategy idGenerator;
     
@@ -218,6 +221,19 @@ public class AddAuthnRequest extends AbstractApplicationAction {
             inboundBindingMap = CollectionSupport.emptyMap();
         }
     }
+    
+    /**
+     * Sets whether to signal failure if unable to establish an Assertion Consumer Service
+     * location to add to the request.
+     * 
+     * <p>Defaults to false. Usually needed only for ECP.</p>
+     * 
+     * @param flag
+     */
+    public void setResponseLocationRequired(final boolean flag) {
+        checkSetterPreconditions();
+        responseLocationRequired = flag;
+    }
 
     /** {@inheritDoc} */
     @Override
@@ -400,6 +416,9 @@ public class AddAuthnRequest extends AbstractApplicationAction {
         if (endpoints.isEmpty()) {
             // No endpoints were supplied, so we have to rely on the IdP selecting a default.
             // Protocol binding comes from config, may be null.
+            if (responseLocationRequired) {
+                return false;
+            }
             authnRequest.setProtocolBinding(profileConfiguration.getResponseBinding(profileRequestContext));
             return true;
         }
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializeOutboundMessageContext.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializeOutboundMessageContext.java
new file mode 100644
index 0000000..f1ffaf3
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializeOutboundMessageContext.java
@@ -0,0 +1,170 @@
+/*
+ * 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.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.messaging.context.MessageContext;
+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.messaging.context.AttributeConsumingServiceContext;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.messaging.context.SAMLSelfEntityContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that adds an outbound {@link MessageContext} and related SAML contexts to the {@link ProfileRequestContext}
+ * based on the identity of a relying party accessed via a lookup strategy, by default an immediate child of the profile
+ * request context.
+ * 
+ * <p>This is a copy of an IdP class that adds optionality to the existence of the peer via a flag, for ECP use.</p>
+ * 
+ * <p>
+ * A {@link SAMLSelfEntityContext} is created based on the identity of the SP/Agent, as derived by a lookup strategy. A
+ * {@link SAMLPeerEntityContext} and {@link SAMLMetadataContext} are created based on the {@link SAMLPeerEntityContext}
+ * that underlies the {@link RelyingPartyContext}.
+ * </p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ */
+public class InitializeOutboundMessageContext extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(InitializeOutboundMessageContext.class);
+
+    /** Relying party context lookup strategy. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+    /** Strategy used to obtain the self identity value. */
+    @Nonnull private Function<ProfileRequestContext,String> selfIdentityLookupStrategy;
+
+    /** Whether a peer identity is required or not. */
+    private boolean requirePeerIdentity;
+    
+    /** The {@link SAMLPeerEntityContext} to base the outbound context on. */
+    @Nullable private SAMLPeerEntityContext peerEntityCtx;
+
+    /** Constructor. */
+    public InitializeOutboundMessageContext() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        selfIdentityLookupStrategy = new IssuerLookupFunction();
+        
+        requirePeerIdentity = true;
+    }
+
+    /**
+     * Set the relying party context lookup strategy.
+     * 
+     * @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 locate the self identity value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSelfIdentityLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        selfIdentityLookupStrategy = Constraint.isNotNull(strategy, "Self identity lookup strategy cannot be null");
+    }
+    
+    /**
+     * Sets whether the existence of the {@link SAMLPeerEntityContext} is required or not.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setRequirePeerIdentity(final boolean flag) {
+        checkSetterPreconditions();
+        requirePeerIdentity = flag;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final RelyingPartyContext relyingPartyCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        final BaseContext identifyingCtx =
+                relyingPartyCtx != null ? relyingPartyCtx.getRelyingPartyIdContextTree() : null;
+        
+        if (identifyingCtx instanceof SAMLPeerEntityContext peer) {
+            peerEntityCtx = peer;
+        } else if (requirePeerIdentity) {
+            log.debug("{} No SAML peer entity context found via relying party context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final MessageContext msgCtx = new MessageContext();
+        profileRequestContext.setOutboundMessageContext(msgCtx);
+
+        final SAMLSelfEntityContext selfContext = msgCtx.ensureSubcontext(SAMLSelfEntityContext.class);
+        selfContext.setEntityId(selfIdentityLookupStrategy.apply(profileRequestContext));
+
+        if (peerEntityCtx != null) {
+            final SAMLPeerEntityContext localCopy = peerEntityCtx;
+            
+            final SAMLPeerEntityContext peerContext = msgCtx.ensureSubcontext(SAMLPeerEntityContext.class);
+            peerContext.setEntityId(localCopy.getEntityId());
+    
+            final SAMLMetadataContext inboundMetadataCtx = localCopy.getSubcontext(SAMLMetadataContext.class);
+            if (inboundMetadataCtx != null) {
+                final SAMLMetadataContext outboundMetadataCtx = peerContext.ensureSubcontext(SAMLMetadataContext.class);
+                outboundMetadataCtx.setEntityDescriptor(inboundMetadataCtx.getEntityDescriptor());
+                outboundMetadataCtx.setRoleDescriptor(inboundMetadataCtx.getRoleDescriptor());
+                final AttributeConsumingServiceContext acsCtx =
+                        inboundMetadataCtx.getSubcontext(AttributeConsumingServiceContext.class);
+                if (null != acsCtx) {
+                    outboundMetadataCtx.ensureSubcontext(AttributeConsumingServiceContext.class)
+                            .setAttributeConsumingService(acsCtx.getAttributeConsumingService());
+                }
+            }
+        }
+
+        log.debug("{} Initialized outbound message context", getLogPrefix());
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializePAOSBindingContext.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializePAOSBindingContext.java
new file mode 100644
index 0000000..9e146cd
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/InitializePAOSBindingContext.java
@@ -0,0 +1,140 @@
+/*
+ * 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.Function;
+
+import javax.annotation.Nonnull;
+
+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.profile.context.navigate.OutboundMessageContextLookup;
+import org.opensaml.saml.common.binding.BindingDescriptor;
+import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that populates the outbound {@link SAMLBindingContext} for a PAOS message.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * 
+ * @post <pre>profileRequestContext.ensureOutboundMessageContext().getSubcontext(SAMLBindingContext.class).getBindingDescriptor() != null</pre>
+ */
+public class InitializePAOSBindingContext extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(InitializePAOSBindingContext.class);
+    
+    /** Lookup strategy for bindings. */
+    @Nonnull private Function<ProfileRequestContext,BindingDescriptor> bindingDescriptorLookupStrategy;
+
+    /** Strategy function for access to {@link SAMLBindingContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,SAMLBindingContext> bindingContextCreationStrategy;
+
+    /** Binding descriptor for PAOS. */
+    @NonnullBeforeExec private BindingDescriptor bindingDescriptor;    
+    
+    /** Constructor. */
+    public InitializePAOSBindingContext() {
+        bindingDescriptorLookupStrategy = FunctionSupport.constant(null);
+                
+        // Default: outbound msg context -> SAMLBindingContext
+        bindingContextCreationStrategy = new ChildContextLookup<>(SAMLBindingContext.class, true).compose(
+                new OutboundMessageContextLookup());
+    }
+    
+    /**
+     * Sets the {@link BindingDescriptor} for PAOS.
+     * 
+     * @param descriptor binding descriptor
+     */
+    public void setBindingDescriptor(@Nonnull final BindingDescriptor descriptor) {
+        checkSetterPreconditions();
+        bindingDescriptorLookupStrategy = FunctionSupport.constant(descriptor);
+    }
+    
+    /**
+     * Set lookup strategy to return the {@link BindingDescriptor} for PAOS.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setBindingDescriptorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,BindingDescriptor> strategy) {
+        checkSetterPreconditions();
+        bindingDescriptorLookupStrategy =
+                Constraint.isNotNull(strategy, "Binding descriptors lookup strategy cannot be null");
+    }
+        
+    /**
+     * Set creation strategy for {@link SAMLBindingContext} to populate.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setBindingContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,SAMLBindingContext> strategy) {
+        checkSetterPreconditions();
+        bindingContextCreationStrategy = Constraint.isNotNull(strategy,
+                "SAMLBindingContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+                
+        if (profileRequestContext.getOutboundMessageContext() == null) {
+            log.error("{} No outbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+       
+        bindingDescriptor = bindingDescriptorLookupStrategy.apply(profileRequestContext);
+        if (bindingDescriptor == null) {
+            log.error("{} No BindingDescriptor for PAOS available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        
+        final SAMLBindingContext bindingCtx = bindingContextCreationStrategy.apply(profileRequestContext);
+        if (bindingCtx == null) {
+            log.debug("{} Error creating SAMLBindingContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return;
+        }
+        
+        bindingCtx.setBindingDescriptor(bindingDescriptor);
+    }
+        
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ParseECPOptions.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ParseECPOptions.java
index ce871ec..a5918b9 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ParseECPOptions.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ParseECPOptions.java
@@ -67,7 +67,7 @@ public class ParseECPOptions extends AbstractApplicationAction {
      * 
      * @param strategy creation strategy
      */
-    public void setStateDataContextCreationStrategy(
+    public void setOptionsContextCreationStrategy(
             @Nonnull final Function<ProfileRequestContext,ECPOptionsContext> strategy) {
         checkSetterPreconditions();
         ecpOptionsContextCreationStrategy =
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareInboundMessageContext.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareInboundMessageContext.java
new file mode 100644
index 0000000..01d75ee
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareInboundMessageContext.java
@@ -0,0 +1,97 @@
+/*
+ * 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.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.slf4j.Logger;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Action that adds an inbound {@link MessageContext} and a {@link SAMLPeerEntityContext} to the
+ * {@link ProfileRequestContext} based on the identity of a relying party.
+ * 
+ * <p>This action mocks up a minimal amount of machinery on the inbound message side to drive an
+ * outbound SAML request.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ */
+public class PrepareInboundMessageContext extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PrepareInboundMessageContext.class);
+    
+    /** Lookup strategy for {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** The relying party name to base the inbound context on. */
+    @NonnullBeforeExec private String relyingPartyId;
+
+    /** Constructor. */
+    public PrepareInboundMessageContext() {
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+
+    /**
+     * Set a lookup strategy to identify the relying party.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+        relyingPartyId = rpContext != null ? rpContext.getRelyingPartyId() : null;
+
+        return relyingPartyId != null;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final MessageContext msgCtx = new MessageContext();
+        profileRequestContext.setInboundMessageContext(msgCtx);
+
+        final SAMLPeerEntityContext peerContext = msgCtx.ensureSubcontext(SAMLPeerEntityContext.class);
+        peerContext.setEntityId(relyingPartyId);
+
+        log.debug("{} Initialized inbound context for message to {}", getLogPrefix(), relyingPartyId);
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer b/sp-saml-impl/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer
new file mode 100644
index 0000000..a047636
--- /dev/null
+++ b/sp-saml-impl/src/main/resources/META-INF/services/org.opensaml.core.config.Initializer
@@ -0,0 +1 @@
+net.shibboleth.sp.saml.saml2.config.impl.XMLObjectProviderInitializer
\ 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