[java-plugin-shibd] branch main updated: Add POST data recovery action and initial unit tests.
Scott Cantor
cantor.2 at osu.edu
Fri Oct 3 13:50:09 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=4b348370eeb931e80db60d7d4fbe544a20109f8d
The following commit(s) were added to refs/heads/main by this push:
new 4b34837 Add POST data recovery action and initial unit tests.
4b34837 is described below
commit 4b348370eeb931e80db60d7d4fbe544a20109f8d
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Oct 3 09:50:02 2025 -0400
Add POST data recovery action and initial unit tests.
---
.../src/main/java/net/shibboleth/sp/Agent.java | 10 +
.../sp/messaging/RemotedHttpServletRequest.java | 69 ++--
.../sp/messaging/RemotedHttpServletResponse.java | 8 +-
.../AbstractTokenConsumerResponseAction.java | 53 ++-
.../java/net/shibboleth/sp/impl/BasicAgent.java | 20 ++
.../sp/profile/impl/PreservePostData.java | 13 +-
.../sp/profile/impl/RecoverPostData.java | 389 +++++++++++++++++++++
.../templates/sp/add-html-body-content.vm | 2 +
.../templates/sp/add-html-head-content.vm | 2 +
.../src/main/resources/templates/sp/post-replay.vm | 54 +++
.../sp/profile/impl/PreservePostDataTest.java | 12 +-
.../sp/profile/impl/RecoverPostDataTest.java | 236 +++++++++++++
sp-server-impl/src/test/resources/logback-test.xml | 17 +
13 files changed, 805 insertions(+), 80 deletions(-)
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 5cce943..b46828b 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
@@ -16,6 +16,7 @@
package net.shibboleth.sp;
import java.net.InetAddress;
+import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;
@@ -109,6 +110,15 @@ public interface Agent extends IdentifiedComponent {
* @return size limit for form data preservation
*/
@NonNegative long getPostLimit();
+
+ /**
+ * Gets the character encoding to apply during POST recovery.
+ *
+ * <p>Defaults to UTF-8.</p>
+ *
+ * @return name of encoding
+ */
+ @Nonnull Charset getCharacterEncoding();
/**
* 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 5038e18..4d8dbd2 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
@@ -116,15 +116,12 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
- /** ISO single byte decoder. */
- @Nonnull private static final CharsetDecoder ISO_8859_1 =
- StandardCharsets.ISO_8859_1.newDecoder()
- .onMalformedInput(CodingErrorAction.REPORT)
- .onUnmappableCharacter(CodingErrorAction.REPORT);
-
/** Underlying object containing remoted data. */
@Nonnull private final DDF obj;
+ /** Decoder to apply to byte array data. */
+ @Nonnull private final CharsetDecoder decoder;
+
/** Cookie array. */
@NonnullElements private List<Cookie> cookies;
@@ -132,12 +129,23 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
@Nullable private Map<String, String[]> parameters;
/**
- * Constructor.
+ * Constructor for UTF-8 usage.
*
* @param ddf remoted request information
*/
public RemotedHttpServletRequest(@Nonnull final DDF ddf) {
+ this(ddf, UTF_8);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param ddf remoted request information
+ * @param dec a backup decoder if UTF-8 fails
+ */
+ public RemotedHttpServletRequest(@Nonnull final DDF ddf, @Nonnull CharsetDecoder dec) {
obj = Constraint.isNotNull(ddf, "DDF cannot be null");
+ decoder = Constraint.isNotNull(dec, "CharsetDecoder cannot be null");
}
/**
@@ -233,7 +241,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(decodeUnsafeString(body));
+ URISupport.parseQueryString(decodeUnsafeString(body, decoder, null));
for (final Pair<String,String> p : qparams) {
multimap.put(p.getFirst(), p.getSecond());
}
@@ -278,7 +286,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
/** {@inheritDoc} */
public String getServerName() {
- return decodeUnsafeString(obj.getmember("hostname").unsafe_string());
+ return decodeUnsafeString(obj.getmember("hostname").unsafe_string(), decoder, null);
}
/** {@inheritDoc} */
@@ -292,7 +300,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
return new BufferedReader(
new InputStreamReader(
new ByteArrayInputStream(
- obj.getmember(BODY).unsafe_string()), StandardCharsets.UTF_8));
+ obj.getmember(BODY).unsafe_string()), decoder.charset()));
}
/** {@inheritDoc} */
@@ -459,7 +467,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
if (name == null) {
return null;
}
- return decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string());
+ return decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string(), decoder, null);
}
/** {@inheritDoc} */
@@ -467,7 +475,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
if (name == null) {
return Collections.emptyEnumeration();
}
- final String s = decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string());
+ final String s = decodeUnsafeString(obj.getmember("headers").getmember(name).unsafe_string(), decoder, null);
if (s != null) {
return Collections.enumeration(Collections.singletonList(s));
}
@@ -486,7 +494,7 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
}
final DDF h = obj.getmember("headers").getmember(name);
if (h.isstring()) {
- return Integer.parseInt(decodeUnsafeString(h.unsafe_string()));
+ return Integer.parseInt(decodeUnsafeString(h.unsafe_string(), decoder, null));
}
return -1;
}
@@ -540,12 +548,12 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
/** {@inheritDoc} */
public String getRequestURI() {
- return decodeUnsafeString(obj.getmember(REQUEST_URI).unsafe_string());
+ return decodeUnsafeString(obj.getmember(REQUEST_URI).unsafe_string(), decoder, '?');
}
/** {@inheritDoc} */
public StringBuffer getRequestURL() {
- final String url = decodeUnsafeString(obj.getmember(REQUEST_URL).unsafe_string());
+ final String url = decodeUnsafeString(obj.getmember(REQUEST_URL).unsafe_string(), decoder, '?');
return new StringBuffer(url != null ? url : "");
}
@@ -619,13 +627,16 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
}
/**
- * Helper method to decode a byte buffer into either UTF-8 or ISO-8859-1.
+ * Helper method to decode a byte array via a designated encoding.
*
* @param buffer input buffer
+ * @param decoder decoder to apply
+ * @param delim optional delimter to truncate at
*
- * @return encoded String form of the data
+ * @return decoded String form of the data or null if a decoding exception was caught
*/
- @Nullable private static String decodeUnsafeString(final byte[] buffer) {
+ @Nullable public static String decodeUnsafeString(final byte[] buffer, @Nonnull final CharsetDecoder decoder,
+ @Nullable final Character delim) {
if (buffer == null) {
return null;
@@ -634,27 +645,15 @@ public class RemotedHttpServletRequest implements HttpServletRequest {
final ByteBuffer wrapper = ByteBuffer.wrap(buffer);
try {
- final String decoded = UTF_8.decode(wrapper).toString();
- final int delim = decoded.indexOf('?');
- if (delim > 0) {
- return decoded.substring(0, delim);
+ final String decoded = decoder.decode(wrapper).toString();
+ final int index = delim != null ? decoded.indexOf(delim) : 0;
+ if (index > 0) {
+ return decoded.substring(0, index);
} else {
return decoded;
}
} catch (final CharacterCodingException e) {
-
- }
-
- try {
- final String decoded = ISO_8859_1.decode(wrapper).toString();
- final int delim = decoded.indexOf('?');
- if (delim > 0) {
- return decoded.substring(0, delim);
- } else {
- return decoded;
- }
- } catch (final CharacterCodingException e) {
-
+
}
return null;
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
index 21d3b66..2a6d1c5 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/messaging/RemotedHttpServletResponse.java
@@ -53,9 +53,15 @@ public class RemotedHttpServletResponse implements HttpServletResponse {
/** Field holding redirect location. */
@Nonnull @NotEmpty public static final String REDIRECT = "redirect";
- /** Field holding response data. */
+ /** Field holding response. */
@Nonnull @NotEmpty public static final String RESPONSE = "response";
+ /** Field holding response body data. */
+ @Nonnull @NotEmpty public static final String DATA = "data";
+
+ /** Field holding response body status. */
+ @Nonnull @NotEmpty public static final String STATUS = "status";
+
/** Field holding header collection. */
@Nonnull @NotEmpty public static final String HEADERS = "headers";
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractTokenConsumerResponseAction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractTokenConsumerResponseAction.java
index 5077cb3..619c694 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractTokenConsumerResponseAction.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractTokenConsumerResponseAction.java
@@ -56,6 +56,9 @@ import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
* redirect to the recovered URL, the serialized attribute data, and the opaque session
* data.</p>
*
+ * <p>If the response is "committed" prior to this action, then the redirect portion will
+ * be skipped in favor of the previous response.</p>
+ *
* @event {@link EventIds#PROCEED_EVENT_ID}
*/
public abstract class AbstractTokenConsumerResponseAction extends AbstractApplicationAction {
@@ -69,6 +72,7 @@ public abstract class AbstractTokenConsumerResponseAction extends AbstractApplic
/** Constructor. */
public AbstractTokenConsumerResponseAction() {
attributeContextLookupStrategy = new ChildContextLookup<>(AttributeContext.class);
+ setCreateOutputObjects(true);
}
/**
@@ -87,19 +91,11 @@ public abstract class AbstractTokenConsumerResponseAction extends AbstractApplic
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ ensureOutputObjects();
+
final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
- DDF output = agentRequestContext.getOutput();
- if (output == null) {
- output = new DDF(null).structure();
- agentRequestContext.setOutput(output);
- }
-
- DDF httpResponse = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
- if (!httpResponse.isstruct() || agentRequestContext.getRemotedHttpServletResponse() == null) {
- httpResponse = output.addmember(RemotedHttpServletResponse.STRUCTURE_NAME);
- agentRequestContext.setRemotedHttpServletResponse(new RemotedHttpServletResponse(httpResponse));
- }
+ final DDF output = agentRequestContext.getOutput();
+ assert output != null;
final AttributeContext attributeContext = attributeContextLookupStrategy.apply(profileRequestContext);
if (attributeContext != null && !attributeContext.getIdPAttributes().isEmpty()) {
@@ -138,27 +134,30 @@ public abstract class AbstractTokenConsumerResponseAction extends AbstractApplic
output.addmember(ConsumerConstants.SESSION_OPAQUE).string(sessionData);
}
- // Issue redirect to proper resource URL. Either recovered from protocol state earlier
- // or falling back to an input parameter from the agent. Final backstop is a relative
- // redirect to the site root.
-
final RemotedHttpServletResponse remotedResponse = agentRequestContext.getRemotedHttpServletResponse();
assert remotedResponse != null;
- byte[] resource = agentRequestContext.getTargetURL();
- if (resource == null || resource.length == 0) {
- final DDF in = agentRequestContext.getInput();
- if (in != null) {
- resource = in.getmember(ConsumerConstants.BASE_URL).unsafe_string();
- }
+ if (!remotedResponse.isCommitted()) {
+ // Issue redirect to proper resource URL. Either recovered from protocol state earlier
+ // or falling back to an input parameter from the agent. Final backstop is a relative
+ // redirect to the site root.
+
+
+ byte[] resource = agentRequestContext.getTargetURL();
if (resource == null || resource.length == 0) {
- resource = "/".getBytes(StandardCharsets.UTF_8);
+ final DDF in = agentRequestContext.getInput();
+ if (in != null) {
+ resource = in.getmember(ConsumerConstants.BASE_URL).unsafe_string();
+ }
+ if (resource == null || resource.length == 0) {
+ resource = "/".getBytes(StandardCharsets.UTF_8);
+ }
+ agentRequestContext.setTargetURL(resource);
}
- agentRequestContext.setTargetURL(resource);
+
+ output.addmember(SPConstants.TARGET).unsafe_string(resource);
+ remotedResponse.sendRedirect(resource);
}
-
- output.addmember(SPConstants.TARGET).unsafe_string(resource);
- remotedResponse.sendRedirect(resource);
}
/**
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 35e0c1e..8070f6b 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
@@ -16,6 +16,8 @@
package net.shibboleth.sp.impl;
import java.net.InetAddress;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
@@ -59,6 +61,9 @@ public class BasicAgent extends BasicApplication implements Agent {
/** Limit on size of form data to preserve. */
private long postLimit;
+ /** Character set encoding for POST recovery. */
+ @Nonnull private Charset encoding;
+
/** Internally configured shared secrets. */
@Nonnull private Set<String> sharedSecrets;
@@ -72,6 +77,7 @@ public class BasicAgent extends BasicApplication implements Agent {
authenticationMethod = null;
supportsPostPreservation = true;
postLimit = 1024 * 1024;
+ encoding = StandardCharsets.UTF_8;
sharedSecrets = CollectionSupport.emptySet();
applicationMap = CollectionSupport.emptyMap();
@@ -193,6 +199,20 @@ public class BasicAgent extends BasicApplication implements Agent {
postLimit = Constraint.isGreaterThanOrEqual(0, limit, "Post limit cannot be negative.");
}
+ /** {@inheritDoc} */
+ @Nonnull public Charset getCharacterEncoding() {
+ return encoding;
+ }
+
+ /**
+ * Sets the character encoding to apply for POST recovery.
+ *
+ * @param name name of encoding
+ */
+ public void CharacterEncoding(@Nonnull final String name) {
+ encoding = Charset.forName(name);
+ }
+
/**
* Sets the {@link Application} instances associated with this agent.
*
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
index 26efe23..e63421c 100644
--- 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
@@ -15,6 +15,7 @@
package net.shibboleth.sp.profile.impl;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
@@ -33,8 +34,6 @@ 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;
@@ -270,8 +269,9 @@ public class PreservePostData extends AbstractApplicationAction {
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);
+ // Given the MIME type, we assume the POST data is URL-encoded, ergo safe to translate as UTF-8.
+ // At this stage the underlying data may not in fact be UTF-8 but the encoded characters are ASCII.
+ final String encoded = new String(postData, StandardCharsets.UTF_8);
// Generate a storage key.
final String key = identifierStrategy.generateIdentifier(false);
@@ -293,11 +293,6 @@ public class PreservePostData extends AbstractApplicationAction {
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) {
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverPostData.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverPostData.java
new file mode 100644
index 0000000..0ff8aaf
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverPostData.java
@@ -0,0 +1,389 @@
+/*
+ * 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.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletResponse;
+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.codec.HTMLEncoder;
+import net.shibboleth.shared.codec.StringDigester;
+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.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.servlet.HttpServletSupport;
+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.messaging.RemotedHttpServletResponse;
+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 RecoverPostData extends AbstractApplicationAction {
+
+ /** Default template ID. */
+ @Nonnull @NotEmpty static public final String DEFAULT_TEMPLATE_ID = "/templates/sp/post-replay.vm";
+
+ /** Default cookie prefix. */
+ @Nonnull @NotEmpty static public final String DEFAULT_COOKIE_PREFIX = "_shibsp_post_";
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(RecoverPostData.class);
+
+ /** Velocity engine used to evaluate the replay form template. */
+ @NonnullAfterInit private VelocityEngine velocityEngine;
+
+ /** ID of the velocity template used to generate replay form. */
+ @NonnullAfterInit private String velocityTemplateId;
+
+ /** Digester for CSP hashes. */
+ @Nullable private StringDigester cspDigester;
+
+ /** CSP nonce generation. */
+ @Nullable private IdentifierGenerationStrategy cspNonceGenerator;
+
+ /** Storage service for data. */
+ @NonnullAfterInit private StorageService storageService;
+
+ /** Cookie manager. */
+ @NonnullAfterInit private CookieManager cookieManager;
+
+ /** Cookie prefix. */
+ @Nonnull private String cookiePrefix;
+
+ /** Deecoder for Agent data. */
+ @NonnullBeforeExec private CharsetDecoder decoder;
+
+ /** Recovered data. */
+ @NonnullBeforeExec private String postData;
+
+ /** Constructor. */
+ public RecoverPostData() {
+ velocityTemplateId = DEFAULT_TEMPLATE_ID;
+ cookiePrefix = DEFAULT_COOKIE_PREFIX;
+ setCreateOutputObjects(true);
+ }
+
+ /**
+ * Set the VelocityEngine instance.
+ *
+ * @param newVelocityEngine the new VelocityEngine instane
+ */
+ public void setVelocityEngine(@Nullable final VelocityEngine newVelocityEngine) {
+ checkSetterPreconditions();
+ velocityEngine = newVelocityEngine;
+ }
+
+ /**
+ * Set the Velocity template ID.
+ *
+ * <p>Defaults to {@link #DEFAULT_TEMPLATE_ID}.</p>
+ *
+ * @param newVelocityTemplateId the new Velocity template id
+ */
+ public void setVelocityTemplateId(@Nullable final String newVelocityTemplateId) {
+ checkSetterPreconditions();
+ velocityTemplateId = newVelocityTemplateId;
+ }
+
+ /**
+ * Set a {@link StringDigester} to use to generate CSP hashes.
+ *
+ * @param digester string digester
+ */
+ public void setCSPDigester(@Nullable final StringDigester digester) {
+ checkSetterPreconditions();
+ cspDigester = digester;
+ }
+
+ /**
+ * Set {@link IdentifierGenerationStrategy} to use for generating CSP nonces.
+ *
+ * @param strategy nonce generator strategy
+ */
+ public void setCSPNonceGenerator(@Nullable final IdentifierGenerationStrategy strategy) {
+ checkSetterPreconditions();
+
+ cspNonceGenerator = Constraint.isNotNull(strategy, "IdentifierGenerationStrategy cannot be null");
+ }
+
+ /**
+ * 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");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (velocityEngine == null) {
+ throw new ComponentInitializationException("VelocityEngine cannot be null");
+ }
+
+ if (velocityTemplateId == null) {
+ throw new ComponentInitializationException("Velocity template ID cannot be null");
+ }
+
+ 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;
+ }
+
+ ensureOutputObjects();
+
+ // Masks any errors with a null return. We do this up front so that any data is cleaned
+ // up even in the event we abort below.
+ postData = getRecoveredData();
+ if (postData == null) {
+ return false;
+ }
+
+ // Check permission.
+
+ if (!ensureAgent().isSupportsPostPreservation()) {
+ log.warn("{} POST data preservation not permitted for agent", getLogPrefix());
+ return false;
+ }
+
+ // See if we know the target URL. If not, a fallback to homeURL or the site root is
+ // not an appropriate target for a POST.
+ if (ensureAgentRequestContext().getTargetURL() == null) {
+ log.warn("{} No definitive target resource, POST data recovery aborted", getLogPrefix());
+ return false;
+ }
+
+ decoder = ensureAgent().getCharacterEncoding().newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ // Try and decode the data as best we can.
+ final String decodedTarget = RemotedHttpServletRequest.decodeUnsafeString(
+ ensureAgentRequestContext().getTargetURL(), decoder, null);
+ if (decodedTarget == null) {
+ log.warn("{} Failure decoding target resource byte array using encoding: {}", getLogPrefix(),
+ decoder.charset().name());
+ return;
+ }
+
+ // Parse the URL-encoded POST data using the agent's character set.
+ // In practice this isn't totally safe because Java's URL decoder will do
+ // character substitutions in the face of illegal encodings, but it's better
+ // than nothing in that at least correctly encoded data will be decoded correctly
+ // into Unicode.
+ final List<Pair<String,String>> params;
+ try {
+ params = URISupport.parseQueryString(postData, true, true, decoder.charset());
+ } catch (final IllegalArgumentException e) {
+ log.warn("{} Failure decoding form parameters using encoding: {}", getLogPrefix(),
+ decoder.charset().name());
+ return;
+ }
+
+ // HTML-encode the parameters in place so they're ready for insertion in the form by the template.
+ for (final Pair<String,String> param : params) {
+ param.setFirst(HTMLEncoder.encodeForHTMLAttribute(param.getFirst()));
+ param.setSecond(HTMLEncoder.encodeForHTMLAttribute(param.getSecond()));
+ }
+
+ // We do the crazy stuff to catch the template output.
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ log.debug("{} Recovering {} characters of POST data into form template", getLogPrefix(), postData.length());
+
+ final VelocityContext context = new VelocityContext();
+
+ if (cspDigester != null) {
+ log.trace("Adding CSP digester to context");
+ context.put("cspDigester", cspDigester);
+ }
+ if (cspNonceGenerator != null) {
+ log.trace("Adding CSP nonce generator to context");
+ context.put("cspNonce", cspNonceGenerator);
+ }
+
+ context.put("action", HTMLEncoder.encodeForHTMLAttribute(decodedTarget));
+ context.put("params", params);
+
+ final RemotedHttpServletResponse response = agentRequestContext.getRemotedHttpServletResponse();
+ // We know this is non-null since we created the output objects if required.
+ assert response != null;
+
+ context.put("response", response);
+
+ HttpServletSupport.addNoCacheHeaders(response);
+ HttpServletSupport.setContentType(response, "text/html");
+ response.setCharacterEncoding(decoder.charset().name());
+ response.setStatus(HttpServletResponse.SC_OK);
+
+ try (final Writer out = new OutputStreamWriter(response.getOutputStream(), decoder.charset())) {
+ velocityEngine.mergeTemplate(velocityTemplateId, decoder.charset().name(), context, out);
+ out.flush();
+ } catch (final IOException e) {
+ log.error("{} Exception producing form response to client for POST recovery", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+ /**
+ * Checks for stored data based on the state token and cookie(s) supplied and reads back from
+ * storage, deleting the record.
+ *
+ * <p>Errors here are logged and suppressed since there's nothing to be done about them.</p>
+ *
+ * @return recovered data, if any
+ */
+ @Nullable private String getRecoveredData() {
+
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ final DDF input = agentRequestContext.getInput();
+ final String stateToken = input != null ? input.getmember(SPConstants.STATE).string() : null;
+ if (stateToken == null) {
+ log.debug("{} No state token found in request, skipping POST recovery check", getLogPrefix());
+ return null;
+ }
+
+ // Do the needful to allow cookies to be loaded/cleared from wrapped input.
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ // Check for cookie to get storage key.
+ final String key = cookieManager.getCookieValue(cookiePrefix + stateToken, null);
+ if (key == null) {
+ log.debug("{} No recovery cookie for state token {}, skipping POST recovery check", getLogPrefix(),
+ stateToken);
+ return null;
+ }
+
+ // Unset the cookie.
+ cookieManager.unsetCookie(cookiePrefix + stateToken);
+
+ // Try and read/delete the storage record.
+
+ final StorageRecord<String> record = storageService.read(ensureAgent().getId() + ".PostData", key);
+ if (record == null) {
+ log.warn("{} POST recovery record was missing for key: {}", getLogPrefix(), key);
+ return null;
+ }
+
+ try {
+ storageService.delete(ensureAgent().getId() + ".PostData", key);
+ } catch (final IOException e) {
+ log.warn("{} Error deleting POST recovery record for key: {}", getLogPrefix(), key);
+ }
+
+ return record.getValue();
+
+ } catch (final IOException e) {
+ log.error("{} Error reading storage record for POST data", getLogPrefix(), e);
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/resources/templates/sp/add-html-body-content.vm b/sp-server-impl/src/main/resources/templates/sp/add-html-body-content.vm
new file mode 100644
index 0000000..bcb4b5a
--- /dev/null
+++ b/sp-server-impl/src/main/resources/templates/sp/add-html-body-content.vm
@@ -0,0 +1,2 @@
+## Stub file that one can replace with Velocity template (and thus HTML) content
+## to be placed into the BODY section of the response
\ No newline at end of file
diff --git a/sp-server-impl/src/main/resources/templates/sp/add-html-head-content.vm b/sp-server-impl/src/main/resources/templates/sp/add-html-head-content.vm
new file mode 100644
index 0000000..08aaeaa
--- /dev/null
+++ b/sp-server-impl/src/main/resources/templates/sp/add-html-head-content.vm
@@ -0,0 +1,2 @@
+## Stub file that one can replace with Velocity template (and thus HTML) content
+## to be placed into the HEAD section of the response
\ No newline at end of file
diff --git a/sp-server-impl/src/main/resources/templates/sp/post-replay.vm b/sp-server-impl/src/main/resources/templates/sp/post-replay.vm
new file mode 100644
index 0000000..f536c47
--- /dev/null
+++ b/sp-server-impl/src/main/resources/templates/sp/post-replay.vm
@@ -0,0 +1,54 @@
+##
+## Velocity Template for SP POST recover form
+##
+## Velocity context may contain the following properties
+## response - HttpServletResponse
+## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+## cspNonce - Calculates secure nonces (call generateIdentifier)
+## action - String - the HTML-encoded action URL for the form
+## params - List<Pair<String,String>> - the HTML-encoded form parameter n-v pairs
+##
+#set ($onLoad="submitOnce()")
+#if ($cspDigester)$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onLoad)'")#end
+## Nonce for dynanmic scripts.
+#set ($nonce = $cspNonce.generateIdentifier())
+$response.addHeader("Content-Security-Policy", "script-src-elem 'nonce-$nonce'")
+<!DOCTYPE html>
+<html>
+ <head>
+ #parse ( "/templates/sp/add-html-head-content.vm" )
+ <script #if ($nonce)nonce="$nonce"#end>
+ <!--
+ function submitOnce() {
+ if (location.hash.length>0) {
+ if (confirm("Are you sure you want to resubmit this form information a second time?")) {
+ document.getElementById("shib_continue").click();
+ } else {
+ document.body.innerHTML="<html>Form information was not resubmitted.</html>";
+ }
+ } else {
+ var loc = window.location;
+ window.location = loc + "#submitted";
+ document.getElementById("shib_continue").click();
+ }
+ }
+ // -->
+ </script>
+ </head>
+ <body onload="$onLoad">
+ <h2>Login Completed</h2>
+ <noscript>
+ <p>A form submission to this site was interrupted by the login process.
+ If you would like to complete it now, submit this form.</p>
+ </noscript>
+
+ <form method="POST" action="$action">
+#foreach ($p in $params)
+ <input type="hidden" name="$p.first" value="$p.second"/>
+#end
+ <input type="submit" id="shib_continue" name="_shib_continue_" value="Continue"/>
+ </form>
+
+ #parse ( "/templates/add-html-body-content.vm" )
+ </body>
+</html>
\ 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
index 3ff678c..95c2c1b 100644
--- 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
@@ -35,8 +35,6 @@ 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;
@@ -53,8 +51,6 @@ 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;
@@ -154,7 +150,7 @@ public class PreservePostDataTest extends BaseAgplicationActionTest {
}
@Test
- public void testSuccess() throws IOException, DecodingException {
+ public void testSuccess() throws IOException {
final Event event = action.execute(src);
ActionTestingSupport.assertProceedEvent(event);
Assert.assertEquals(response.getCookies().length, 1);
@@ -166,14 +162,14 @@ public class PreservePostDataTest extends BaseAgplicationActionTest {
final String key = cookie.getValue();
- StorageRecord<String> record = storageService.read(agent.getId() + ".PostData", key);
+ final 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());
+ Assert.assertEquals(record.getValue(), TEST_DATA);
}
@Test
- public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
+ public void testPurge() throws ComponentInitializationException, InterruptedException {
final List<Cookie> cookies = new ArrayList<>(12);
for (int i = 0; i < 12; ++i) {
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/RecoverPostDataTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/RecoverPostDataTest.java
new file mode 100644
index 0000000..cd42f57
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/RecoverPostDataTest.java
@@ -0,0 +1,236 @@
+/*
+ * 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.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.RuntimeConstants;
+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.StringDigester;
+import net.shibboleth.shared.codec.StringDigester.OutputFormat;
+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.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * Unit test for {@link RecoverPostData} action.
+ */
+ at SuppressWarnings("javadoc")
+public class RecoverPostDataTest extends BaseAgplicationActionTest {
+
+ @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
+ @Nonnull @NotEmpty private final static String TEST_DATA = "foo=bar&zorkmid=a+b";
+ @Nonnull @NotEmpty private final static String BAD_TEST_DATA = "foo=bar&zorkmid=a+b&bad=%EB%8C%04";
+
+ private DDF input;
+ private MockHttpServletRequest request;
+ private MockHttpServletResponse response;
+
+ private MemoryStorageService storageService;
+ private CookieManager cookieManager;
+ private VelocityEngine velocityEngine;
+
+ private RecoverPostData action;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ * @throws NoSuchAlgorithmException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException, NoSuchAlgorithmException {
+ 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();
+
+ velocityEngine = new VelocityEngine();
+ velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath");
+ velocityEngine.setProperty("classpath.resource.loader.class",
+ "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+ velocityEngine.init();
+
+ action = new RecoverPostData();
+ action.setCookieManager(cookieManager);
+ action.setStorageService(storageService);
+ action.setVelocityEngine(velocityEngine);
+ action.setCSPDigester(new StringDigester("SHA256", OutputFormat.BASE64));
+ action.setCSPNonceGenerator(IdentifierGenerationStrategy.getInstance(ProviderType.SECURE));
+ action.initialize();
+
+ input = new DDF(null).structure();
+ input.addmember(SPConstants.STATE).string(TEST_STATE);
+ input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME);
+
+ arc.setInput(input);
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ cookieManager.destroy();
+ storageService.destroy();
+ }
+
+ @Test
+ public void testNoData() {
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ validateNoResponse(0);
+ }
+
+ @Test
+ public void testNoStateToken() {
+ input.addmember(SPConstants.STATE).remove();
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ validateNoResponse(0);
+ }
+
+ @Test
+ public void testDisallowed() throws IOException {
+ agent.setSupportsPostPreservation(false);
+ storePOSTData(TEST_DATA);
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ validateNoResponse(1);
+ }
+
+ @Test
+ public void testNoTargetURL() throws IOException {
+ storePOSTData(TEST_DATA);
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ validateNoResponse(1);
+ }
+
+ @Test
+ public void testBadTargetEncoding() throws IOException {
+ storePOSTData(TEST_DATA);
+
+ // Simple invalid UTF-8 sequence (cribbed from a web site).
+ final byte[] invalid = { (byte)235, (byte)140, (byte)4 };
+
+ arc.setTargetURL(invalid);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ validateNoResponse(1);
+ }
+
+ @Test
+ public void testSuccess() throws IOException {
+
+ final String key = storePOSTData(TEST_DATA);
+ arc.setTargetURL("https://sp.example.org/test.cgi".getBytes());
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ Assert.assertEquals(response.getCookies().length, 1);
+
+ final RemotedHttpServletResponse remotedResponse = arc.getRemotedHttpServletResponse();
+ assert remotedResponse != null;
+ Assert.assertTrue(remotedResponse.isCommitted());
+
+ Assert.assertEquals(response.getCookies().length, 1);
+ Assert.assertNull(storageService.read(agent.getId() + ".PostData", key));
+
+ final DDF output = arc.getOutput();
+ assert output != null;
+ final DDF response = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME)
+ .getmember(RemotedHttpServletResponse.RESPONSE);
+ Assert.assertEquals(response.getmember(RemotedHttpServletResponse.STATUS).integer(), 200);
+ Assert.assertNotNull(response.getmember(RemotedHttpServletResponse.DATA).unsafe_string());
+ }
+
+ /**
+ * Validate the action doing no work.
+ */
+ private void validateNoResponse(final int cookieHeaderCount) {
+ final RemotedHttpServletResponse remotedResponse = arc.getRemotedHttpServletResponse();
+ assert remotedResponse != null;
+ Assert.assertFalse(remotedResponse.isCommitted());
+
+ final DDF output = arc.getOutput();
+ assert output != null;
+ Assert.assertNull(
+ output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME)
+ .getmember(RemotedHttpServletResponse.RESPONSE)
+ .getmember(RemotedHttpServletResponse.DATA).unsafe_string()
+ );
+
+ Assert.assertEquals(response.getCookies().length, cookieHeaderCount);
+ }
+
+ /**
+ * Insert a storage record for the POST data to recover.
+ *
+ * @param data data to recover
+ *
+ * @throws IOException
+ */
+ @Nonnull private String storePOSTData(@Nonnull final String data) throws IOException {
+ final String key = IdentifierGenerationStrategy.getInstance(ProviderType.SECURE).generateIdentifier(false);
+ Assert.assertTrue(
+ storageService.create(agent.ensureId() + ".PostData", key, data,
+ Instant.now().plus(Duration.ofMinutes(15)).toEpochMilli()));
+ final Cookie cookie = new Cookie(RecoverPostData.DEFAULT_COOKIE_PREFIX + TEST_STATE, key);
+ request.setCookies(cookie);
+ return key;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/resources/logback-test.xml b/sp-server-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..ec68238
--- /dev/null
+++ b/sp-server-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%level [%logger:%line] - %msg%n</pattern>
+ <charset>UTF-8</charset>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="INFO" />
+ <appender-ref ref="STDOUT" />
+ </root>
+
+</configuration>
\ 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