[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