[java-plugin-shibd] branch main updated: Add URL encoding to StateData and a base class action for preservation.
Codeberg
noreply at shibboleth.net
Thu Apr 23 14:49:54 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/7fded7f20f866a789477c000df17b2a8c52f86ce
The following commit(s) were added to refs/heads/main by this push:
new 7fded7f Add URL encoding to StateData and a base class action for preservation.
7fded7f is described below
commit 7fded7f20f866a789477c000df17b2a8c52f86ce
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Thu Apr 23 10:49:39 2026 -0400
Add URL encoding to StateData and a base class action for preservation.
---
.../shibboleth/sp/context/StateDataContext.java | 40 ++++--
.../sp/profile/PreserveStateDataAction.java | 139 +++++++++++++++++++++
.../shibboleth/sp/state/AbstractStateManager.java | 26 ++--
.../java/net/shibboleth/sp/state/StateData.java | 52 ++------
4 files changed, 197 insertions(+), 60 deletions(-)
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/context/StateDataContext.java b/sp-server-api/src/main/java/net/shibboleth/sp/context/StateDataContext.java
index a6ae775..4d04ccc 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/context/StateDataContext.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/context/StateDataContext.java
@@ -18,6 +18,8 @@ import javax.annotation.Nullable;
import org.opensaml.messaging.context.BaseContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.primitive.StringSupport;
import net.shibboleth.sp.state.StateData;
/**
@@ -25,18 +27,30 @@ import net.shibboleth.sp.state.StateData;
*/
public class StateDataContext extends BaseContext {
- /** The authentication state data. */
+ /** A string token that represents the preserved or "to be recovered" state data. */
+ @Nullable private String stateToken;
+
+ /** The request state data. */
@Nullable private StateData stateData;
/**
- * Sets the state tracked for a request.
+ * Get the token associated with the state after preservation or before recovery.
*
- * @param state state data to set
+ * @return state token
+ */
+ @Nullable @NotEmpty public String getStateToken() {
+ return stateToken;
+ }
+
+ /**
+ * Set the token associated with the state after preservation or before recovery.
*
- * @return this context
+ * @param token state token
+ *
+ * @return this context
*/
- @Nonnull public StateDataContext setAuthnState(@Nullable final StateData state) {
- stateData = state;
+ @Nonnull public StateDataContext setStateToken(@Nullable @NotEmpty final String token) {
+ stateToken = StringSupport.trimOrNull(token);
return this;
}
@@ -45,8 +59,20 @@ public class StateDataContext extends BaseContext {
*
* @return the state data object
*/
- @Nullable public StateData getAuthnState() {
+ @Nullable public StateData getStateData() {
return stateData;
}
+ /**
+ * Sets the state tracked for a request.
+ *
+ * @param state state data to set
+ *
+ * @return this context
+ */
+ @Nonnull public StateDataContext setStateData(@Nullable final StateData state) {
+ stateData = state;
+ return this;
+ }
+
}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/PreserveStateDataAction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/PreserveStateDataAction.java
new file mode 100644
index 0000000..a556081
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/PreserveStateDataAction.java
@@ -0,0 +1,139 @@
+/*
+ * 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;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * Base class for an action that maps {@link StateData} from a {@link StateDataContext} to a token
+ * suitable for safe transmission to a peer such that it can be used later to recover the information.
+ *
+ * <p>Errors may be ignored or result in an {@link EventIds#IO_ERROR} event.</p>
+ *
+ * <p>The action will bypass execution if no {@link StateDataContext} exists or if
+ * {@link StateDataContext#getStateToken()} is non-nul or {@link StateDataContext#getStateData()} is null.</p>
+ *
+ * <p>The default implementation simply generates the token and stores it to the context. Subclasses
+ * may override the {@link #processToken(ProfileRequestContext,String)} action to perform protocol-specific
+ * processing of the token.</p>
+ *
+ * @post ProfileRequestContext.getSubcontext(StateDataContext.class) == null or
+ * ProfileRequestContext.getSubcontext(StateDataContext.class).getStateData() == null or
+ * ProfileRequestContext.getSubcontext(StateDataContext.class).getStateToken() != null
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#IO_ERROR}
+ */
+public class PreserveStateDataAction extends AbstractApplicationAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(PreserveStateDataAction.class);
+
+ /** Whether an error constructing a state token is fatal. */
+ private boolean errorFatal;
+
+ /** Context to operate on. */
+ @NonnullBeforeExec private StateDataContext stateDataContext;
+
+ /**
+ * 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 boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ stateDataContext = profileRequestContext.getSubcontext(StateDataContext.class);
+ if (stateDataContext == null || stateDataContext.getStateData() == null) {
+ log.debug("{} No StateData found, skipping action", getLogPrefix());
+ return false;
+ } else if (stateDataContext.getStateToken() != null) {
+ log.debug("{} State token already established, skipping action", getLogPrefix());
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ // We do the crazy stuff to accomodate cookie-backed state management.
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ try {
+ final StateData data = stateDataContext.getStateData();
+ assert data != null;
+ final String token = ensureApplication().getStateManager().preserveToStateToken(
+ ensureAgent(), ensureApplication(), data);
+ stateDataContext.setStateToken(token);
+ log.debug("{} State data preserved to token: {}", getLogPrefix(), token);
+ } catch (final IOException e) {
+ log.warn("{} Exception preserving state data", getLogPrefix(), e);
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ }
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+ /**
+ * Perform additional processing on the generated state token as required by a particular protocol.
+ *
+ * <p>The default implementation is a no-op.</p>
+ *
+ * @param profileRequestContext profile request context
+ * @param token state token
+ */
+ protected void processToken(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull @NotEmpty final String token) {
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/state/AbstractStateManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/state/AbstractStateManager.java
index 13cd872..4cba0e6 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/state/AbstractStateManager.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/AbstractStateManager.java
@@ -51,6 +51,9 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(AbstractStateManager.class);
+ /** Supplier for the servlet request to read from. */
+ @NonnullAfterInit private NonnullSupplier<HttpServletRequest> httpRequestSupplier;
+
/** Identifier generation. */
@NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
@@ -63,9 +66,6 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
/** Optional component to prevent replay of state. */
@Nullable private ReplayCache replayCache;
- /** Supplier for the servlet request to read from. */
- @NonnullAfterInit private NonnullSupplier<HttpServletRequest> httpRequestSupplier;
-
/** Expiration for state tokens. */
@Nonnull private Duration expiration;
@@ -77,6 +77,16 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
expiration = Duration.ofMinutes(30);
}
+ /**
+ * Set the Supplier for the servlet request to read from.
+ *
+ * @param requestSupplier servlet request supplier
+ */
+ public void setHttpServletRequestSupplier(@Nonnull final NonnullSupplier<HttpServletRequest> requestSupplier) {
+ checkSetterPreconditions();
+ httpRequestSupplier = Constraint.isNotNull(requestSupplier, "HttpServletRequest cannot be null");
+ }
+
/**
* Get {@link IdentifierGenerationStrategy} to use.
*
@@ -152,16 +162,6 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
replayCache = cache;
}
-
- /**
- * Set the Supplier for the servlet request to read from.
- *
- * @param requestSupplier servlet request supplier
- */
- public void setHttpServletRequestSupplier(@Nonnull final NonnullSupplier<HttpServletRequest> requestSupplier) {
- checkSetterPreconditions();
- httpRequestSupplier = Constraint.isNotNull(requestSupplier, "HttpServletRequest cannot be null");
- }
/**
* Get the expiration limit for state tokens.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/state/StateData.java b/sp-server-api/src/main/java/net/shibboleth/sp/state/StateData.java
index 44571c5..cc3e063 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/state/StateData.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/StateData.java
@@ -17,6 +17,7 @@ package net.shibboleth.sp.state;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -29,9 +30,11 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;
+import net.shibboleth.shared.annotation.constraint.Live;
import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.annotation.constraint.Unmodifiable;
import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
/**
* A DTO class that carries protocol state information that needs to be recovered to validate a protocol
@@ -40,10 +43,10 @@ import net.shibboleth.shared.collection.CollectionSupport;
*
* <p>This class is designed to be extended to support non-generic protocol state as required.</p>
*
- * <p>Any URLs managed by this class are typed internally as String and are opaque to this class so as to
+ * <p>The resource URL is typed internally as String and is opaque to this class so as to
* support any character encoding necessary without assuming UTF-8. Callers should take care to encode
- * any URLs as required (e.g., even base64 encoding is acceptable), or use the methods suitable for
- * operating to and from byte arrays to allow the class to address it.</p>
+ * the URL as required (e.g., even base64 encoding is acceptable), or use the method suitable for
+ * operating to and from byte array to allow the class to address it.</p>
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@NotThreadSafe
@@ -72,7 +75,7 @@ public class StateData {
/** Constructor.*/
public StateData() {
- acrs = CollectionSupport.emptyList();
+ acrs = new ArrayList<>();
}
/**
@@ -169,12 +172,12 @@ public class StateData {
}
/**
- * Get the authentication context classes requested.
+ * Get the mutable list of authentication context classes requested.
*
* @return the context classes
*/
@JsonProperty("acrs")
- @Nonnull @Unmodifiable @NotLive public List<String> getAcrs() {
+ @Nonnull @Live public List<String> getAcrs() {
return acrs;
}
@@ -187,9 +190,9 @@ public class StateData {
*/
@Nonnull public StateData setAcrs(@Nullable final List<String> refs) {
if (refs != null) {
- acrs = CollectionSupport.copyToList(refs);
+ acrs = new ArrayList<>(StringSupport.normalizeStringCollection(refs));
} else {
- acrs = CollectionSupport.emptyList();
+ acrs = new ArrayList<>();
}
return this;
}
@@ -216,38 +219,6 @@ public class StateData {
return this;
}
- /**
- * Get the location to which the response is expected to be sent in a raw form.
- *
- * <p>The value retrieved must have previously been set via the
- * {@link StateData#setRawResponseLocation(byte[])} method.</p>
- *
- * @return the expected response location as a byte array
- */
- @Nullable public byte[] getRawResponseLocation() {
- if (responseLocation != null) {
- return decode(responseLocation);
- } else {
- return null;
- }
- }
-
- /**
- * Set the location to which the response is expected to be sent in a raw form.
- *
- * @param loc the location as a byte array
- *
- * @return the updated object
- */
- public StateData setRawResponseLocation(@Nullable final byte[] loc) {
- if (loc != null) {
- responseLocation = encode(loc);
- } else {
- responseLocation = null;
- }
- return this;
- }
-
/**
* Get the resource location associated with the request.
*
@@ -277,6 +248,7 @@ public class StateData {
*
* @return resource as a byte array
*/
+ @JsonIgnore
@Nullable public byte[] getRawResource() {
if (resource != null) {
return decode(resource);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list