[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