[java-plugin-shibd] branch main updated: Implement POST preservation, wire into end of initiator master flow.
Scott Cantor
cantor.2 at osu.edu
Tue Sep 30 18:29:00 UTC 2025
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-plugin-shibd.
View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd.git;a=commit;h=1c2542a135aa8389da5591dd5dd5bae1224264cf
The following commit(s) were added to refs/heads/main by this push:
new 1c2542a Implement POST preservation, wire into end of initiator master flow.
1c2542a is described below
commit 1c2542a135aa8389da5591dd5dd5bae1224264cf
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Sep 30 14:28:35 2025 -0400
Implement POST preservation, wire into end of initiator master flow.
---
.../session-initiator/session-initiator-beans.xml | 8 +
.../session-initiator/session-initiator-flow.xml | 9 +-
.../shibboleth/idp/module/conf/sp/sp.properties | 8 +
.../net/shibboleth/sp/conf/agents-system.xml | 6 +-
.../src/main/java/net/shibboleth/sp/Agent.java | 26 +-
.../sp/messaging/RemotedHttpServletRequest.java | 2 +-
.../sp/profile/AbstractStateTokenManager.java | 3 -
.../java/net/shibboleth/sp/impl/BasicAgent.java | 43 ++-
.../sp/impl/StorageServiceStateTokenManager.java | 2 +
.../sp/profile/impl/IssueCorrelationCookie.java | 5 +-
.../sp/profile/impl/PreservePostData.java | 311 +++++++++++++++++++++
.../sp/profile/impl/PreservePostDataTest.java | 191 +++++++++++++
12 files changed, 600 insertions(+), 14 deletions(-)
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
index cbff124..c5a1570 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
@@ -14,5 +14,13 @@
class="net.shibboleth.sp.profile.impl.MapResourceToStateToken" scope="prototype"
p:createOutputObjects="true"
p:errorFatal="%{sp.stateToken.errorsFatal:false}" />
+
+ <bean id="PreservePostData"
+ class="net.shibboleth.sp.profile.impl.PreservePostData" scope="prototype"
+ p:errorFatal="%{sp.postData.errorsFatal:false}"
+ p:lifetime="%{sp.postData.lifetime:PT15M}"
+ p:storageService-ref="#{'%{sp.postData.StorageService:shibboleth.StorageService}'.trim()}"
+ p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
+ p:cookiePrefix="#{'%{sp.postData.cookiePrefix:__Host-_shibsp_post_}'.trim()}" />
</beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
index 9c8ea82..362b68e 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
@@ -29,10 +29,17 @@
<subflow-state id="CallInitiatorFlow" subflow="sp/initiator/#{SessionInitiatorIterator.next()}">
<input name="calledAsSubflow" value="true" />
- <transition on="proceed" to="EncodeAgentResponse" />
+ <transition on="proceed" to="PreservePostData" />
<transition on="ReselectFlow" to="CheckIterator" />
</subflow-state>
+ <action-state id="PreservePostData">
+ <evaluate expression="PreservePostData" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="EncodeAgentResponse" />
+ </action-state>
+
<action-state id="NoPotentialFlow">
<evaluate expression="'NoPotentialFlow'" />
</action-state>
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 4f45dd4..86c1fe0 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -42,6 +42,14 @@ sp.service.agents.checkInterval = PT5M
# Request/response correlation control
#sp.correlation.cookiePrefix = __Host-_shibsp_req_
+# POST data preservation controls
+#sp.postData.preservation = true
+#sp.postData.limit = 1048576
+#sp.postData.lifetime = PT15M
+#sp.postData.cookiePrefix = __Host-_shibsp_post_
+#sp.postData.StorageService = shibboleth.StorageService
+#sp.postData.errorsFatal = false
+
# Uncomment/set to define a default IdP discovery service URL or Function
#sp.discoveryURL =
#sp.discoveryURLFunction =
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
index fb99553..5fe5abe 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
@@ -28,7 +28,9 @@
class="net.shibboleth.sp.impl.BasicAgent" abstract="true"
p:authenticationMethod="%{sp.agent.authn.method:}"
p:allowedAddressRanges-ref="DefaultAllowedAddressRanges"
- p:supportsCachedAuthentication="%{sp.agent.authn.cached:true}" />
+ p:supportsCachedAuthentication="%{sp.agent.authn.cached:true}"
+ p:supportsPostPreservation="%{sp.postData.preservation:true}"
+ p:postLimit="%{sp.postData.limit:1048576}" />
<bean id="shibboleth.sp.Application.NoInheritance" class="net.shibboleth.sp.impl.BasicApplication" abstract="true"
p:allowInheritance="false"
@@ -64,7 +66,7 @@
<bean id="shibboleth.sp.CookieStateTokenManager" class="net.shibboleth.sp.impl.CookieStateTokenManager" lazy-init="true"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="%{sp.stateToken.cookiePrefix:_Host-shibsp_state_}" />
+ p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:_Host-shibsp_state_}'.trim()}" />
<!-- More traditional beans akin to IdP service. -->
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java b/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
index a510c56..5cce943 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
@@ -23,6 +23,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.component.IdentifiedComponent;
import net.shibboleth.shared.net.IPRange;
@@ -38,10 +39,10 @@ import net.shibboleth.shared.net.IPRange;
public interface Agent extends IdentifiedComponent {
/** ID of default {@link Application}. */
- @Nonnull @NotEmpty public static String DEFAULT_APPLICATION_ID = "default";
+ @Nonnull @NotEmpty static String DEFAULT_APPLICATION_ID = "default";
/** Method constant for "basic". */
- @Nonnull @NotEmpty public static String AUTH_METHOD_BASIC = "basic";
+ @Nonnull @NotEmpty static String AUTH_METHOD_BASIC = "basic";
/**
* Get the network addresses or ranges of addresses from which requests from this agent may
@@ -87,6 +88,27 @@ public interface Agent extends IdentifiedComponent {
* @return true iff the agent supports cached authentication via cookie
*/
boolean isSupportsCachedAuthentication();
+
+ /**
+ * Returns true iff the agent is permitted to submit form data to preserve
+ * during SSO.
+ *
+ * <p>Defaults to true.</p>
+ *
+ * @return true iff the agent is permitted to submit form data to preserve
+ */
+ boolean isSupportsPostPreservation();
+
+ /**
+ * Gets the size limit to apply to form data preservation attempts.
+ *
+ * <p>The default is 1024 * 1024.</p>
+ *
+ * <p>Zero indicates no inherent limit.</p>
+ *
+ * @return size limit for form data preservation
+ */
+ @NonNegative long getPostLimit();
/**
* Get an {@link Application} associated with this agent.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
index 7c62d4a..5038e18 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletRequest.java
@@ -233,7 +233,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
final byte[] body = obj.getmember("body").unsafe_string();
if (body != null) {
final List<Pair<String,String>> qparams =
- URISupport.parseQueryString(new String(body, StandardCharsets.UTF_8));
+ URISupport.parseQueryString(decodeUnsafeString(body));
for (final Pair<String,String> p : qparams) {
multimap.put(p.getFirst(), p.getSecond());
}
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractStateTokenManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractStateTokenManager.java
index 2c7eba3..9ac8b90 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractStateTokenManager.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractStateTokenManager.java
@@ -89,8 +89,6 @@ public abstract class AbstractStateTokenManager extends AbstractIdentifiableInit
expiration = Constraint.isNotNull(exp, "Expiration cannot be null");
}
- /** {@inheritDoc} */
-
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -101,7 +99,6 @@ public abstract class AbstractStateTokenManager extends AbstractIdentifiableInit
}
}
-
/**
* Generate a state token.
*
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
index 0612719..35e0c1e 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
@@ -29,6 +29,7 @@ import javax.annotation.Nullable;
import com.google.common.base.Functions;
import com.google.common.base.MoreObjects;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -52,9 +53,15 @@ public class BasicAgent extends BasicApplication implements Agent {
/** Authentication method. */
@Nullable @NotEmpty String authenticationMethod;
+ /** Whether to support form submission preservation and recovery. */
+ private boolean supportsPostPreservation;
+
+ /** Limit on size of form data to preserve. */
+ private long postLimit;
+
/** Internally configured shared secrets. */
@Nonnull private Set<String> sharedSecrets;
-
+
/** Application map. */
@Nonnull private Map<String,Application> applicationMap;
@@ -63,6 +70,8 @@ public class BasicAgent extends BasicApplication implements Agent {
supportsCachedAuthentication = true;
allowedAddressRanges = CollectionSupport.emptySet();
authenticationMethod = null;
+ supportsPostPreservation = true;
+ postLimit = 1024 * 1024;
sharedSecrets = CollectionSupport.emptySet();
applicationMap = CollectionSupport.emptyMap();
@@ -152,6 +161,38 @@ public class BasicAgent extends BasicApplication implements Agent {
supportsCachedAuthentication = flag;
}
+ /** {@inheritDoc} */
+ public boolean isSupportsPostPreservation() {
+ return supportsPostPreservation;
+ }
+
+ /**
+ * Sets whether agent is permitted to submit form data for preservation during SSO.
+ *
+ * @param flag flag to set
+ */
+ public void setSupportsPostPreservation(final boolean flag) {
+ checkSetterPreconditions();
+
+ supportsPostPreservation = flag;
+ }
+
+ /** {@inheritDoc} */
+ public long getPostLimit() {
+ return postLimit;
+ }
+
+ /**
+ * Sets limit on size of form data to preserve.
+ *
+ * @param limit size limit or 0 for none
+ */
+ public void setPostLimit(@NonNegative final long limit) {
+ checkSetterPreconditions();
+
+ postLimit = Constraint.isGreaterThanOrEqual(0, limit, "Post limit cannot be negative.");
+ }
+
/**
* Sets the {@link Application} instances associated with this agent.
*
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
index ccd934b..aa5f095 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
@@ -65,6 +65,8 @@ public class StorageServiceStateTokenManager extends AbstractStateTokenManager {
if (storageService == null) {
throw new ComponentInitializationException("StorageService cannot be null");
+ } else if (!storageService.getCapabilities().isServerSide()) {
+ throw new ComponentInitializationException("StorageService cannot be client-side");
}
}
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
index 7e9077c..0d8962c 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
@@ -44,9 +44,6 @@ import net.shibboleth.sp.profile.SPConstants;
* Action that issues a cookie used to record state about a request for
* later enforcement/evaluation.
*
- * <p>Principally used to capture a request message ID for correlation to a
- * response, also tracks whether a request was "passive" or not.</p>
- *
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#INVALID_MESSAGE}
*/
@@ -190,7 +187,7 @@ public class IssueCorrelationCookie extends AbstractApplicationAction {
cookieManager.purgeStaleCookies(cookiePrefix);
- log.debug("{} Tracking request ID {} against RelayState token {}", getLogPrefix(), requestID, stateToken);
+ log.debug("{} Tracking request ID {} against state token {}", getLogPrefix(), requestID, stateToken);
final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), escaper.escape(requestID));
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PreservePostData.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PreservePostData.java
new file mode 100644
index 0000000..26efe23
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PreservePostData.java
@@ -0,0 +1,311 @@
+/*
+ * 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.profile.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+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.annotation.constraint.Positive;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Action that detects submitted form data, and when permitted, stores it in a {@link StorageService} and
+ * issues a cookie associated with the active state token to preserve a pointer to the data for recovery.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ */
+public class PreservePostData extends AbstractApplicationAction {
+
+ /** Default cookie prefix. */
+ @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_post_";
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(PreservePostData.class);
+
+ /** Identifier generation. */
+ @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
+
+ /** Lifetime for data in storage. */
+ @Nonnull private Duration lifetime;
+
+ /** Storage service for data. */
+ @NonnullAfterInit private StorageService storageService;
+
+ /** Cookie manager. */
+ @NonnullAfterInit private CookieManager cookieManager;
+
+ /** Cookie prefix. */
+ @Nonnull private String cookiePrefix;
+
+ /** Whether an error constructing a correlation cookie is fatal. */
+ private boolean errorFatal;
+
+ /** State token vakue used in cookie name. */
+ @NonnullBeforeExec private String stateToken;
+
+ /** POST data to preserve. */
+ @NonnullBeforeExec private byte[] postData;
+
+ /** Constructor. */
+ public PreservePostData() {
+ lifetime = Duration.ofMinutes(15);
+ cookiePrefix = DEFAULT_COOKIE_PREFIX;
+ }
+
+ /**
+ * Set {@link IdentifierGenerationStrategy} to use.
+ *
+ * <p>Defaults to a secure random source that produces 16 byte values.</p>
+ *
+ * @param strategy identifier generator strategy
+ */
+ public void setIdentifierGenerationStrategy(@Nonnull final IdentifierGenerationStrategy strategy) {
+ checkSetterPreconditions();
+
+ identifierStrategy = Constraint.isNotNull(strategy, "IdentifierGenerationStrategy cannot be null");
+ }
+
+ /**
+ * Set the lifetime for data in storage.
+ *
+ * <p>Defaults to PT15M.</p>
+ *
+ * @param dur data lifetime
+ */
+ public void setLifetime(@Positive @Nonnull final Duration dur) {
+ checkSetterPreconditions();
+
+ Constraint.isFalse(lifetime == null || lifetime.isNegative() || lifetime.isZero(),
+ "Lifetime cannot be zero or negative");
+ lifetime = dur;
+ }
+
+ /**
+ * Set {@link StorageService} to use.
+ *
+ * @param storage storage service
+ */
+ public void setStorageService(@Nonnull final StorageService storage) {
+ checkSetterPreconditions();
+
+ storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+ }
+
+ /**
+ * Sets the {@link CookieManager} to use.
+ *
+ * @param manager cookie manager instance
+ */
+ public void setCookieManager(@Nonnull final CookieManager manager) {
+ checkSetterPreconditions();
+
+ cookieManager = Constraint.isNotNull(manager, "CookieManager cannot be null");
+ }
+
+ /**
+ * Sets the cookie prefix.
+ *
+ * <p>Defaults to {@link #DEFAULT_COOKIE_PREFIX}.</p>
+ *
+ * @param prefix cookie prefix
+ */
+ public void setCookiePrefix(@Nonnull @NotEmpty final String prefix) {
+ checkSetterPreconditions();
+
+ cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
+ }
+
+ /**
+ * Sets whether an error computing a state token should result in a fatal event.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setErrorFatal(final boolean flag) {
+ checkSetterPreconditions();
+
+ errorFatal = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (identifierStrategy == null) {
+ identifierStrategy = IdentifierGenerationStrategy.getInstance(ProviderType.SECURE);
+ }
+
+ if (cookieManager == null) {
+ throw new ComponentInitializationException("CookieManager cannot be null");
+ } else if (storageService == null) {
+ throw new ComponentInitializationException("StorageService cannot be null");
+ } else if (!storageService.getCapabilities().isServerSide()) {
+ throw new ComponentInitializationException("StorageService cannot be client-side");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ // First see if anything is even there.
+ final DDF input = ensureAgentRequestContext().getInput();
+ if (input != null) {
+ postData = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+ .getmember(RemotedHttpServletRequest.BODY)
+ .unsafe_string();
+ }
+
+ if (postData == null) {
+ log.debug("{} No POST data to preserve", getLogPrefix());
+ return false;
+ }
+
+ assert input != null;
+ final String contentType = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+ .getmember(RemotedHttpServletRequest.CONTENT_TYPE)
+ .string();
+ if (!"application/x-www-form-urlencoded".equals(contentType)) {
+ log.warn("{} Unsupported content type: {}", getLogPrefix(), contentType);
+ return false;
+ }
+
+ // Check permission.
+
+ if (!ensureAgent().isSupportsPostPreservation()) {
+ log.warn("{} POST data supplied for preservation, but not permitted for agent", getLogPrefix());
+ return false;
+ }
+
+ long limit = ensureAgent().getPostLimit();
+ if (limit > 0 && postData.length > limit) {
+ log.warn("{} POST data supplied for preservation, but size {} exceeds agent limit", getLogPrefix(),
+ postData.length);
+ return false;
+ }
+
+ // Check for state token for cookie correlation.
+
+ if (input != null) {
+ stateToken = input.getmember(SPConstants.STATE).string();
+ }
+
+ if (stateToken == null) {
+ if (errorFatal) {
+ log.warn("{} Input was missing {} parameter, failing due to POST data preservationr",
+ getLogPrefix(), SPConstants.STATE);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ } else {
+ log.warn("{} Input was missing {} parameter, skipping POST data preservation", getLogPrefix(),
+ SPConstants.STATE);
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ // We do the crazy stuff to accomodate the cookies being set or unset.
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ cookieManager.purgeStaleCookies(cookiePrefix);
+
+ log.debug("{} Preserving {} bytes of POST data against state token {}", getLogPrefix(), postData.length,
+ stateToken);
+
+ // Encode the data for storage.
+ final String encoded = Base64Support.encode(postData, false);
+
+ // Generate a storage key.
+ final String key = identifierStrategy.generateIdentifier(false);
+
+ if (!storageService.create(ensureAgent().getId() + ".PostData", key, encoded,
+ Instant.now().plus(lifetime).toEpochMilli())) {
+ log.warn("{} Collision attempting to create storage record for POST data under key {}", getLogPrefix(), key);
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ return;
+ }
+
+ if (stateToken.length() > 16) {
+ stateToken = stateToken.substring(0, 16);
+ }
+
+ // Save off cookie. Name is decorated with state token prefix, value is the storage key.
+ final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+ cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), escaper.escape(key));
+
+ } catch (final EncodingException e) {
+ log.error("{} Error base64-encoding data", getLogPrefix(), e);
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ }
+ } catch (final IOException e) {
+ log.error("{} Error creating storage record for POST data", getLogPrefix(), e);
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/PreservePostDataTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/PreservePostDataTest.java
new file mode 100644
index 0000000..3ff678c
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/PreservePostDataTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.profile.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.Cookie;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.net.CookieManager.SameSiteValue;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Unit test for {@link PreservePostData} action.
+ */
+ at SuppressWarnings("javadoc")
+public class PreservePostDataTest extends BaseAgplicationActionTest {
+
+ @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
+ @Nonnull @NotEmpty private final static String TEST_DATA = "foo=bar&zorkmid=a+b";
+
+ private String requestId;
+
+ private DDF input;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ private MemoryStorageService storageService;
+ private CookieManager cookieManager;
+ private PreservePostData action;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+ super.beforeMethod();
+
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+
+ cookieManager = new CookieManager();
+ cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
+ cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
+ cookieManager.setCookieLimit(10);
+ cookieManager.setSameSite(SameSiteValue.None);
+ cookieManager.setMaxAge(-1);
+ cookieManager.initialize();
+
+ storageService = new MemoryStorageService();
+ storageService.setId("test");
+ storageService.setCleanupInterval(Duration.ZERO);
+ storageService.initialize();
+
+ action = new PreservePostData();
+ action.setCookieManager(cookieManager);
+ action.setStorageService(storageService);
+ action.setErrorFatal(true);
+ action.initialize();
+
+ input = new DDF(null).structure();
+ input.addmember(SPConstants.STATE).string(TEST_STATE);
+ final DDF http = input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME);
+ http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
+ http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(TEST_DATA.getBytes());
+
+ arc.setInput(input);
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ cookieManager.destroy();
+ storageService.destroy();
+ }
+
+ @Test
+ public void testNoData() {
+ input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).getmember(RemotedHttpServletRequest.BODY).remove();
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getCookies().length, 0);
+ }
+
+ @Test
+ public void testWrongType() {
+ input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).getmember(RemotedHttpServletRequest.CONTENT_TYPE).remove();
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getCookies().length, 0);
+ }
+
+ @Test
+ public void testDisallowed() {
+ agent.setSupportsPostPreservation(false);
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getCookies().length, 0);
+ }
+
+ @Test
+ public void testSizeLimit() {
+ agent.setPostLimit(5);
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getCookies().length, 0);
+ }
+
+ @Test
+ public void testNoStateToken() {
+ input.addmember(SPConstants.STATE).remove();
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+ }
+
+ @Test
+ public void testSuccess() throws IOException, DecodingException {
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(response.getCookies().length, 1);
+
+ final Cookie cookie = response.getCookie(PreservePostData.DEFAULT_COOKIE_PREFIX + TEST_STATE);
+ assert cookie != null;
+ Assert.assertEquals(cookie.getMaxAge(), -1);
+ Assert.assertEquals(cookie.getAttribute("SameSite"), SameSiteValue.None.getValue());
+
+ final String key = cookie.getValue();
+
+ StorageRecord<String> record = storageService.read(agent.getId() + ".PostData", key);
+ assert record != null;
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(Base64Support.decode(record.getValue()), TEST_DATA.getBytes());
+ }
+
+ @Test
+ public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
+
+ final List<Cookie> cookies = new ArrayList<>(12);
+ for (int i = 0; i < 12; ++i) {
+ cookies.add(new Cookie(PreservePostData.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
+ Thread.sleep(250);
+ }
+ request.setCookies(cookies.toArray(new Cookie[12]));
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ Assert.assertEquals(response.getCookies().length, 3);
+ }
+
+}
\ 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