[java-identity-provider] branch main updated: IDP-1829 - Add option to disable outbound logout

Scott Cantor cantor.2 at osu.edu
Fri Jun 11 18:03:59 UTC 2021


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

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

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

The following commit(s) were added to refs/heads/main by this push:
       new  aac60b278 IDP-1829 - Add option to disable outbound logout
aac60b278 is described below

commit aac60b27814aae9830f741a7f96cadf38403dde8
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Jun 11 14:03:54 2021 -0400

    IDP-1829 - Add option to disable outbound logout
    
    https://issues.shibboleth.net/jira/browse/IDP-1829
    
    Option added to treat lack of endpoints as async.
---
 .../flows/saml/saml2/slo-front-abstract-beans.xml  |   4 +
 .../flows/saml/saml2/slo-front-abstract-flow.xml   |  20 ++-
 idp-conf/src/main/resources/conf/idp.properties    |   3 +
 idp-conf/src/test/resources/conf/idp.properties    |   3 +
 .../profile/impl/PreProcessLogoutMessage.java      | 171 +++++++++++++++++++++
 .../saml2/profile/impl/ProcessLogoutRequest.java   |   6 -
 .../profile/impl/PreProcessLogoutMessageTest.java  | 104 +++++++++++++
 7 files changed, 294 insertions(+), 17 deletions(-)

diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-beans.xml
index 5909d9823..5ee862cd8 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-beans.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-beans.xml
@@ -28,6 +28,10 @@
         p:loggingId="%{idp.service.logging.saml2slo:Logout}"
         p:browserProfile="true" />
 
+    <bean id="PreProcessLogoutMessage"
+        class="net.shibboleth.idp.saml.saml2.profile.impl.PreProcessLogoutMessage" scope="prototype"
+        p:assumeAsynchronousLogout="%{idp.logout.assumeAsync:false}" />
+
     <bean id="InitializeMessageChannelSecurityContext"
         class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
         p:confidentialityActive="false" p:integrityActive="false" />
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-flow.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-flow.xml
index 9a290019b..95813d4d6 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-flow.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/saml/saml2/slo-front-abstract-flow.xml
@@ -25,19 +25,18 @@
     passes control back to the next parent state. 
     -->
     <subflow-state id="DoInboundInterceptSubflow">
-        <transition on="proceed" to="CheckLogoutResponse" />
+        <transition on="proceed" to="PreProcessLogoutMessage" />
     </subflow-state>
     
-    <decision-state id="CheckLogoutResponse">
-        <if test="opensamlProfileRequestContext.getInboundMessageContext().getMessage() instanceof T(org.opensaml.saml.saml2.core.LogoutResponse)"
-            then="LogoutResponseView" else="CheckAsync1"/>
-    </decision-state>
+    <!-- Tri-decision state. -->
+    <action-state id="PreProcessLogoutMessage">
+        <evaluate expression="PreProcessLogoutMessage" />
+        <evaluate expression="'proceed'" />
 
-    <decision-state id="CheckAsync1">
-        <!-- If parent DoInboundInterceptSubflow transition changes on 'proceed', the else clause needs to also change. -->
-        <if test="opensamlProfileRequestContext.getInboundMessageContext().getMessage() instanceof T(org.opensaml.saml.saml2.core.LogoutRequest) and opensamlProfileRequestContext.getInboundMessageContext().getMessage().getExtensions() != null and !opensamlProfileRequestContext.getInboundMessageContext().getMessage().getExtensions().getUnknownXMLObjects(T(org.opensaml.saml.ext.saml2aslo.Asynchronous).DEFAULT_ELEMENT_NAME).isEmpty()"
-            then="AsyncDecryptionParameters" else="OutboundContextsAndSecurityParameters" />
-    </decision-state>
+        <transition on="proceed" to="OutboundContextsAndSecurityParameters" />
+        <transition on="IsLogoutResponse" to="LogoutResponseView" />
+        <transition on="IsLogoutRequestAsync" to="AsyncDecryptionParameters" />
+    </action-state>
     
     <!-- This is the one action inside "OutboundContextsAndSecurityParameters" that we can't skip. -->
     <action-state id="AsyncDecryptionParameters">
@@ -46,7 +45,6 @@
         
         <transition on="proceed" to="ExtractSubject" />
     </action-state>
-    
 
     <!-- LogoutRequest handling starts by loading client storage if needed. -->
         
diff --git a/idp-conf/src/main/resources/conf/idp.properties b/idp-conf/src/main/resources/conf/idp.properties
index 7064c18d8..8b5d71a35 100644
--- a/idp-conf/src/main/resources/conf/idp.properties
+++ b/idp-conf/src/main/resources/conf/idp.properties
@@ -182,6 +182,9 @@ idp.bindings.inMetadataOrder = false
 # Whether to require logout requests/responses be signed/authenticated.
 #idp.logout.authenticated = true
 
+# Whether to handle logout lacking response endpoonts as asynchronous.
+#idp.logout.assumeAsync = false
+
 # Whether to hide logout propagation status reporting.
 #idp.logout.propagationHidden = false
 
diff --git a/idp-conf/src/test/resources/conf/idp.properties b/idp-conf/src/test/resources/conf/idp.properties
index c6dd2a22b..37c1297f2 100644
--- a/idp-conf/src/test/resources/conf/idp.properties
+++ b/idp-conf/src/test/resources/conf/idp.properties
@@ -163,6 +163,9 @@ idp.session.secondaryServiceIndex = true
 # Whether to require logout requests/responses be signed/authenticated.
 #idp.logout.authenticated = true
 
+# Whether to handle logout lacking response endpoonts as asynchronous.
+idp.logout.assumeAsync = true
+
 # Whether to hide logout propagation status reporting.
 #idp.logout.propagationHidden = false
 
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessage.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessage.java
new file mode 100644
index 000000000..27c16cb15
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessage.java
@@ -0,0 +1,171 @@
+/*
+ * 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 java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+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.InboundMessageContextLookup;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.messaging.context.SAMLMetadataContext;
+import org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import org.opensaml.saml.saml2.core.Extensions;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
+import org.opensaml.saml.saml2.metadata.SSODescriptor;
+import org.opensaml.saml.saml2.metadata.SingleLogoutService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Profile action that performs initial analysis of a {@link LogoutRequest} or {@link LogoutResponse} to
+ * dispatch it for subsequent processing.
+ * 
+ * <p>This action is essentially three decision states in one, using some custom events.</p>
+ * 
+ * <p>If the inbound message is a {@link LogoutResponse} then the event "IsLogoutResponse" is signaled.</p>
+ * 
+ * <p>If the inbound message is a {@link LogoutRequest}, then one of two events is signaled. If the request
+ * contains the {@link Asynchronous} extension, then the "IsLogoutRequestAsync" event is signaled. This also
+ * occurs, provided a particular option is enabled, if the request does not contain the extension but there
+ * is no SAML metadata available for the requester or the metadata contains no {@link SingleLogoutService}
+ * endpoints.</p>
+ * 
+ * <p>Finally, {@link EventIds#PROCEED_EVENT_ID} is the result if neither of the above applies.</p>
+ * 
+ * <p>Various standard events may occur if the message is missing or isn't an appropriate type.</p>
+ * 
+ * @event {@link #IS_LOGOUT_RESPONSE}
+ * @event {@link #IS_LOGOUT_REQUEST_ASYNC}
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * 
+ * @since 4.2.0
+ */
+public class PreProcessLogoutMessage extends AbstractProfileAction {
+
+    /** Event to signal for a logout response. */
+    @Nonnull public static final String IS_LOGOUT_RESPONSE = "IsLogoutResponse";
+
+    /** Event to signal for a logout response. */
+    @Nonnull public static final String IS_LOGOUT_REQUEST_ASYNC = "IsLogoutRequestAsync";
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PreProcessLogoutMessage.class);
+
+    /** Assume asynchronous in absence of metadata. */
+    private boolean assumeAsync;
+    
+    /** Lookup strategy for metadata context. */
+    @Nonnull private Function<ProfileRequestContext,SAMLMetadataContext> metadataContextLookupStrategy;
+    
+    /** Constructor. */
+    public PreProcessLogoutMessage() {
+        metadataContextLookupStrategy = new InboundMessageContextLookup().andThen(
+                new ChildContextLookup<>(SAMLPeerEntityContext.class).andThen(
+                        new ChildContextLookup<>(SAMLMetadataContext.class)));
+    }
+
+    /**
+     * Sets whether to treat logout requests as asynchronous (not requiring a response) if no
+     * metadata is available or lacks endpoints.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag
+     */
+    public void setAssumeAsynchronousLogout(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        assumeAsync = flag;
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link SAMLMetadataContext}.
+     * 
+     * @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");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.getInboundMessageContext() == null) {
+            log.warn("{} No inbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        } else if (!(profileRequestContext.getInboundMessageContext().getMessage() instanceof SAMLObject)) {
+            log.warn("{} No inbound SAML message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final SAMLObject msg = (SAMLObject) profileRequestContext.getInboundMessageContext().getMessage();
+        if (msg instanceof LogoutResponse) {
+            ActionSupport.buildEvent(profileRequestContext, IS_LOGOUT_RESPONSE);
+            return;
+        } else if (!(msg instanceof LogoutRequest)) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        final Extensions exts = ((LogoutRequest) msg).getExtensions();
+        if (exts != null && !exts.getUnknownXMLObjects(Asynchronous.DEFAULT_ELEMENT_NAME).isEmpty()) {
+            log.debug("{} LogoutRequest contained Asynchronous extension", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IS_LOGOUT_REQUEST_ASYNC);
+            return;
+        }
+        
+        if (assumeAsync) {
+            final SAMLMetadataContext mdCtx = metadataContextLookupStrategy.apply(profileRequestContext);
+            if (mdCtx == null || !(mdCtx.getRoleDescriptor() instanceof SSODescriptor) ||
+                    ((SSODescriptor) mdCtx.getRoleDescriptor()).getSingleLogoutServices().isEmpty()) {
+                log.debug("{} LogoutRequest treated as Asynchronous due to lack of metadata", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, IS_LOGOUT_REQUEST_ASYNC);
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
index 55ccd97d5..1019cde32 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ProcessLogoutRequest.java
@@ -53,7 +53,6 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.opensaml.saml.common.profile.SAMLEventIds;
-import org.opensaml.saml.ext.saml2aslo.Asynchronous;
 import org.opensaml.saml.saml2.core.LogoutRequest;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.SessionIndex;
@@ -305,11 +304,6 @@ public class ProcessLogoutRequest extends AbstractProfileAction {
             return false;
         }
         
-        if (log.isDebugEnabled() && logoutRequest.getExtensions() != null
-                && !logoutRequest.getExtensions().getUnknownXMLObjects(Asynchronous.DEFAULT_ELEMENT_NAME).isEmpty()) {
-            log.debug("{} LogoutRequest contained Asynchronous extension", getLogPrefix());
-        }
-        
         qualifiedNameIDFormats = new HashSet<>(qualifiedNameIDFormatsLookupStrategy.apply(profileRequestContext));
         
         return true;
diff --git a/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessageTest.java b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessageTest.java
new file mode 100644
index 000000000..fd9290b51
--- /dev/null
+++ b/idp-saml-impl/src/test/java/net/shibboleth/idp/saml/saml2/profile/impl/PreProcessLogoutMessageTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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 net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.idp.saml.saml2.profile.SAML2ActionTestingSupport;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+import org.opensaml.core.testing.OpenSAMLInitBaseTestCase;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import org.opensaml.saml.saml2.core.Extensions;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** {@link PreProcessLogoutMessage} unit test. */
+ at SuppressWarnings("javadoc")
+public class PreProcessLogoutMessageTest extends OpenSAMLInitBaseTestCase {
+    
+    private RequestContext src;
+    
+    private ProfileRequestContext prc;
+    
+    private PreProcessLogoutMessage action;
+    
+    @BeforeMethod public void setUpAction() throws ComponentInitializationException {
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
+        
+        action = new PreProcessLogoutMessage();
+        action.initialize();
+    }
+    
+    @Test public void testNoMessage() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+    }
+
+    @Test public void testLogoutResponse() {
+        //final NameID nameId = SAML2ActionTestingSupport.buildNameID("jdoe");
+        prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutResponse());
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, PreProcessLogoutMessage.IS_LOGOUT_RESPONSE);
+    }
+    
+    @Test public void testLogoutRequest() {
+        prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutRequest(null));
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+    }
+
+    @Test public void testAssumeAsyncLogoutRequest() throws ComponentInitializationException {
+        prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutRequest(null));
+        
+        action = new PreProcessLogoutMessage();
+        action.setAssumeAsynchronousLogout(true);
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, PreProcessLogoutMessage.IS_LOGOUT_REQUEST_ASYNC);
+    }
+
+    @Test public void testAsyncLogoutRequest() {
+        prc.getInboundMessageContext().setMessage(SAML2ActionTestingSupport.buildLogoutRequest(null));
+        
+        final SAMLObjectBuilder<Extensions> extsBuilder = (SAMLObjectBuilder<Extensions>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<Extensions>getBuilderOrThrow(
+                        Extensions.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<Asynchronous> asyncBuilder = (SAMLObjectBuilder<Asynchronous>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<Asynchronous>getBuilderOrThrow(
+                        Asynchronous.DEFAULT_ELEMENT_NAME);
+        
+        ((LogoutRequest) prc.getInboundMessageContext().getMessage()).setExtensions(extsBuilder.buildObject());
+        ((LogoutRequest) prc.getInboundMessageContext().getMessage()).getExtensions().getUnknownXMLObjects().add(
+                asyncBuilder.buildObject());
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, PreProcessLogoutMessage.IS_LOGOUT_REQUEST_ASYNC);
+    }
+    
+}
\ 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