[java-identity-provider] branch master updated: IDP-964 - Support SAML logout propagation via SOAP

Scott Cantor cantor.2 at osu.edu
Fri Nov 16 11:58:30 EST 2018


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

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=aa717134dcaf9072d700637e19012f6481c2d9ca

The following commit(s) were added to refs/heads/master by this push:
       new  aa71713   IDP-964 - Support SAML logout propagation via SOAP
aa71713 is described below

commit aa717134dcaf9072d700637e19012f6481c2d9ca
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Nov 16 11:52:42 2018 -0500

    IDP-964 - Support SAML logout propagation via SOAP
    
    https://issues.shibboleth.net/jira/browse/IDP-964
    
    Add SOAP support in front-channel propagation.
---
 .../system/conf/relying-party-mddriven.xml         |  24 ++
 .../resources/system/conf/saml-binding-config.xml  |   8 +
 .../src/main/resources/system/conf/soap-client.xml |  15 +-
 .../flows/logout/logout-propagation-flow.xml       |   1 +
 .../flows/logout/propagation/saml2-beans.xml       |   8 +
 .../system/flows/logout/propagation/saml2-flow.xml |  39 ++-
 .../flows/saml/saml2/slo-front-abstract-beans.xml  |   2 +-
 .../ClientTLSSOAPLogoutRequestsPredicate.java      |  47 +++
 .../messaging/SignSOAPLogoutRequestsPredicate.java |  47 +++
 .../config/SingleLogoutProfileConfiguration.java   |  62 +++-
 .../saml/saml2/profile/impl/SOAPLogoutRequest.java | 328 +++++++++++++++++++++
 11 files changed, 572 insertions(+), 9 deletions(-)

diff --git a/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml b/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
index 18bca21..beacd93 100644
--- a/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
+++ b/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
@@ -421,6 +421,30 @@
                 </constructor-arg>
             </bean>
         </property>
+        <property name="signSOAPRequests">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="signSOAPRequests" />
+                </constructor-arg>
+                <constructor-arg>
+                    <bean class="org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate" />
+                </constructor-arg>
+            </bean>
+        </property>
+        <property name="clientTLSSOAPRequests">
+            <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
+                <constructor-arg>
+                    <bean parent="shibboleth.MDDrivenBoolProperty" p:propertyName="clientTLSSOAPRequests" />
+                </constructor-arg>
+                <constructor-arg>
+                    <bean parent="shibboleth.Conditions.NOT">
+                        <constructor-arg>
+                            <bean class="org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate" />
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+            </bean>
+        </property>
         <property name="signRequests">
             <bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
                 <constructor-arg>
diff --git a/idp-conf/src/main/resources/system/conf/saml-binding-config.xml b/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
index b8407f7..6ee5272 100644
--- a/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
+++ b/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
@@ -95,11 +95,19 @@
         <ref bean="shibboleth.Binding.SAML2Artifact" />
     </util:list>
 
+    <util:list id="shibboleth.OutgoingSAML2SLOFrontBindings">
+        <ref bean="shibboleth.Binding.SAML2Redirect" />
+        <ref bean="shibboleth.Binding.SAML2POST" />
+        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
+        <ref bean="shibboleth.Binding.SAML2Artifact" />
+    </util:list>
+
     <util:list id="shibboleth.OutgoingSAML2SLOBindings">
         <ref bean="shibboleth.Binding.SAML2Redirect" />
         <ref bean="shibboleth.Binding.SAML2POST" />
         <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
         <ref bean="shibboleth.Binding.SAML2Artifact" />
+        <ref bean="shibboleth.Binding.SAML2SOAP" />
     </util:list>
 
     <util:list id="shibboleth.OutgoingSOAPBindings">
diff --git a/idp-conf/src/main/resources/system/conf/soap-client.xml b/idp-conf/src/main/resources/system/conf/soap-client.xml
index 7a2c987..7db69f2 100644
--- a/idp-conf/src/main/resources/system/conf/soap-client.xml
+++ b/idp-conf/src/main/resources/system/conf/soap-client.xml
@@ -30,6 +30,7 @@
         <property name="serviceMappings">
             <props>
                 <prop key="SAML2.ArtifactDecoder">SOAPClient.SAML2.Pipeline</prop>
+                <prop key="SAML2.Logout">SOAPClient.SAML2.Pipeline</prop>
             </props>
         </property>
     </bean>
@@ -41,10 +42,12 @@
          IMPORTANT: actual instances of this bean and the encoder/decoder below MUST be prototypes, as they are per-request instance. -->
     <bean id="SOAPClient.SAML2.Pipeline.Abstract" abstract="true" class="org.opensaml.messaging.pipeline.httpclient.BasicHttpClientMessagePipeline">
         <constructor-arg>
-            <bean scope="prototype" init-method="" class="org.opensaml.saml.saml2.binding.encoding.impl.HttpClientRequestSOAP11Encoder" />
+            <bean scope="prototype" init-method=""
+                class="org.opensaml.saml.saml2.binding.encoding.impl.HttpClientRequestSOAP11Encoder" />
         </constructor-arg>
         <constructor-arg>
-            <bean scope="prototype" init-method="" class="org.opensaml.saml.saml2.binding.decoding.impl.HttpClientResponseSOAP11Decoder" />
+            <bean scope="prototype" init-method=""
+                class="org.opensaml.saml.saml2.binding.decoding.impl.HttpClientResponseSOAP11Decoder" />
         </constructor-arg>
     </bean>
     
@@ -142,8 +145,6 @@
     </bean>
     
     
-    
-    
     <!--  Supporting beans. -->
     
     <bean id="SOAPClient.OperationContextLookup" 
@@ -187,6 +188,9 @@
                 <entry key="SAML2.ArtifactDecoder">
                     <bean class="net.shibboleth.idp.saml.profile.config.logic.messaging.ClientTLSArtifactRequestsPredicate" />
                 </entry>
+                <entry key="SAML2.Logout">
+                    <bean class="net.shibboleth.idp.saml.profile.config.logic.messaging.ClientTLSSOAPLogoutRequestsPredicate" />
+                </entry>
             </map>
         </constructor-arg>
     </bean>
@@ -198,6 +202,9 @@
                 <entry key="SAML2.ArtifactDecoder">
                     <bean class="net.shibboleth.idp.saml.profile.config.logic.messaging.SignArtifactRequestsPredicate" />
                 </entry>
+                <entry key="SAML2.Logout">
+                    <bean class="net.shibboleth.idp.saml.profile.config.logic.messaging.SignSOAPLogoutRequestsPredicate" />
+                </entry>
             </map>
         </constructor-arg>
     </bean>
diff --git a/idp-conf/src/main/resources/system/flows/logout/logout-propagation-flow.xml b/idp-conf/src/main/resources/system/flows/logout/logout-propagation-flow.xml
index a38385b..8255e7a 100644
--- a/idp-conf/src/main/resources/system/flows/logout/logout-propagation-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/logout-propagation-flow.xml
@@ -34,6 +34,7 @@
 
     <end-state id="RenderResult" view="logout/result">
         <on-entry>
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutPropagationContext))" result="requestScope.logoutPropCtx" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.UserAgentContext))" result="requestScope.userAgentContext" />
             <evaluate expression="userAgentContext.isInstance(T(eu.bitwalker.useragentutils.Browser).IE)
                 ? externalContext.nativeResponse.setContentType('text/plain')
diff --git a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
index 2fd3b18..bc8bcc6 100644
--- a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-beans.xml
@@ -66,6 +66,9 @@
 
     <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
         p:fieldExtractors="#{getObject('shibboleth.PostResponseAuditExtractors') ?: getObject('shibboleth.DefaultPostResponseAuditExtractors')}" />
+
+    <bean id="PostDecodePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.PostDecodeAuditExtractors') ?: getObject('shibboleth.DefaultPostDecodeAuditExtractors')}" />
     
     <bean id="shibboleth.PreEncodeMessageHandler"
             class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
@@ -86,5 +89,10 @@
             </list>
         </property>
     </bean>
+    
+    <bean id="SOAPLogoutRequest"
+        class="net.shibboleth.idp.saml.saml2.profile.impl.SOAPLogoutRequest" scope="prototype"
+        p:SOAPClient-ref="shibboleth.SOAPClient.SAML"
+        p:SOAPPipelineName="SAML2.Logout" />
 
 </beans>
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
index d4c441b..1a2294a 100644
--- a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
@@ -22,7 +22,26 @@
         <evaluate expression="InitializeOutboundMessageContext" />
         <evaluate expression="InitializeMessageChannelSecurityContext" />
         <evaluate expression="PopulateBindingAndEndpointContexts" />
+        
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckForSynchronousBinding1" />
+    </action-state>
+    
+    <decision-state id="CheckForSynchronousBinding1">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('org.opensaml.saml.common.messaging.context.SAMLBindingContext').getBindingDescriptor().isSynchronous()"
+            then="PrepareRequest"
+            else="PopulateNonSOAPRequestSignatureSigningParameters"/>
+    </decision-state>
+    
+    <action-state id="PopulateNonSOAPRequestSignatureSigningParameters">
         <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PrepareRequest" />
+    </action-state>
+    
+    <action-state id="PrepareRequest">
         <evaluate expression="PopulateEncryptionParameters" />
 
         <evaluate expression="AddLogoutRequest" />
@@ -35,7 +54,21 @@
 
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="HandleOutboundMessage" />
+        <transition on="proceed" to="CheckForSynchronousBinding2" />
+    </action-state>
+
+    <decision-state id="CheckForSynchronousBinding2">
+        <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('org.opensaml.saml.common.messaging.context.SAMLBindingContext').getBindingDescriptor().isSynchronous()"
+            then="HandleBackChannelMessage"
+            else="HandleOutboundMessage"/>
+    </decision-state>
+
+    <action-state id="HandleBackChannelMessage">
+        <evaluate expression="SOAPLogoutRequest" />
+        <evaluate expression="PostDecodePopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="proceed" />
     </action-state>
 
     <action-state id="HandleOutboundMessage">
@@ -68,8 +101,8 @@
     </end-state>
 
     <!--
-    Unsuccessful terminal state. This is "proceed" because the parent flow will respond to that
-    by emitting the outcome in the LogoutPropagationContext, which is always left at Failure.
+    Terminal state. This is "proceed" because the parent flow will respond to that
+    by emitting the outcome in the LogoutPropagationContext, which is set appropriately.
     -->
     <end-state id="proceed"/>
 
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
index 952eb89..e789a2d 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml2/slo-front-abstract-beans.xml
@@ -30,7 +30,7 @@
         class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
         p:confidentialityActive="false" p:integrityActive="false" />
 
-    <alias name="shibboleth.OutgoingSAML2SLOBindings" alias="shibboleth.OutgoingBindings"/>
+    <alias name="shibboleth.OutgoingSAML2SLOFrontBindings" alias="shibboleth.OutgoingBindings"/>
 
     <bean id="ProcessLogoutRequest"
         class="net.shibboleth.idp.saml.saml2.profile.impl.ProcessLogoutRequest" scope="prototype"
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/ClientTLSSOAPLogoutRequestsPredicate.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/ClientTLSSOAPLogoutRequestsPredicate.java
new file mode 100644
index 0000000..6c6a5be
--- /dev/null
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/ClientTLSSOAPLogoutRequestsPredicate.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.saml.profile.config.logic.messaging;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+
+import net.shibboleth.idp.profile.config.ProfileConfiguration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.profile.logic.messaging.AbstractRelyingPartyPredicate;
+import net.shibboleth.idp.saml.saml2.profile.config.SingleLogoutProfileConfiguration;
+
+/** A predicate implementation that forwards to 
+ * {@link SingleLogoutProfileConfiguration#getClientTLSSOAPRequests()}. */
+public class ClientTLSSOAPLogoutRequestsPredicate extends AbstractRelyingPartyPredicate {
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean apply(@Nullable final MessageContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc != null && pc instanceof SingleLogoutProfileConfiguration) {
+                return ((SingleLogoutProfileConfiguration) pc).getClientTLSSOAPRequests().apply(input);
+            }
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/SignSOAPLogoutRequestsPredicate.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/SignSOAPLogoutRequestsPredicate.java
new file mode 100644
index 0000000..888a006
--- /dev/null
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/logic/messaging/SignSOAPLogoutRequestsPredicate.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.saml.profile.config.logic.messaging;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+
+import net.shibboleth.idp.profile.config.ProfileConfiguration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.profile.logic.messaging.AbstractRelyingPartyPredicate;
+import net.shibboleth.idp.saml.saml2.profile.config.SingleLogoutProfileConfiguration;
+
+/** A predicate implementation that forwards to 
+ * {@link SingleLogoutProfileConfiguration#getSignSOAPRequests()}. */
+public class SignSOAPLogoutRequestsPredicate extends AbstractRelyingPartyPredicate {
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean apply(@Nullable final MessageContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc != null && pc instanceof SingleLogoutProfileConfiguration) {
+                return ((SingleLogoutProfileConfiguration) pc).getSignSOAPRequests().apply(input);
+            }
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
index fe515f8..fe4c7f5 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
@@ -23,15 +23,19 @@ import java.util.Collections;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.logic.NoConfidentialityMessageChannelPredicate;
 import org.opensaml.profile.logic.NoIntegrityMessageChannelPredicate;
 
 import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 /** Configuration support for SAML 2 Single Logout. */
@@ -40,10 +44,16 @@ public class SingleLogoutProfileConfiguration extends AbstractSAML2ArtifactAware
     /** ID for this profile configuration. */
     public static final String PROFILE_ID = "http://shibboleth.net/ns/profiles/saml2/logout";
 
+    /** Predicate used to determine if SOAP-based requests should be signed. */
+    @Nonnull private Predicate<MessageContext> signSOAPRequestsPredicate;
+    
+    /** Predicate used to determine if SOAP-based requests should use client TLS. */
+    @Nonnull private Predicate<MessageContext> clientTLSSOAPRequestsPredicate;
+    
     /** Lookup function to supply {@link #qualifiedNameIDFormats} property. */
     @Nullable private Function<ProfileRequestContext,Collection<String>> qualifiedNameIDFormatsLookupStrategy;
     
-    /** NameID formats whose matching rules accomodate defaulted qualifiers. */
+    /** NameID formats whose matching rules accommodate defaulted qualifiers. */
     @Nonnull @NonnullElements private Collection<String> qualifiedNameIDFormats;
     
     /** Constructor. */
@@ -62,10 +72,60 @@ public class SingleLogoutProfileConfiguration extends AbstractSAML2ArtifactAware
         setSignResponses(new NoIntegrityMessageChannelPredicate());
         setEncryptNameIDs(new NoConfidentialityMessageChannelPredicate());
 
+        signSOAPRequestsPredicate = new org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate();
+        clientTLSSOAPRequestsPredicate =
+                Predicates.not(new org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate());
+        
         qualifiedNameIDFormats = Collections.emptyList();
     }
 
     /**
+     * Get the predicate used to determine if SOAP-based requests should be signed.
+     * 
+     * @return predicate used to determine if SOAP-based requests should be signed
+     * 
+     * @since 4.0.0
+     */
+    @Nonnull public Predicate<MessageContext> getSignSOAPRequests() {
+        return signSOAPRequestsPredicate;
+    }
+    
+    /**
+     * Set the predicate used to determine if SOAP-based requests should be signed.
+     * 
+     * @param predicate the predicate
+     * 
+     * @since 4.0.0
+     */
+    public void setSignSOAPRequests(@Nonnull final Predicate<MessageContext> predicate) {
+        signSOAPRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine SOAP-based signing cannot be null");
+    }
+
+    /**
+     * Get the predicate used to determine if SOAP-based requests should use client TLS.
+     * 
+     * @return predicate used to determine if SOAP-based requests should use client TLS
+     * 
+     * @since 4.0.0
+     */
+    @Nonnull public Predicate<MessageContext> getClientTLSSOAPRequests() {
+        return clientTLSSOAPRequestsPredicate;
+    }
+    
+    /**
+     * Set the predicate used to determine if SOAP-based requests should use client TLS.
+     * 
+     * @param predicate the predicate
+     * 
+     * @since 4.0.0
+     */
+    public void setClientTLSSOAPRequests(@Nonnull final Predicate<MessageContext> predicate) {
+        clientTLSSOAPRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine SOAP-based client TLS use cannot be null");
+    }
+    
+    /**
      * Get a collection of {@link org.opensaml.saml.saml2.core.NameID} Format values for which the use of
      * the NameQualifier and SPNameQualifier attributes is defined to allow default/implicit values
      * derived from the asserting and relying parties.
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java
new file mode 100644
index 0000000..8f1defc
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SOAPLogoutRequest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.saml.saml2.profile.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.idp.session.context.LogoutPropagationContext.Result;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+import org.opensaml.messaging.MessageException;
+import org.opensaml.messaging.context.InOutOperationContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.MessageLookup;
+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.messaging.context.SAMLBindingContext;
+import org.opensaml.saml.common.messaging.context.SAMLEndpointContext;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.common.messaging.soap.SAMLSOAPClientContextBuilder;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
+import org.opensaml.saml.saml2.core.Status;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.security.SecurityException;
+import org.opensaml.soap.client.SOAPClient;
+import org.opensaml.soap.client.http.PipelineFactoryHttpSOAPClient;
+import org.opensaml.soap.common.SOAPException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+/**
+ * Profile action that propagates a prepared {@link LogoutRequest} message to an SP via the SOAP
+ * binding, encapsulating SOAP pipeline construction and execution.
+ * 
+ * <p>The outbound message is pulled from the {@link ProfileRequestContext} to allow the surrounding
+ * flow to remain largely SOAP-unaware.</p>
+ * 
+ * <p>Success or failure is reflected in a {@link LogoutPropagationContext} accessed via a lookup
+ * strategy.</p>
+ * 
+ * <p>The response message is also stored off in the inbound message context.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#IO_ERROR}
+ * 
+ * @post {@link LogoutPropagationContext#getResult()} reflects the status of the logout attempt.
+ * @post profileRequestContext.getInboundMessageContext().getMessage() is populated if a response is obtained.
+ * 
+ * @since 4.0.0
+ */
+public class SOAPLogoutRequest extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SOAPLogoutRequest.class);
+        
+    /** Lookup strategy for {@link LogoutRequest} to process. */
+    @Nonnull private Function<ProfileRequestContext,LogoutRequest> logoutRequestLookupStrategy;
+
+    /** Lookup strategy for context in which to report result. */
+    @Nonnull private Function<ProfileRequestContext,LogoutPropagationContext> propagationContextLookupStrategy;
+    
+    /** Strategy function for access to {@link SAMLMetadataContext} for input to SOAP client. */
+    @Nonnull private Function<ProfileRequestContext,SAMLMetadataContext> metadataContextLookupStrategy;
+
+    /** Strategy function for access to {@link SAMLEndpointContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,SAMLEndpointContext> endpointContextLookupStrategy;
+    
+    /** SOAP client. */
+    @NonnullAfterInit private SOAPClient soapClient;
+    
+    /** The SOAP client message pipeline name. */
+    @Nullable @NotEmpty private String soapPipelineName;
+    
+    /** LogoutRequest to process. */
+    @Nullable private LogoutRequest logoutRequest;
+    
+    /** LogoutPropagationContext. */
+    @Nullable private LogoutPropagationContext propagationContext;
+    
+    /** Optional metadata for use in SOAP client. */
+    @Nullable private SAMLMetadataContext mdContext;
+    
+    /** Endpoint context to determine destination address. */
+    @Nullable private SAMLEndpointContext epContext;
+    
+    /** Constructor. */
+    public SOAPLogoutRequest() {
+        
+        logoutRequestLookupStrategy = Functions.compose(new MessageLookup<>(LogoutRequest.class),
+                new OutboundMessageContextLookup());
+
+        propagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+        
+        // Default: outbound msg context -> SAMLPeerEntityContext -> SAMLMetadataContext
+        metadataContextLookupStrategy = Functions.compose(
+                new ChildContextLookup<>(SAMLMetadataContext.class),
+                Functions.compose(new ChildContextLookup<>(SAMLPeerEntityContext.class),
+                        new OutboundMessageContextLookup()));
+
+        // Default: outbound msg context -> SAMLPeerEntityContext -> SAMLEndpointContext
+        endpointContextLookupStrategy = Functions.compose(
+                new ChildContextLookup<>(SAMLEndpointContext.class, true),
+                Functions.compose(new ChildContextLookup<>(SAMLPeerEntityContext.class, true),
+                        new OutboundMessageContextLookup()));
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link LogoutRequest} to send.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setLogoutRequestLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,LogoutRequest> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        logoutRequestLookupStrategy = Constraint.isNotNull(strategy, "LogoutRequest lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the lookup strategy for the {@link LogoutPropagationContext} to update.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setPropagationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,LogoutPropagationContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        propagationContextLookupStrategy =
+                Constraint.isNotNull(strategy, "LogoutPropagationContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set lookup strategy for {@link SAMLMetadataContext} for input to SOAP client.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SAMLMetadataContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        metadataContextLookupStrategy = Constraint.isNotNull(strategy,
+                "SAMLMetadataContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set lookup strategy for {@link SAMLEndpointContext} to read from.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setEndpointContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SAMLEndpointContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        endpointContextLookupStrategy = Constraint.isNotNull(strategy,
+                "SAMLEndpointContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the SOAP client instance.
+     * 
+     * @param client the SOAP client
+     */
+    public void setSOAPClient(@Nonnull final SOAPClient client) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        soapClient = Constraint.isNotNull(client, "SOAPClient cannot be null");
+    }
+    
+    /**
+     * Set the name of the specific SOAP client message pipeline to use, 
+     * for example with {@link PipelineFactoryHttpSOAPClient}.
+     * 
+     * @param name the pipeline name, or null
+     */
+    public void setSOAPPipelineName(@Nullable @NotEmpty final String name) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        soapPipelineName = StringSupport.trimOrNull(name);
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (soapClient == null) {
+            throw new ComponentInitializationException("SOAPClient cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        propagationContext = propagationContextLookupStrategy.apply(profileRequestContext);
+        if (propagationContext == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        logoutRequest = logoutRequestLookupStrategy.apply(profileRequestContext);
+        if (logoutRequest == null) {
+            log.warn("{} No LogoutRequest found to process", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        epContext = endpointContextLookupStrategy.apply(profileRequestContext);
+        if (epContext == null || epContext.getEndpoint() == null || epContext.getEndpoint().getLocation() == null) {
+            log.warn("{} No destination endpoint found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        mdContext = metadataContextLookupStrategy.apply(profileRequestContext);
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        try {
+            final InOutOperationContext<LogoutResponse,LogoutRequest> opContext = new SAMLSOAPClientContextBuilder()
+                    .setOutboundMessage(logoutRequest)
+                    .setProtocol(SAMLConstants.SAML20P_NS)
+                    .setPipelineName(soapPipelineName)
+                    .setSecurityConfigurationProfileId(profileRequestContext.getProfileId())
+                    .setPeerRoleDescriptor(mdContext != null ? mdContext.getRoleDescriptor() : null)
+                    .build();
+            
+            logoutRequest.setDestination(epContext.getEndpoint().getLocation());
+        
+            log.debug("{} Executing LogoutRequest over SOAP 1.1 binding to endpoint: {}", getLogPrefix(),
+                    logoutRequest.getDestination());
+            
+            soapClient.send(logoutRequest.getDestination(), opContext);
+            final LogoutResponse response = opContext.getInboundMessageContext().getMessage();
+            
+            if (response == null) {
+                throw new MessageException("No response message received");
+            }
+            
+            // Store off message so audit extraction works.
+            // Also mock/copy SAMLBindingContext for the same reason (it's SOAP in both directions).
+            profileRequestContext.getInboundMessageContext().setMessage(response);
+            final SAMLBindingContext bctx =
+                    profileRequestContext.getInboundMessageContext().getSubcontext(SAMLBindingContext.class, true);
+            bctx.setBindingDescriptor(
+                    profileRequestContext.getOutboundMessageContext().getSubcontext(
+                            SAMLBindingContext.class).getBindingDescriptor());
+            
+            log.debug("{} Processing LogoutResponse received via SOAP 1.1 binding from endpoint: {}", getLogPrefix(),
+                    logoutRequest.getDestination());
+            handleResponse(profileRequestContext, response);
+        } catch (final ClassCastException e) {
+            log.warn("{} SOAP message payload was not an instance of LogoutResponse", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+        } catch (final MessageException | SOAPException | SecurityException e) {
+            log.warn("{} SOAP logout request failed", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+        }
+    }
+
+    /**
+     * Turn status from response into an appropriate result.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param response message to examine
+     */
+    private void handleResponse(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final LogoutResponse response) {
+        final Status status = response.getStatus();
+        if (status != null) {
+            final StatusCode code = status.getStatusCode();
+            if (code != null) {
+                if (StatusCode.SUCCESS.equals(code.getValue())) {
+                    log.debug("{} LogoutResponse was successful", getLogPrefix());
+                    propagationContext.setResult(Result.Success);
+                    return;
+                }
+                log.warn("{} LogoutResponse received with status code '{}'", getLogPrefix(), code.getValue());
+            } else {
+                log.warn("{} LogoutResponse received with no status code", getLogPrefix());
+            }
+        } else {
+            log.warn("{} LogoutResponse received with no status", getLogPrefix());
+        }
+    }
+    
+}
\ 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