[java-plugin-shibd-saml] 02/02: Action to process the login-consumer flow token parameter.
Codeberg
noreply at shibboleth.net
Wed May 27 19:19:28 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/fdff4276d789d179ef24bb4a14b9fe033b15c7c5
commit fdff4276d789d179ef24bb4a14b9fe033b15c7c5
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed May 27 15:19:15 2026 -0400
Action to process the login-consumer flow token parameter.
---
.../profile/impl/ProcessLogoutTokenRequest.java | 198 +++++++++++++++++++
.../impl/ProcessLogoutTokenRequestTest.java | 218 +++++++++++++++++++++
2 files changed, 416 insertions(+)
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequest.java
new file mode 100644
index 0000000..596d491
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+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.saml.saml2.core.LogoutResponse;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.StateDataContext;
+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.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Processes a request to potentially issue a SAML 2.0 {@link LogoutResponse} by examining the input
+ * to recover the "token" issued by the logout-consumer flow in a previous call.
+ *
+ * <p>Assuming the token is present and sufficient, it creates a {@link RelyingPartyContext}
+ * based on the identity of the original IdP that needs to receive the message and a
+ * {@link StateDataContext} to store the state recovered from the token, principally the
+ * original request's ID and RelayState value.</p>
+ *
+ * @pre <pre>profileRequestContext.ensureSubcontext(AgentRequestContext.class).getInput().getmember("token").string() != null</pre>
+ * @post <pre>profileRequestContext.getSubcontext(StateDataContext.class) != null</pre>
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#UNABLE_TO_DECODE}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ */
+public class ProcessLogoutTokenRequest extends AbstractApplicationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessLogoutTokenRequest.class);
+
+ /** Custom {@link StateManager} used to process the "token" parameter. */
+ @NonnullAfterInit private StateManager stateManager;
+
+ /** Creation strategy for {@link StateDataContext}. */
+ @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextCreationStrategy;
+
+ /** Creation strategy for {@link RelyingPartyContext}. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextCreationStrategy;
+
+ /** State data recovered from token. */
+ @NonnullBeforeExec private SAMLStateData stateData;
+
+ /** Constructor. */
+ public ProcessLogoutTokenRequest() {
+ stateDataContextCreationStrategy = new ChildContextLookup<>(StateDataContext.class, true);
+ relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+ }
+
+ /**
+ * Sets the {@link StateManager} to parse token parameter with.
+ *
+ * @param manager state manager
+ */
+ public void setStateManager(@Nonnull final StateManager manager) {
+ checkSetterPreconditions();
+
+ stateManager = Constraint.isNotNull(manager, "StateManager cannot be null");
+ }
+
+ /**
+ * Set strategy to create the {@link StateDataContext}.
+ *
+ * @param strategy creation strategy
+ */
+ public void setStateDataContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+ checkSetterPreconditions();
+
+ stateDataContextCreationStrategy =
+ Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+ }
+
+ /**
+ * Set strategy to create the {@link RelyingPartyContext}.
+ *
+ * @param strategy creation strategy
+ */
+ public void setRelyingPartyContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ checkSetterPreconditions();
+
+ relyingPartyContextCreationStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (stateManager == null) {
+ throw new ComponentInitializationException("StateManager cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final DDF input = ensureAgentRequestContext().getInput();
+ if (input == null) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ log.error("{} No input message from agent", getLogPrefix());
+ return false;
+ }
+
+ final String token = input.getmember(ConsumerConstants.TOKEN_PARAM).string();
+ if (token == null || !token.startsWith(ProcessLogoutRequest.TOKEN_PREFIX)) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ log.error("{} '{}' parameter missing or invalid", getLogPrefix(), ConsumerConstants.TOKEN_PARAM);
+ return false;
+ }
+
+ try {
+ stateData = stateManager.recoverFromStateToken(ensureAgent(), ensureApplication(),
+ token.substring(ProcessLogoutRequest.TOKEN_PREFIX.length()), SAMLStateData.class);
+ } catch (final IOException e) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+ log.error("{} Error decoding '{}' parameter", getLogPrefix(), ConsumerConstants.TOKEN_PARAM, e);
+ return false;
+ }
+
+ if (stateData.getAuthenticationAuthority() == null) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ log.error("{} State recovered from '{}' parameter did not include IdP entityID", getLogPrefix(),
+ ConsumerConstants.TOKEN_PARAM);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final StateDataContext stateDataContext = stateDataContextCreationStrategy.apply(profileRequestContext);
+ if (stateDataContext == null) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ log.error("{} Unable to create StateDataContext", getLogPrefix());
+ return;
+ }
+ stateDataContext.setStateData(stateData);
+
+ final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+ if (rpContext == null) {
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ log.error("{} Unable to create RelyingPartyContext", getLogPrefix());
+ return;
+ }
+
+ log.debug("{} Decoded {} created from LogoutRequest with ID: {}", getLogPrefix(),
+ ConsumerConstants.TOKEN_PARAM, stateData.getRequestID());
+
+ rpContext.setRelyingPartyId(stateData.getAuthenticationAuthority());
+ log.debug("{} Initialized RelyingPartyContext for {}", getLogPrefix(), stateData.getAuthenticationAuthority());
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequestTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequestTest.java
new file mode 100644
index 0000000..f4fd03c
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutTokenRequestTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.EventIds;
+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.sp.context.StateDataContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
+import net.shibboleth.sp.state.impl.PassthroughStateManager;
+import net.shibboleth.sp.testing.TestResourceConverter;
+
+/**
+ * Unit test for {@link ProcessLogoutTokenRequest} action.
+ */
+ at SuppressWarnings("javadoc")
+public class ProcessLogoutTokenRequestTest extends BaseApplicationActionTest {
+
+ @Nonnull private static final String RELAY_STATE = "dummy";
+
+ private Resource keystoreResource;
+ private Resource versionResource;
+ private DataSealer sealer;
+ private PassthroughStateManager stateManager;
+
+ private ProcessLogoutTokenRequest action;
+
+ @BeforeClass
+ public void beforeClass() throws ComponentInitializationException {
+
+ 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();
+ }
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+ super.beforeMethod();
+ prc.removeSubcontext(RelyingPartyContext.class);
+
+ action = new ProcessLogoutTokenRequest();
+ action.setStateManager(stateManager);
+ action.initialize();
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ }
+
+ @Test(expectedExceptions=ComponentInitializationException.class)
+ public void testNoStateManager() throws ComponentInitializationException {
+ new ProcessLogoutTokenRequest().initialize();
+ }
+
+ @Test
+ public void testNoInputMessage() {
+ prc.ensureInboundMessageContext().setMessage(null);
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+ Assert.assertNull(prc.getSubcontext(StateDataContext.class));
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testNoToken() throws IOException {
+ arc.setInput(new DDF().structure());
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+ Assert.assertNull(prc.getSubcontext(StateDataContext.class));
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testBadToken() throws IOException {
+ final DDF input = new DDF().structure();
+ input.addmember(ConsumerConstants.TOKEN_PARAM).string(ProcessLogoutRequest.TOKEN_PREFIX + "bad");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+ Assert.assertNull(prc.getSubcontext(StateDataContext.class));
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testMissingEntityID() throws IOException {
+ final DDF input = new DDF().structure();
+ input.addmember(ConsumerConstants.TOKEN_PARAM).string(createToken(null, "12345", RELAY_STATE));
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+ Assert.assertNull(prc.getSubcontext(StateDataContext.class));
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testSuccess() throws IOException {
+ final DDF input = new DDF().structure();
+ input.addmember(ConsumerConstants.TOKEN_PARAM).string(
+ createToken(ActionTestingSupport.OUTBOUND_MSG_ISSUER, "12345", RELAY_STATE));
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ final StateDataContext stateContext = prc.getSubcontext(StateDataContext.class);
+ final SAMLStateData data = stateContext != null ? (SAMLStateData) stateContext.getStateData() : null;
+ assert data != null;
+ Assert.assertEquals(data.getAuthenticationAuthority(), ActionTestingSupport.OUTBOUND_MSG_ISSUER);
+ Assert.assertEquals(data.getRequestID(), "12345");
+ Assert.assertEquals(data.getResource(), RELAY_STATE);
+
+ final RelyingPartyContext rpc = prc.getSubcontext(RelyingPartyContext.class);
+ assert rpc != null;
+ Assert.assertEquals(rpc.getRelyingPartyId(), ActionTestingSupport.OUTBOUND_MSG_ISSUER);
+ }
+
+ /**
+ * Create a token for use with test based on supplied data.
+ *
+ * @return the encoded token parameter value
+ *
+ * @throws IOException
+ */
+ @Nonnull private String createToken(@Nullable final String entityID, @Nullable final String requestID,
+ @Nullable final String relayState) throws IOException {
+
+ final SAMLStateData data = new SAMLStateData();
+
+ data.setAuthenticationAuthority(entityID);
+ data.setRequestID(requestID);
+ data.setResource(relayState);
+
+ return ProcessLogoutRequest.TOKEN_PREFIX + stateManager.preserveToStateToken(agent, application, data);
+ }
+
+}
\ 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