[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