[java-plugin-shibd-saml] branch main updated: Action to process inbound logout request.

Codeberg noreply at shibboleth.net
Mon May 25 14:48:51 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/088270492583cabe49792f26b9347c63157f5c5d

The following commit(s) were added to refs/heads/main by this push:
     new 0882704  Action to process inbound logout request.
0882704 is described below

commit 088270492583cabe49792f26b9347c63157f5c5d
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon May 25 10:48:24 2026 -0400

    Action to process inbound logout request.
---
 .../saml2/profile/impl/ProcessLogoutRequest.java   | 359 +++++++++++++++++++++
 .../profile/impl/ProcessLogoutRequestTest.java     | 345 ++++++++++++++++++++
 2 files changed, 704 insertions(+)

diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequest.java
new file mode 100644
index 0000000..b426951
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequest.java
@@ -0,0 +1,359 @@
+/*
+ * 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.io.IOException;
+import java.io.StringReader;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.profile.SAML2ObjectSupport;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.xml.ParserPool;
+import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+import net.shibboleth.sp.state.StateManager;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.saml.saml2.profile.config.navigate.QualifiedNameIDFormatsLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Processes an inbound {@link LogoutRequest} from an IdP against the information supplied by
+ * the Agent.
+ * 
+ * <p>Based on the results, we encode a state token for the Agent to return later to get a response issued
+ * to the IdP.</p>
+ * 
+ * @pre <pre>profileRequestContext.ensureInboundMessageContext().getMessage() instanceof org.opensaml.saml.saml2.core.LogoutRequest</pre>
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#UNABLE_TO_DECODE}
+ */
+public class ProcessLogoutRequest extends AbstractApplicationAction {
+    
+    /** "matched" parameter name. */
+    @Nonnull @NotEmpty public static final String MATCHED_PARAM = "matched";
+
+    /** "token" parameter name. */
+    @Nonnull @NotEmpty public static final String TOKEN_PARAM = "token";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessLogoutRequest.class);
+    
+    /** Parser machinery. */
+    @NonnullAfterInit private ParserPool parserPool;
+
+    /** State manager to issue token with. */
+    @NonnullAfterInit private StateManager stateManager;
+
+    /** Lookup strategy for obtaining qualifier-defaultable NameID Formats. */
+    @Nonnull private Function<ProfileRequestContext,Collection<String>> qualifiedNameIDFormatsLookupStrategy;
+    
+    /** Lookup function for obtaining default NameQualifier. */
+    @Nonnull private Function<ProfileRequestContext,String> assertingPartyLookupStrategy;
+    
+    /** Lookup function for obtaining default SPNameQualifier. */
+    @Nonnull private Function<ProfileRequestContext,String> relyingPartyLookupStrategy;
+    
+    /** {@link NameID} Formats allowing defaulted qualifiers. */
+    @Nonnull private Set<String> qualifiedNameIDFormats;
+    
+    /** Logout request message. */
+    @NonnullBeforeExec private LogoutRequest logoutRequest;
+    
+    /** Output message. */
+    @NonnullBeforeExec private DDF output;
+    
+    /** Constructor. */
+    public ProcessLogoutRequest() {
+        qualifiedNameIDFormatsLookupStrategy = new QualifiedNameIDFormatsLookupFunction();
+
+        qualifiedNameIDFormats = CollectionSupport.emptySet();
+        
+        // Note these are reversed from the IdP as we are the SP here, so the
+        // NameQualifier would be the request issuer and the SPNameQualifier is us.
+        assertingPartyLookupStrategy = new RelyingPartyIdLookupFunction();
+        relyingPartyLookupStrategy = new IssuerLookupFunction();
+    }
+    
+    /**
+     * Sets the {@link ParserPool} to parse session data with.
+     * 
+     * @param pool parser pool
+     */
+    public void setParserPool(@Nonnull final ParserPool pool) {
+        checkSetterPreconditions();
+        
+        parserPool = Constraint.isNotNull(pool, "ParserPool cannot be null");
+    }
+    
+    /**
+     * Sets the {@link StateManager} to issue token with.
+     * 
+     * @param manager state manager
+     */
+    public void setStateManager(@Nonnull final StateManager manager) {
+        checkSetterPreconditions();
+        
+        stateManager = Constraint.isNotNull(manager, "StateManager cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link NameID} Formats to allow defaulted qualifiers.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setQualifiedNameIDFormatsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Collection<String>> strategy) {
+        checkSetterPreconditions();
+        qualifiedNameIDFormatsLookupStrategy = Constraint.isNotNull(strategy,
+                "Qualified NameID Formats lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy to obtain the default IdP NameQualifier.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAssertingPartyLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        assertingPartyLookupStrategy = Constraint.isNotNull(strategy, "Asserting party lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy to obtain the default SPNameQualifier.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        relyingPartyLookupStrategy = Constraint.isNotNull(strategy, "Relying party lookup strategy cannot be null");
+    }
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (parserPool == null) {
+            throw new ComponentInitializationException("ParserPool cannot be null");
+        } else if (stateManager == null) {
+            throw new ComponentInitializationException("StateManager cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.ensureInboundMessageContext().getMessage() instanceof LogoutRequest req) {
+            logoutRequest = req;
+        } else {
+            log.error("{} Input message was missing or wrong type", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        output = ensureAgentRequestContext().getOutput();
+        if (output == null) {
+            log.error("{} Output message was missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        qualifiedNameIDFormats = new HashSet<>(qualifiedNameIDFormatsLookupStrategy.apply(profileRequestContext));
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+        
+        // Establish request issuer if it's legitimate.
+        Issuer issuer = logoutRequest.getIssuer();
+        if (issuer != null && issuer.getFormat() != null && !NameIDType.ENTITY.equals(issuer.getFormat())) {
+            issuer = null;
+        }
+        
+        // Check for absent NameID in request.
+        final NameID requestedNameID = logoutRequest.getNameID();
+        if (requestedNameID == null) {
+            log.info("{} No NameID in logout request", getLogPrefix());
+            addToken(issuer, false);
+            return;
+        }
+        
+        final DDF input = agentRequestContext.getInput();
+        final String pickled = input != null ?
+                input.getmember(ConsumerConstants.SESSION_OPAQUE).getmember(PrepareAgentResponse.NAMEID_PARAM).string()
+                    : null;
+        if (pickled == null) {
+            log.info("{} No encoded NameID found in input message", getLogPrefix());
+            addToken(issuer, false);
+            return;
+        }
+                
+        // Decode NameID from session data from Agent and unpack the buried information.
+        NameID sessionNameID = null;
+        try {
+            final XMLObject xmlObject = XMLObjectSupport.unmarshallFromReader(parserPool, new StringReader(pickled));
+            if (xmlObject instanceof NameID n) {
+                sessionNameID = n;
+            } else {
+                throw new XMLParserException("Decoded object was of unexpected type.");
+            }
+        } catch (final XMLParserException | UnmarshallingException e) {
+            log.warn("{} Failed to decode session information", getLogPrefix(), e);
+        }
+
+        final boolean matched = requestMatchesSession(profileRequestContext, issuer, sessionNameID);
+        log.debug("{} LogoutRequest {} session from Agent", getLogPrefix(), matched ? "matched" : "did not match");
+        addToken(issuer, matched);
+    }
+    
+    /**
+     * Determines whethe the {@link NameID} and optional {@link SessionIndex} in the {@link LogoutRequest}
+     * strongly match the values preserved for the session. 
+     * 
+     * @param profileRequestContext profile request context
+     * @param issuer logout request issuer
+     * @param sessionNameID session-preserved NameID
+     * 
+     * @return true iff the logout request strongly matched
+     */
+    private boolean requestMatchesSession(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nullable final Issuer issuer, @Nullable final NameID sessionNameID) {
+        
+        if (sessionNameID == null) {
+            return false;
+        }
+        
+        // Extract details from session.
+        String entityID = null;
+        String sessionIndex = null;
+        
+        final String buriedData = sessionNameID.getSPProvidedID();
+        if (buriedData != null) {
+            final int index = buriedData.indexOf("!!");
+            if (index > 0) {
+                entityID = buriedData.substring(0, index);
+                sessionIndex = buriedData.substring(index + 2);
+            } else {
+                entityID = buriedData;
+            }
+        }
+        
+        // Check for NameID/etc. match.
+        if (issuer == null || entityID == null || !entityID.equals(issuer.getValue())) {
+            log.error("{} LogoutRequest issuer ({}) did not match session issuer ({})", getLogPrefix(),
+                    issuer != null ? issuer.getValue() : null, entityID);
+            return false;
+        }
+        
+        final NameID requestedNameID = logoutRequest.getNameID();
+        assert requestedNameID != null;
+        
+        if (NameID.PERSISTENT.equals(sessionNameID.getFormat()) || NameID.TRANSIENT.equals(sessionNameID.getFormat())
+                || qualifiedNameIDFormats.contains(sessionNameID.getFormat())) {
+            
+            if (!SAML2ObjectSupport.areNameIDsEquivalent(sessionNameID, requestedNameID,
+                    assertingPartyLookupStrategy.apply(profileRequestContext),
+                    relyingPartyLookupStrategy.apply(profileRequestContext))) {
+                log.warn("{} LogoutRequest's NameID did not strongly match session", getLogPrefix());
+                return false;
+            }
+        } else if (!SAML2ObjectSupport.areNameIDsEquivalent(sessionNameID, requestedNameID)) {
+            log.warn("{} LogoutRequest's NameID did not strongly match session", getLogPrefix());
+            return false;
+        }
+        
+        // Check for SessionIndex match.
+        
+        if (logoutRequest.getSessionIndexes().isEmpty()) {
+            return true;
+        }
+        
+        for (final SessionIndex index : logoutRequest.getSessionIndexes()) {
+            final String value = index.getValue();
+            if (value != null && value.equals(sessionIndex)) {
+                return true;
+            }
+        }
+        
+        log.warn("{} LogoutRequest SessionIndexes did not match session's recorded index ({})", getLogPrefix(),
+                sessionIndex);
+        return false;
+    }
+    
+    /**
+     * Generate token for Agent based on logout request and add to output.
+     * 
+     * <p>The token tracks the request ID and the issuer.</p>
+     * 
+     * @param issuer issuer of request
+     * @param matched matched signal
+     */
+    private void addToken(@Nullable final Issuer issuer, final boolean matched) {
+        
+        output.addmember(MATCHED_PARAM).integer(matched ? 1 : 0);
+        
+        final SAMLStateData state = new SAMLStateData();
+        
+        state.setRequestID(logoutRequest.getID());
+        state.setAuthenticationAuthority(issuer != null ? issuer.getValue() : null);
+        
+        try {
+            final String token = stateManager.preserveToStateToken(ensureAgent(), ensureApplication(), state);
+            output.addmember(TOKEN_PARAM).string(token);
+        } catch (final IOException e) {
+            log.error("{} Exception producing state token", getLogPrefix(), e);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
new file mode 100644
index 0000000..1f3970e
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutRequestTest.java
@@ -0,0 +1,345 @@
+/*
+ * 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.io.IOException;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.testing.SAML2ActionTestingSupport;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resource.Resource;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.impl.BasicKeystoreKeyStrategy;
+import net.shibboleth.shared.xml.impl.BasicParserPool;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
+import net.shibboleth.sp.profile.impl.CreateOutputMessage;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+import net.shibboleth.sp.state.impl.PassthroughStateManager;
+import net.shibboleth.sp.testing.TestResourceConverter;
+
+/**
+ * Unit test for {@link ProcessLogoutRequest} action.
+ */
+ at SuppressWarnings("javadoc")
+public class ProcessLogoutRequestTest extends BaseApplicationActionTest {
+
+    private Resource keystoreResource;
+    private Resource versionResource;
+    private DataSealer sealer;
+    private BasicParserPool parserPool;
+    private PassthroughStateManager stateManager;
+    
+    private ProcessLogoutRequest action;
+    
+    @BeforeClass
+    public void beforeClass() throws ComponentInitializationException {
+        parserPool = new BasicParserPool();
+        parserPool.initialize();
+     
+        ClassPathResource resource =
+                new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.jks");
+        Assert.assertTrue(resource.exists());
+        keystoreResource = TestResourceConverter.of(resource);
+
+        resource =
+                new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.kver");
+        Assert.assertTrue(resource.exists());
+        versionResource = TestResourceConverter.of(resource);
+
+        final BasicKeystoreKeyStrategy strategy = new BasicKeystoreKeyStrategy();
+        strategy.setKeyAlias("secret");
+        strategy.setKeyPassword("kpassword");
+        strategy.setKeystorePassword("password");
+        strategy.setKeystoreResource(keystoreResource);
+        strategy.setKeyVersionResource(versionResource);
+        strategy.initialize();
+        
+        sealer = new DataSealer();
+        sealer.setKeyStrategy(strategy);
+        sealer.initialize();
+        
+                
+        stateManager = new PassthroughStateManager();
+        stateManager.setId("test");
+        stateManager.setDataSealer(sealer);        
+        final ObjectMapper mapper = new ObjectMapper();
+        mapper.registerModule(new JavaTimeModule());
+        stateManager.setObjectMapper(mapper);        
+        stateManager.initialize();
+    }
+    
+    @AfterClass
+    public void afterClass() {
+        sealer.destroy();
+        stateManager.destroy();
+        parserPool.destroy();
+    }
+
+    /**
+     * Set up test.
+     * 
+     * @throws ComponentInitializationException
+     */
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.beforeMethod();
+        prc.removeSubcontext(RelyingPartyContext.class);
+        
+        action = new ProcessLogoutRequest();
+        action.setParserPool(parserPool);
+        action.setStateManager(stateManager);
+        action.initialize();
+        
+        final CreateOutputMessage prep = new CreateOutputMessage();
+        prep.initialize();
+        ActionTestingSupport.assertProceedEvent(prep.execute(src));
+        
+        buildLogoutRequest();
+    }
+        
+    /**
+     * Tear down test.
+     */
+    @AfterMethod
+    public void tearDown() {
+        action.destroy();
+    }
+    
+    
+    @Test(expectedExceptions=ComponentInitializationException.class)
+    public void testNoParserPool() throws ComponentInitializationException {
+        new ProcessLogoutInitiatorRequest().initialize();
+    }
+    
+    @Test
+    public void testNoInputMessage() throws ComponentInitializationException {
+        prc.ensureInboundMessageContext().setMessage(null);
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+    }
+
+    @Test
+    public void testWrongMessageType() throws ComponentInitializationException {
+        buildLogoutResponse();
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+    }
+
+    @Test
+    public void testNoOutputMessage() throws ComponentInitializationException {
+        arc.setOutput(null);
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+
+    @Test
+    public void testNoNameID() throws ComponentInitializationException, IOException {
+        ((LogoutRequest) prc.ensureInboundMessageContext().ensureMessage()).setNameID(null);
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+    
+    @Test
+    public void testNoSessionData() throws ComponentInitializationException, IOException {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    @Test
+    public void testMisnamedSessionData() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember("foo").string("bar");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+    
+    @Test
+    public void testInvalidSessionData() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string("bar");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    @Test
+    public void testIncompleteSessionData() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar'>foo</NameID>");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    @Test
+    public void testMatchNoIndex() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' SPProvidedID='"
+                        + ActionTestingSupport.INBOUND_MSG_ISSUER + "'>jdoe</NameID>");
+        arc.setInput(input);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 1);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    @Test
+    public void testNoMatchWithIndex() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' SPProvidedID='"
+                        + ActionTestingSupport.INBOUND_MSG_ISSUER + "!!12345'>jdoe</NameID>");
+        arc.setInput(input);
+
+        final SAMLObjectBuilder<SessionIndex> indexBuilder = (SAMLObjectBuilder<SessionIndex>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<SessionIndex>ensureBuilder(
+                        SessionIndex.DEFAULT_ELEMENT_NAME);
+        
+        final SessionIndex index = indexBuilder.buildObject();
+        index.setValue("foo");
+        
+        final LogoutRequest request = (LogoutRequest) prc.ensureInboundMessageContext().getMessage();
+        assert request != null;
+        request.getSessionIndexes().add(index);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 0);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    @Test
+    public void testMatchWithIndex() throws ComponentInitializationException, IOException {
+        final DDF input = new DDF(null).structure();
+        input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+                "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' SPProvidedID='"
+                        + ActionTestingSupport.INBOUND_MSG_ISSUER + "!!12345'>jdoe</NameID>");
+        arc.setInput(input);
+
+        final SAMLObjectBuilder<SessionIndex> indexBuilder = (SAMLObjectBuilder<SessionIndex>)
+                XMLObjectProviderRegistrySupport.getBuilderFactory().<SessionIndex>ensureBuilder(
+                        SessionIndex.DEFAULT_ELEMENT_NAME);
+        
+        final SessionIndex index = indexBuilder.buildObject();
+        index.setValue("12345");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final DDF output = arc.getOutput();
+        assert output != null;
+        Assert.assertEquals(output.getmember(ProcessLogoutRequest.MATCHED_PARAM).integer(), 1);
+        validateToken(output.getmember(ProcessLogoutRequest.TOKEN_PARAM).string());
+    }
+
+    /**
+     * Adds mock request content to the inbound {@link MessageContext).
+     */
+    private void buildLogoutRequest() {
+        final LogoutRequest request = SAML2ActionTestingSupport.buildLogoutRequest(
+                SAML2ActionTestingSupport.buildNameID("jdoe"));
+        prc.ensureInboundMessageContext().setMessage(request);
+    }
+    
+    /**
+     * Adds mock response content to the inbound {@link MessageContext).
+     */
+    private void buildLogoutResponse() {
+        final LogoutResponse response = SAML2ActionTestingSupport.buildLogoutResponse();
+        prc.ensureInboundMessageContext().setMessage(response);
+    }
+    
+    /**
+     * Validate content of token returned by action.
+     * 
+     * @param token token returned by action
+     * @throws IOException 
+     */
+    private void validateToken(@Nullable final String token) throws IOException {
+        assert token != null;
+        final SAMLStateData data = stateManager.recoverFromStateToken(agent, application, token, SAMLStateData.class);
+        assert data != null;
+        
+        final LogoutRequest request = (LogoutRequest) prc.ensureInboundMessageContext().getMessage();
+        assert request != null;
+        final Issuer issuer = request.getIssuer();
+        assert issuer != null;
+        Assert.assertEquals(data.getAuthenticationAuthority(), issuer.getValue());
+        Assert.assertEquals(data.getRequestID(), request.getID());
+    }
+}
\ 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