[java-plugin-shibd-saml] branch dev/StateMgmtWIP updated: Fix a bug and add a unit test with RelayState included.
Codeberg
noreply at shibboleth.net
Tue Apr 28 19:44:42 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch dev/StateMgmtWIP
in repository java-plugin-shibd-saml.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/0411efb320f0a953c1b205202ec8599383ad4e17
The following commit(s) were added to refs/heads/dev/StateMgmtWIP by this push:
new 0411efb Fix a bug and add a unit test with RelayState included.
0411efb is described below
commit 0411efb320f0a953c1b205202ec8599383ad4e17
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Apr 28 15:44:30 2026 -0400
Fix a bug and add a unit test with RelayState included.
---
.../idp/flows/sp/consumer/saml2/saml2-beans.xml | 3 +-
.../flows/saml2/SAML2TokenConsumerFlowTest.java | 212 ++++++++++++++++++---
2 files changed, 191 insertions(+), 24 deletions(-)
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
index 99f3e30..21dc7df 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/saml2/saml2-beans.xml
@@ -15,7 +15,8 @@
<bean id="RecoverStateData"
class="net.shibboleth.sp.profile.impl.RecoverStateData" scope="prototype"
p:stateTokenLookupStrategy-ref="RelayStateLookup"
- p:stateDataClass="net.shibboleth.sp.saml.saml2.SAMLStateData" />
+ p:stateDataClass="net.shibboleth.sp.saml.saml2.SAMLStateData"
+ p:createOutputObjects="true" />
<bean id="RelayStateLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
index b047e9e..61aca70 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
@@ -56,13 +57,23 @@ import org.opensaml.xmlsec.signature.support.SignatureException;
import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.webflow.executor.FlowExecutionResult;
import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+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 jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
@@ -72,17 +83,32 @@ import net.shibboleth.shared.codec.DecodingException;
import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.net.CookieManager;
import net.shibboleth.shared.net.URISupport;
+import net.shibboleth.shared.net.CookieManager.SameSiteValue;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.service.ReloadableService;
+import net.shibboleth.shared.service.ServiceableComponent;
import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.AgentCriterion;
+import net.shibboleth.sp.AgentResolver;
+import net.shibboleth.sp.Application;
import net.shibboleth.sp.context.AgentRequestContext;
import net.shibboleth.sp.ddf.DDF;
import net.shibboleth.sp.flows.AbstractSPFlowTest;
import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.SAMLStateData;
import net.shibboleth.sp.saml.saml2.profile.impl.PrepareAgentResponse;
+import net.shibboleth.sp.state.StateData;
+import net.shibboleth.sp.state.impl.CookieStateManager;
/**
* Unit test for the SP session-initiator flow.
@@ -116,18 +142,84 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
/** ACS URL. */
@Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+ @Autowired
+ @Qualifier("shibboleth.sp.AgentResolver")
+ protected ReloadableService<AgentResolver> agentResolver;
+
@Autowired
@Qualifier("shibboleth.SessionIDGenerator")
protected IdentifierGenerationStrategy idGenerator;
/** Dummy signing key. */
- @Autowired @Qualifier("dummy.idp.Credential") protected Credential idpCredential;
+ @Autowired
+ @Qualifier("dummy.idp.Credential")
+ protected Credential idpCredential;
+ // Used to create state cookies for subsequent inclusion in mock requests to flow.
+
+ private CookieManager cookieManager;
+ private CookieStateManager stateManager;
+ private MockHttpServletRequest request2;
+ private MockHttpServletResponse response2;
+
/** Constructor. */
public SAML2TokenConsumerFlowTest() {
super(FLOW_ID);
}
+ /**
+ * Set up state manager.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeClass
+ public void beforeClass() throws ComponentInitializationException {
+ cookieManager = new CookieManager();
+ cookieManager.setCookiePath("/");
+ cookieManager.setSameSite(SameSiteValue.None);
+ cookieManager.setCookieLimit(10);
+ cookieManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
+ @Nonnull public HttpServletRequest get() {
+ assert request2 != null;
+ return request2;
+ }
+ });
+ cookieManager.setHttpServletResponseSupplier(new NonnullSupplier<HttpServletResponse>() {
+ @Nonnull public HttpServletResponse get() {
+ assert response2 != null;
+ return response2;
+ }
+ });
+ cookieManager.initialize();
+
+ stateManager = new CookieStateManager();
+ stateManager.setId("test");
+ stateManager.setCookiePrefix("__Host-shibsp_state");
+
+ final ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ stateManager.setObjectMapper(mapper);
+
+ stateManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
+ @Nonnull public HttpServletRequest get() {
+ assert request2 != null;
+ return request2;
+ }
+ });
+
+ stateManager.setCookieManager(cookieManager);
+ stateManager.initialize();
+ }
+
+ /**
+ * Tear down state manager.
+ */
+ @AfterClass
+ public void tearDown() {
+ stateManager.destroy();
+ cookieManager.destroy();
+ }
+
/** Pre-test work. */
@BeforeMethod
public void beforeMethod() {
@@ -158,7 +250,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
public void testErrorStatus() throws IOException {
final Response response = buildSAMLResponse(ISSUER, StatusCode.RESPONDER, null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -174,7 +266,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testUnverified() throws IOException {
- final DDF input = buildRemotedPOSTResponse(buildSAMLResponse(ISSUER + "/bad", StatusCode.SUCCESS, null));
+ final DDF input = buildRemotedPOSTResponse(buildSAMLResponse(ISSUER + "/bad", StatusCode.SUCCESS, null), null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -193,7 +285,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.setIssueInstant(Instant.EPOCH);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -212,7 +304,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.setDestination(RESPONSE_URL + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -231,7 +323,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setIssueInstant(Instant.now().minusSeconds(1800));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -253,7 +345,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert locality != null;
locality.setAddress("127.0.0.1");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -278,7 +370,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert data != null;
data.setAddress("127.0.0.1");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -301,7 +393,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert subject != null;
subject.getSubjectConfirmations().get(0).setMethod(SubjectConfirmation.METHOD_SENDER_VOUCHES);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -326,7 +418,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert data != null;
data.setNotOnOrAfter(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -351,7 +443,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert data != null;
data.setRecipient(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -376,7 +468,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert data != null;
data.setRecipient(RESPONSE_URL + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -397,7 +489,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setIssuer(SAML2ActionTestingSupport.buildIssuer(ISSUER + "bad"));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -421,7 +513,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
issuer.setFormat(NameIDType.EMAIL);
response.getAssertions().get(0).setIssuer(issuer);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -439,7 +531,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
@Test
public void testUnsigned() throws IOException {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -459,7 +551,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
response.getAssertions().get(0).setConditions(null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -481,7 +573,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert conditions != null;
conditions.setNotOnOrAfter(Instant.now().minusSeconds(300));
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -504,7 +596,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
assert conditions != null;
conditions.getAudienceRestrictions().get(0).getAudiences().get(0).setURI(AUDIENCE + "/bad");
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -524,7 +616,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
public void testSuccess() throws IOException {
final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, null);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -537,6 +629,53 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
}
+ /**
+ * Test successful flow with InResponseTo available.
+ *
+ * @throws IOException
+ * @throws ResolverException
+ */
+ @Test
+ public void testSuccessWithState() throws IOException, ResolverException {
+
+ request2 = new MockHttpServletRequest();
+ response2 = new MockHttpServletResponse();
+ final StateData state = buildStateData("foo");
+
+ final String stateToken;
+
+ try (final ServiceableComponent<AgentResolver> resolver = agentResolver.getServiceableComponent()) {
+ final Agent agent = resolver.getComponent().resolveSingle(
+ new CriteriaSet(new AgentCriterion(AGENT_ID)));
+ assert agent != null;
+ final Application app = agent.getApplication(APPLICATION_ID);
+ assert app != null;
+ stateToken = stateManager.preserveToStateToken(agent, app, state);
+ }
+
+ final Cookie cookie = response2.getCookies()[0];
+ assert cookie != null;
+
+ final Response response = buildSAMLResponse(ISSUER, StatusCode.SUCCESS, "foo");
+ sign(response);
+ final DDF input = buildRemotedPOSTResponse(response, stateToken);
+ input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+ .addmember("headers").structure()
+ .addmember("Cookie")
+ .unsafe_string(new String(cookie.getName() + '=' + cookie.getValue()).getBytes(StandardCharsets.UTF_8));
+
+ setApplicationRequest(APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ final DDF output = assertOutputMessageSuccess(result);
+ assert output != null;
+ System.out.println("testSuccess output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.singleton("mail"),
+ RESOURCE_URL, response.getAssertions().get(0).getAuthnStatements().get(0).getSessionIndex());
+ }
+
/**
* Test successful flow with attributes.
*
@@ -554,7 +693,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
CollectionSupport.listOf("staff at example.org", "employee at example.org")));
response.getAssertions().get(0).getAttributeStatements().add(statement);
sign(response);
- final DDF input = buildRemotedPOSTResponse(response);
+ final DDF input = buildRemotedPOSTResponse(response, null);
setApplicationRequest(APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -717,12 +856,14 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
* Encodes a SAML response into a form POST embedded in a remoted message.
*
* @param response SAML response to encode
+ * @param relayState relay state if any
*
* @return input object suitable for token consumer flow
*
* @throws IOException on error
*/
- @Nonnull private DDF buildRemotedPOSTResponse(@Nonnull final Response response) throws IOException {
+ @Nonnull private DDF buildRemotedPOSTResponse(@Nonnull final Response response, @Nullable final String relayState)
+ throws IOException {
try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
XMLObjectSupport.marshallToOutputStream(response, sink);
@@ -736,8 +877,16 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
- http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(Constraint.isNotNull(URISupport.buildQuery(
- CollectionSupport.listOf(new Pair<>("SAMLResponse", base64))), "Query string is null").getBytes(StandardCharsets.UTF_8));
+ final List<Pair<String,String>> params;
+ if (relayState != null) {
+ params = CollectionSupport.listOf(new Pair<>("SAMLResponse", base64),
+ new Pair<>("RelayState", relayState));
+ } else {
+ params = CollectionSupport.singletonList(new Pair<>("SAMLResponse", base64));
+ }
+
+ http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(
+ Constraint.isNotNull(URISupport.buildQuery(params), "Query string is null").getBytes(StandardCharsets.UTF_8));
return obj;
} catch (final MarshallingException | EncodingException e) {
@@ -766,4 +915,21 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
}
}
+ /**
+ * Create state object.
+ *
+ * @param requestID message ID to include
+ *
+ * @return state object
+ */
+ @Nonnull private StateData buildStateData(@Nonnull final String requestID) {
+ final SAMLStateData data = new SAMLStateData();
+ data.setRequestID(requestID);
+ data.setRequestTime(Instant.now());
+ data.setIssuer(AUDIENCE);
+ data.setAuthenticationAuthority(ISSUER);
+ data.setRawResource(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+ return 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