[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