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

Codeberg noreply at shibboleth.net
Mon Aug 10 19:01:02 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/ae7f020818a0ba54a37568f711ef6ad029a6a550

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

commit ae7f020818a0ba54a37568f711ef6ad029a6a550
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon Aug 10 15:00:43 2026 -0400

    Lightly tested ECP consumer flow.
---
 sp-saml-conf-impl/pom.xml                          |  10 ++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  20 +++
 .../idp/flows/sp/consumer/saml2/ecp/ecp-beans.xml  |  23 +++
 .../idp/flows/sp/consumer/saml2/ecp/ecp-flow.xml   |   9 ++
 .../idp/flows/sp/initiator/ecp/ecp-beans.xml       |  11 --
 .../shibboleth/idp/flows/sp/saml2-common-beans.xml |  13 +-
 .../net/shibboleth/sp/service/agent/postconfig.xml |   2 +-
 .../flows/saml2/SAML2TokenConsumerFlowTest.java    | 112 +++++++++++++-
 .../binding/decoding/impl/HTTPECPDecoder.java      | 165 +++++++++++++++++++++
 9 files changed, 350 insertions(+), 15 deletions(-)

diff --git a/sp-saml-conf-impl/pom.xml b/sp-saml-conf-impl/pom.xml
index a298232..644b0f5 100644
--- a/sp-saml-conf-impl/pom.xml
+++ b/sp-saml-conf-impl/pom.xml
@@ -116,6 +116,16 @@
             <artifactId>opensaml-saml-impl</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-soap-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-soap-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-storage-impl</artifactId>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index a321b72..f2f1450 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -30,6 +30,26 @@
 
     <!-- Token consumer descriptors for each SAML binding. -->
     
+    <bean p:id="sp/consumer/saml2/ecp" class="net.shibboleth.sp.profile.BasicTokenConsumerFlowDescriptor">
+        <property name="activationCondition">
+            <bean class="net.shibboleth.sp.profile.context.logic.HttpServletRequestPredicate"
+                    p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier">
+                <property name="httpServletRequestValidator">
+                    <bean class="net.shibboleth.shared.servlet.impl.ChainingHttpServletRequestValidator">
+                        <property name="validators">
+                            <list>
+                                <bean class="net.shibboleth.shared.servlet.impl.BasicHttpServletRequestMethodValidator"
+                                    p:allowedMethods="POST" />
+                                <bean class="net.shibboleth.shared.servlet.impl.BasicHttpServletRequestContentTypeValidator"
+                                    p:allowedContentTypes="application/vnd.paos+xml" />
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+    
     <bean p:id="sp/consumer/saml2/post" class="net.shibboleth.sp.profile.BasicTokenConsumerFlowDescriptor">
         <property name="activationCondition">
             <bean class="net.shibboleth.sp.profile.context.logic.HttpServletRequestPredicate"
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-beans.xml
new file mode 100644
index 0000000..3b91103
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-beans.xml
@@ -0,0 +1,23 @@
+<?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">
+
+    <bean id="shibboleth.HttpServletRequestValidator" class="net.shibboleth.shared.servlet.impl.NoOpHttpServletRequestValidator" />
+
+    <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
+            p:messageType="org.opensaml.saml.saml2.core.Response">
+        <constructor-arg>
+            <bean class="net.shibboleth.sp.saml.saml2.binding.decoding.impl.HTTPECPDecoder" scope="prototype"
+                p:checkDuringInit="false"
+                p:parserPool-ref="shibboleth.ParserPool"
+                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
+                p:bindingDescriptor-ref="shibboleth.Binding.SAML2PAOS" />
+        </constructor-arg>
+    </bean>
+
+</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-flow.xml
new file mode 100644
index 0000000..9bed995
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/ecp/ecp-flow.xml
@@ -0,0 +1,9 @@
+<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/consumer/saml2">
+
+    <!-- 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/consumer/saml2/ecp/ecp-beans.xml" />
+
+</flow>
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
index 1cbd99a..904bd66 100644
--- 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
@@ -133,15 +133,4 @@
     <bean id="EncodeSOAPFault" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
         p:messageEncoderFactory-ref="messageEncoderFactory" />
 
-    <!-- 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/saml2-common-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
index b564d24..09ae6c2 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
@@ -10,7 +10,7 @@
     <bean id="ValidateHttpServletRequest" class="net.shibboleth.sp.profile.impl.ValidateHttpServletRequest" scope="prototype"
         p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
         p:checkDuringInit="false"
-        p:validator="#{getObject('shibboleth.HttpServletRequestValidator') ?: null}" />
+        p:validator="#{getObject('shibboleth.HttpServletRequestValidator')}" />
 
     <bean id="InboundResponsePopulateAuditContext" parent="shibboleth.sp.AbstractPopulateAuditContext"
             p:fieldExtractors="#{getObject('shibboleth.sp.saml.InboundResponseAuditExtractors') ?: getObject('shibboleth.sp.saml.DefaultInboundResponseAuditExtractors')}">
@@ -239,5 +239,16 @@
           p:velocityEngine-ref="shibboleth.VelocityEngine"
           p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
           p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+    <!-- Add PAOS binding material for ECP. -->
+
+    <bean id="shibboleth.Encoders.SAML2PAOSEncoder"
+          class="net.shibboleth.sp.saml.saml2.binding.encoding.impl.HTTPPAOSEncoder" scope="prototype" init-method=""
+          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
+
+    <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" />
     
 </beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index 26178ab..3b6fa25 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -23,7 +23,7 @@
             p:metadataResolver-ref="shibboleth.MetadataResolverService"
             p:sessionInitiators="#{{ 'ecp', 'saml2' }}"
             p:logoutInitiators="saml2"
-            p:tokenConsumers="#{{ 'saml2/post', 'saml2/post-simplesign', 'saml2/artifact' }}"
+            p:tokenConsumers="#{{'saml2/ecp', 'saml2/post', 'saml2/post-simplesign', 'saml2/artifact' }}"
             p:logoutConsumers="#{{ 'saml2/redirect', 'saml2/post', 'saml2/post-simplesign', 'saml2/artifact' }}">
         <property name="id">
             <util:constant static-field="net.shibboleth.sp.saml.saml2.SAML2ProtocolSupportService.PROTOCOL_ID" />
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
index d298efb..d4bc520 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
@@ -24,6 +24,8 @@ import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.core.xml.XMLObjectBuilder;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
 import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.io.UnmarshallingException;
 import org.opensaml.core.xml.util.XMLObjectSupport;
@@ -44,9 +46,13 @@ import org.opensaml.saml.saml2.core.Subject;
 import org.opensaml.saml.saml2.core.SubjectConfirmation;
 import org.opensaml.saml.saml2.core.SubjectConfirmationData;
 import org.opensaml.saml.saml2.core.SubjectLocality;
+import org.opensaml.saml.saml2.ecp.RelayState;
 import org.opensaml.saml.saml2.testing.SAML2ActionTestingSupport;
 import org.opensaml.security.SecurityException;
 import org.opensaml.security.credential.Credential;
+import org.opensaml.soap.soap11.Body;
+import org.opensaml.soap.soap11.Envelope;
+import org.opensaml.soap.soap11.Header;
 import org.opensaml.xmlsec.SignatureSigningParameters;
 import org.opensaml.xmlsec.signature.SignableXMLObject;
 import org.opensaml.xmlsec.signature.support.SignatureConstants;
@@ -104,6 +110,7 @@ import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 import net.shibboleth.sp.profile.ConsumerConstants;
 import net.shibboleth.sp.saml.saml2.SAMLStateData;
 import net.shibboleth.sp.saml.saml2.SessionDataSupport;
+import net.shibboleth.sp.saml.saml2.profile.SAML2InitiatorConstants;
 import net.shibboleth.sp.saml.saml2.profile.impl.PrepareAgentResponse;
 import net.shibboleth.sp.state.StateData;
 import net.shibboleth.sp.state.impl.CookieStateManager;
@@ -135,7 +142,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
     @Nonnull public static final String AUDIENCE = "https://testsp.example.org";
 
     /** ACS URL. */
-    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/SAML2/POST";
+    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/Validate";
 
     /** Target resource. */
     @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
@@ -767,6 +774,29 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
                 RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
     }
 
+    /**
+     * Test successful ECP flow.
+     * 
+     * @throws Exception 
+     */
+    @Test
+    public void testSuccessECP() throws Exception {
+        final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
+        sign(response);
+        
+        final DDF input = buildRemotedECPResponse(buildECPResponse(response, null), null);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("testSuccess output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.singleton("mail"),
+                RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
+    }
+    
     /**
      * Decode an encoded response and run sanity checks against it.
      * 
@@ -920,6 +950,43 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
         return response;
     }
     
+    /**
+     * Constructs a SOAP Envelope for ECP around a response and state token.
+     * 
+     * @param response the response to embed
+     * @param relayState state token
+     * 
+     * @return SOAP envelope
+     */
+    @Nonnull private Envelope buildECPResponse(@Nonnull final Response response, @Nullable final String relayState) {
+        final XMLObjectBuilder<Envelope> envBuilder =
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<Envelope>ensureBuilder(Envelope.DEFAULT_ELEMENT_NAME);
+        final XMLObjectBuilder<Body> bodyBuilder =
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<Body>ensureBuilder(Body.DEFAULT_ELEMENT_NAME);
+        
+        final Envelope env = envBuilder.buildObject(Envelope.DEFAULT_ELEMENT_NAME);
+        
+        final Body body = bodyBuilder.buildObject(Body.DEFAULT_ELEMENT_NAME);
+        body.getUnknownXMLObjects().add(response);
+        env.setBody(body);
+        
+        if (relayState != null) {
+            final XMLObjectBuilder<Header> headerBuilder =
+                    XMLObjectProviderRegistrySupport.getBuilderFactory().<Header>ensureBuilder(Header.DEFAULT_ELEMENT_NAME);
+            final XMLObjectBuilder<RelayState> stateBuilder =
+                    XMLObjectProviderRegistrySupport.getBuilderFactory().<RelayState>ensureBuilder(RelayState.DEFAULT_ELEMENT_NAME);
+
+            final Header header = headerBuilder.buildObject(Header.DEFAULT_ELEMENT_NAME);
+            env.setHeader(header);
+            
+            final RelayState stateHeader = stateBuilder.buildObject(RelayState.DEFAULT_ELEMENT_NAME);
+            stateHeader.setValue(relayState);
+            header.getUnknownXMLObjects().add(stateHeader);
+        }
+        
+        return env;
+    }
+    
     /**
      * Encodes a SAML response into a form POST embedded in a remoted message.
      * 
@@ -948,7 +1015,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
             http.addmember(RemotedHttpServletRequest.METHOD).string("POST");
             http.addmember(RemotedHttpServletRequest.REMOTE_ADDR).string("192.168.1.1");
             http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
-            http.addmember(RemotedHttpServletRequest.REQUEST_URI).unsafe_string("/Shibboleth.sso/SAML2/POST".getBytes(StandardCharsets.UTF_8));
+            http.addmember(RemotedHttpServletRequest.REQUEST_URI).unsafe_string("/Shibboleth.sso/Validate".getBytes(StandardCharsets.UTF_8));
             
             if (stateCookie != null) {
                 http.addmember("headers").structure().addmember("Cookie")
@@ -972,6 +1039,47 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
         }
     }
 
+    /**
+     * Encodes a SAML response into a form POST embedded in a remoted message.
+     * 
+     * @param envelope SOAP envelope
+     * @param stateCookie the state cookie to attack to the input message if any
+     * 
+     * @return input object suitable for token consumer flow
+     * 
+     * @throws IOException on error 
+     */
+    @Nonnull private DDF buildRemotedECPResponse(@Nonnull final Envelope envelope, @Nullable Cookie stateCookie)
+            throws IOException {
+                
+        try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
+            XMLObjectSupport.marshallToOutputStream(envelope, sink);
+
+            final DDF obj = new DDF(null).structure();
+            obj.addmember(ConsumerConstants.HOME_URL).unsafe_string(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));            
+            final DDF http = obj.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+            
+            http.addmember(RemotedHttpServletRequest.SCHEME).string("https");
+            http.addmember(RemotedHttpServletRequest.PORT).integer(443);
+            http.addmember(RemotedHttpServletRequest.HOSTNAME).unsafe_string("sp.example.org".getBytes(StandardCharsets.UTF_8));
+            http.addmember(RemotedHttpServletRequest.METHOD).string("POST");
+            http.addmember(RemotedHttpServletRequest.REMOTE_ADDR).string("192.168.1.1");
+            http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string(SAML2InitiatorConstants.PAOS_MEDIA_TYPE);
+            http.addmember(RemotedHttpServletRequest.REQUEST_URI).unsafe_string("/Shibboleth.sso/Validate".getBytes(StandardCharsets.UTF_8));
+            
+            if (stateCookie != null) {
+                http.addmember("headers").structure().addmember("Cookie")
+                    .unsafe_string(new String(stateCookie.getName() + '=' + stateCookie.getValue()).getBytes(StandardCharsets.UTF_8));
+            }
+            
+            http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(sink.toByteArray());
+            
+            return obj;
+        } catch (final MarshallingException e) {
+            throw new IOException(e);
+        }
+    }
+    
     /**
      * Sign object.
      * 
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/binding/decoding/impl/HTTPECPDecoder.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/binding/decoding/impl/HTTPECPDecoder.java
new file mode 100644
index 0000000..47eb00f
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/binding/decoding/impl/HTTPECPDecoder.java
@@ -0,0 +1,165 @@
+/*
+ * 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.binding.decoding.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.messaging.handler.MessageHandler;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.binding.BindingDescriptor;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.common.binding.decoding.SAMLMessageDecoder;
+import org.opensaml.saml.common.binding.impl.SAMLSOAPDecoderBodyHandler;
+import org.opensaml.saml.common.messaging.context.SAMLBindingContext;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.ecp.RelayState;
+import org.opensaml.soap.messaging.context.SOAP11Context;
+import org.opensaml.soap.soap11.Envelope;
+import org.opensaml.soap.soap11.Header;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.saml.saml2.profile.SAML2InitiatorConstants;
+
+/**
+ * SAML 2.0 ECP/PAOS Decoder.
+ */
+public class HTTPECPDecoder extends org.opensaml.soap.soap11.decoder.http.impl.HTTPSOAP11Decoder 
+        implements SAMLMessageDecoder {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPECPDecoder.class);
+    
+    /** Optional {@link BindingDescriptor} to inject into {@link SAMLBindingContext} created. */
+    @Nullable private BindingDescriptor bindingDescriptor;
+    
+    /** Constructor. */
+    public HTTPECPDecoder() {
+        setProtocolMessageLoggerSubCategory("SAML");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getBindingURI() {
+        return SAMLConstants.SAML2_PAOS_BINDING_URI;
+    }
+
+    /**
+     * Get an optional {@link BindingDescriptor} to inject into {@link SAMLBindingContext} created.
+     * 
+     * @return binding descriptor
+     */
+    @Nullable public BindingDescriptor getBindingDescriptor() {
+        return bindingDescriptor;
+    }
+    
+    /**
+     * Set an optional {@link BindingDescriptor} to inject into {@link SAMLBindingContext} created.
+     * 
+     * @param descriptor a binding descriptor
+     */
+    public void setBindingDescriptor(@Nullable final BindingDescriptor descriptor) {
+        bindingDescriptor = descriptor;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        
+        // Need to set this before calling base class.
+        if (getBodyHandler() == null) {
+            final MessageHandler handler = new SAMLSOAPDecoderBodyHandler();
+            handler.initialize();
+            setBodyHandler(handler);
+        }
+        
+        super.doInitialize();
+    }
+    
+    /** {@inheritDoc} */
+    protected void doDecode() throws MessageDecodingException {
+        super.doDecode();
+        
+        final MessageContext msgCtx = getMessageContext();
+        assert msgCtx != null;
+        
+        populateBindingContext(msgCtx);
+        
+        processSOAPHeaders(msgCtx);
+        
+        final Object samlMessage = msgCtx.getMessage();
+        if (samlMessage instanceof SAMLObject) {
+            log.debug("Decoded SOAP message which included SAML message of type {}",
+                    ((SAMLObject) samlMessage).getElementQName());
+        } else {
+            throw new MessageDecodingException("Decoded SOAP message did not contain SAMLObject");
+        }
+    }
+    
+    /**
+     * Populate the context which carries information specific to this binding.
+     * 
+     * @param messageContext the current message context
+     */
+    protected void populateBindingContext(@Nonnull final MessageContext messageContext) {
+        final SAMLBindingContext bindingContext = messageContext.ensureSubcontext(SAMLBindingContext.class);
+        bindingContext.setBindingUri(getBindingURI());
+        bindingContext.setBindingDescriptor(bindingDescriptor);
+        bindingContext.setHasBindingSignature(false);
+        bindingContext.setIntendedDestinationEndpointURIRequired(true);
+    }
+    
+    /**
+     * Process ECP/PAOS headers.
+     * 
+     * @param messageContext message context
+     */
+    protected void processSOAPHeaders(@Nonnull final MessageContext messageContext) {
+        final SOAP11Context soapContext = messageContext.ensureSubcontext(SOAP11Context.class);
+        final Envelope env = soapContext.getEnvelope();
+        final Header header = env != null ? env.getHeader() : null;
+        if (header != null) {
+            final List<XMLObject> relayState = header.getUnknownXMLObjects(RelayState.DEFAULT_ELEMENT_NAME);
+            if (relayState != null && relayState.size() > 0) {
+                SAMLBindingSupport.setRelayState(messageContext, ((RelayState) relayState.get(0)).getValue());
+            }
+            
+            // We're ignoring the PAOS Response header if it exists, as the correlation at the SAML layer is
+            // applied later.
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void validateHttpRequest(@Nonnull final HttpServletRequest request) throws MessageDecodingException {
+        
+        // Replace usual SOAP type check.
+        if (!SAML2InitiatorConstants.PAOS_MEDIA_TYPE.equals(request.getContentType())) {
+            log.warn("Saw unsupported request Content-Type: {}", request.getContentType());
+            throw new MessageDecodingException(
+                    String.format("Content-Type '%s' was not a supported media type", request.getContentType()));
+        }
+ 
+    }
+    
+}
\ 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