[java-plugin-shibd] branch main updated: Redesign of state management and porting over cookie-backed version.

Codeberg noreply at shibboleth.net
Mon Apr 13 20:48:06 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/7987472e8e3b6f4c8818e230d88c90e9efaf8ca0

The following commit(s) were added to refs/heads/main by this push:
     new 7987472  Redesign of state management and porting over cookie-backed version.
7987472 is described below

commit 7987472e8e3b6f4c8818e230d88c90e9efaf8ca0
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Mon Apr 13 16:47:30 2026 -0400

    Redesign of state management and porting over cookie-backed version.
---
 pom.xml                                            |   8 +
 sp-server-api/pom.xml                              |  21 ++
 .../shibboleth/sp/context/StateDataContext.java    |  52 ++++
 .../sp/profile/AbstractStateTokenManager.java      |   3 +
 .../shibboleth/sp/profile/StateTokenManager.java   |   3 +
 .../shibboleth/sp/state/AbstractStateManager.java  | 293 +++++++++++++++++++++
 .../java/net/shibboleth/sp/state/StateData.java    | 263 ++++++++++++++++++
 .../StateManager.java}                             |  26 +-
 .../java/net/shibboleth/sp/state/package-info.java |  18 ++
 .../sp/impl/CookieStateTokenManager.java           |   3 +
 .../sp/impl/PassthroughStateTokenManager.java      |   3 +
 .../sp/impl/StorageServiceStateTokenManager.java   |   3 +
 .../impl/CookieStateManager.java}                  |  65 ++---
 .../net/shibboleth/sp/state/impl/package-info.java |  18 ++
 .../sp/impl/CookieStateTokenManagerTest.java       |   4 +-
 .../sp/impl/PassthroughStateTokenManagerTest.java  |   4 +-
 .../impl/StorageServiceStateTokenManagerTest.java  |   4 +-
 ...ionTest.java => BaseApplicationActionTest.java} |   2 +-
 .../profile/impl/IssueCorrelationCookieTest.java   |   2 +-
 .../sp/profile/impl/IssueDiscoveryRequestTest.java |   2 +-
 .../profile/impl/MapResourceToStateTokenTest.java  |   2 +-
 .../sp/profile/impl/PreservePostDataTest.java      |   2 +-
 .../profile/impl/ProcessCorrelationCookieTest.java |   2 +-
 .../sp/profile/impl/RecoverPostDataTest.java       |   2 +-
 .../impl/ValidateSessionInitiatorRequestTest.java  |   2 +-
 .../impl/CookieStateManagerTest.java}              |  45 +++-
 26 files changed, 782 insertions(+), 70 deletions(-)

diff --git a/pom.xml b/pom.xml
index 3538031..a196d4f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -123,6 +123,14 @@
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <!-- Jackson BOM -->
+            <dependency>
+                <groupId>com.fasterxml.jackson</groupId>
+                <artifactId>jackson-bom</artifactId>
+                <version>${jackson.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/sp-server-api/pom.xml b/sp-server-api/pom.xml
index 7ece971..ab81bce 100644
--- a/sp-server-api/pom.xml
+++ b/sp-server-api/pom.xml
@@ -52,6 +52,27 @@
             <scope>provided</scope>
         </dependency>
         
+         <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jdk8</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
         <dependency>
             <groupId>${spring.groupId}</groupId>
             <artifactId>spring-beans</artifactId>
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
new file mode 100644
index 0000000..a6ae775
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/context/StateDataContext.java
@@ -0,0 +1,52 @@
+/*
+ * 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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * A context to hold state information about a request.
+ */
+public class StateDataContext extends BaseContext {
+    
+    /** The authentication state data. */
+    @Nullable private StateData stateData;
+    
+    /**
+     * Sets the state tracked for a request.
+     * 
+     * @param state state data to set
+     * 
+     * @return this context 
+     */
+    @Nonnull public StateDataContext setAuthnState(@Nullable final StateData state) {      
+        stateData = state;
+        return this;
+    }
+    
+    /**
+     * Get the state tracked for a request.
+     * 
+     * @return the state data object
+     */
+    @Nullable public StateData getAuthnState() {
+        return stateData;
+    }
+
+}
\ No newline at end of file
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 9ac8b90..5580661 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
@@ -27,7 +27,10 @@ import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
 
 /**
  * Base class for {@link StateTokenManager} implementations.
+ * 
+ * @deprecated
  */
+ at Deprecated
 public abstract class AbstractStateTokenManager extends AbstractIdentifiableInitializableComponent
         implements StateTokenManager {
     
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
index 83ae7d3..f542405 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
@@ -33,7 +33,10 @@ import net.shibboleth.sp.Application;
  * <p>There are multiple possible implementations of this concept, some involving cookies.</p>
  * 
  * <p>The value type is a byte array to accomodate non-Unicode data from agents.</p>
+ * 
+ * @deprecated
  */
+ at Deprecated
 public interface StateTokenManager {
     
     /**
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
new file mode 100644
index 0000000..9dae5d8
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/AbstractStateManager.java
@@ -0,0 +1,293 @@
+/*
+ * 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.state;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.ReplayCache;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.Application;
+
+/**
+ * Base class for {@link StateManager} implementations.
+ */
+public abstract class AbstractStateManager extends AbstractIdentifiableInitializableComponent
+        implements StateManager {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractStateManager.class);
+    
+    /** Identifier generation. */
+    @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
+
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Optional component to protect the data from tampering/visibility. */
+    @Nullable private DataSealer dataSealer;
+
+    /** Optional component to prevent replay of state. */
+    @Nullable private ReplayCache replayCache;
+    
+    /** Expiration for state tokens. */
+    @Nonnull private Duration expiration;
+    
+    /**
+     * Constructor.
+     */
+    @SuppressWarnings("null")
+    public AbstractStateManager() {
+        expiration = Duration.ofMinutes(30);
+    }
+    
+    /**
+     * Get {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @return identifier generator strategy
+     */
+    @NonnullAfterInit public IdentifierGenerationStrategy getIdentifierGenerationStrategy() {
+        return identifierStrategy;
+    }
+    
+    /**
+     * 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 JSON {@link ObjectMapper} to use for serialization.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+    
+    /**
+     * Gets the {@link DataSealer} to use to protect data from tampering/visbility.
+     * 
+     * @return data sealer or null
+     */
+    @Nullable public DataSealer getDataSealer() {
+        return dataSealer;
+    }
+    
+    /**
+     * Sets the {@link DataSealer} to use to protect data from tampering/visbility.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        checkSetterPreconditions();
+        
+        dataSealer = sealer;
+    }
+
+    /**
+     * Gets the {@link ReplayCache} to use to prevent replay of state.
+     * 
+     * @return replay cache or null
+     */
+    @Nullable public ReplayCache getReplayCache() {
+        return replayCache;
+    }
+    
+    /**
+     * Sets the {@link ReplayCache} to use to prevent replay of state.
+     * 
+     * <p>This is an additional layer of protection over and above the clearing of
+     * state that takes place routinely.</p>
+     * 
+     * @param cache replay cache
+     */
+    public void setReplayCache(@Nullable final ReplayCache cache) {
+        checkSetterPreconditions();
+        
+        replayCache = cache;
+    }
+    
+    /**
+     * Get the expiration limit for state tokens.
+     * 
+     * @return expiration limit
+     */
+    @Nonnull public Duration getExpiration() {
+        return expiration;
+    }
+    
+    /**
+     * Set the expiration limit for state tokens.
+     * 
+     * <p>Defaults to PT30M.</p>
+     * 
+     * @param exp expiration limit
+     */
+    public void setExpiration(@Nonnull final Duration exp) {
+        checkSetterPreconditions();
+        
+        expiration = Constraint.isNotNull(exp, "Expiration cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+        
+        if (identifierStrategy == null) {
+            identifierStrategy = IdentifierGenerationStrategy.getInstance(ProviderType.SECURE);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final StateData data)
+            throws IOException {
+        
+        try {
+            // Serialize the input data.
+            final String serializedState = objectMapper.writeValueAsString(data);
+            if (serializedState == null) {
+                throw new IOException("Data could not be serialized into JSON.");
+            }
+            final DataSealer localDataSealer = dataSealer;
+            if (localDataSealer != null) {
+                log.debug("State data will be sealed before preservation");
+                return doPreserve(agent, application, localDataSealer.wrap(serializedState,
+                        Instant.now().plus(expiration)));
+            } else {
+                return doPreserve(agent, application, serializedState);
+            }
+        } catch (final JsonProcessingException | DataSealerException e) {
+            throw new IOException("Error preserving state", e);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public <T extends StateData> T recoverFromStateToken(@Nonnull final Agent agent,
+            @Nonnull final Application application, @Nonnull @NotEmpty final String token,
+            @Nonnull final Class<T> type) throws IOException {
+        
+        final ReplayCache localCache = replayCache;
+        if (localCache != null) {
+            log.debug("Checking state token '{}' for replay", token);
+            if (!localCache.check(getClass().getName() + '!' + agent.getId() + '!' + application.getApplicationId(),
+                    token, Instant.now().plus(expiration))) {
+                log.warn("Replay of state token '{}' detected", token);
+                return null;
+            }
+        }
+        
+        String data = doRecover(agent, application, token);
+        if (data == null) {
+            return null;
+        }
+        
+        try {
+            final DataSealer localDataSealer = dataSealer;
+            if (localDataSealer != null) {
+                log.debug("Unwrapping sealed state data after recovery from token '{}'", token);
+                data = localDataSealer.unwrap(data);
+            }
+            
+            final T stateData = objectMapper.readValue(data, type);
+            final Instant issued = stateData.getRequestTime();
+            if (issued != null && issued.plus(expiration).isBefore(Instant.now())) {
+                log.warn("State data for token '{}' has expired", token);
+                return null;
+            }
+            return stateData;
+            
+        } catch (final DataSealerException|JsonProcessingException e) {
+            throw new IOException("Error unwrapping or parsing state", e);
+        }
+    }
+    
+    /**
+     * Subclasses implement this method to preserve the transformed data in whatever way is necessary and return
+     * a token.
+     * 
+     * @param agent agent owning the state
+     * @param application application owning the state
+     * @param data data to preserve
+     * 
+     * @return the state token
+     * 
+     * @throws IOException if an error occurs
+     */
+    @Nonnull protected abstract String doPreserve(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final String data) throws IOException;
+
+    /**
+     * Subclasses implement this method to recover the stored data in whatever way is necessary and return
+     * the supplied data string as a successful result.
+     * 
+     * <p>Subclasses may assume that the state token inputs they receive will have been returned by them
+     * via the {@link #doPreserve(Agent, Application, String)} method.</p> 
+     * 
+     * @param agent agent owning the state
+     * @param application application owning the state
+     * @param stateToken the state token to map from/clear
+     * 
+     * @return the recovered data, or null if unable to recover without underlying cause
+     * 
+     * @throws IOException if an error occurs
+     */
+    @Nullable protected abstract String doRecover(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull @NotEmpty final String stateToken) throws IOException;
+    
+    /**
+     * Generate a state token.
+     * 
+     * @return a new state token
+     */
+    @Nonnull protected String generateToken() {
+        return identifierStrategy.generateIdentifier(false);
+    }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..aa7b6d3
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/StateData.java
@@ -0,0 +1,263 @@
+/*
+ * 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.state;
+
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A DTO class that carries protocol state information that needs to be recovered to validate a protocol
+ * response. This class is designed for JSON serialization and deserialization for storage e.g. in a cookie
+ * or database.
+ * 
+ * <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 as String and strictly opaque to this layer 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).</p>
+ */
+ at JsonInclude(JsonInclude.Include.NON_EMPTY)
+ at NotThreadSafe
+public class StateData {
+    
+    /** The identifier of the party that is making a request, i.e., us. */
+    @Nullable private String issuer;
+
+    /** The expected issuer of the authentication response. */
+    @Nullable private String authenticatingAuthority;
+    
+    /** The time at which a request was issued. */
+    @Nullable private Instant requestTime;
+    
+    /** List of authentication context class references requested in the authentication request. */
+    @Nonnull @Unmodifiable @NotLive private List<String> acrs;
+    
+    /** The location to which a response is expected. */
+    @Nullable private String responseLocation;
+    
+    /** A resource location associated with the request. */
+    @Nullable private String resource;
+    
+    /** Constructor.*/
+    public StateData() {
+        acrs = CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Get the identifier of the party that is making the request, i.e., us.
+     * 
+     * <p>Can be used to ensure the audience of the response matches the client that made the request.</p>
+     * 
+     * @return the issuer
+     */
+    @JsonProperty("issuer")
+    @Nullable public String getIssuer() {
+        return issuer;
+    }
+
+    /**
+     * Set the identifier of the client that is making the authentication request. Can be used to ensure the audience
+     * of the response matches the client that made the request.
+     * 
+     * @param id The client ID to set.
+     * 
+     * @return the updated object
+     */
+    @Nonnull public StateData setIssuer(@Nullable final String id) {
+        issuer = id;
+        return this;
+    }
+    
+    /**
+     * Get the expected issuer of the authentication response.
+     * 
+     * @return the authentication authority
+     */
+    @JsonProperty("authority")
+    @Nullable public String getAuthenticationAuthority() {
+        return authenticatingAuthority;
+    }
+
+    /**
+     * Set the expected issuer of the authentication response.
+     * 
+     * @param authority the authentication authority to set
+     * 
+     * @return the updated object
+     */
+    @Nonnull public StateData setAuthenticationAuthority(@Nullable final String authority) {
+        authenticatingAuthority = authority;
+        return this;
+    }
+    
+    /**
+     * Get the time at which the request was issued.
+     * 
+     * @return the time the request was issued
+     */
+    @JsonProperty("req_time")
+    @Nullable public Instant getRequestTime() {
+        return requestTime;
+    }
+
+    /**
+     * Set the time at which the request was issued.
+     * 
+     * @param time the time the request was issued
+     * 
+     * @return the updated object
+     */
+    @Nonnull public StateData setRequestTime(@Nullable final Instant time) {
+        requestTime = time;
+        return this;
+    }
+    
+    /**
+     * Get the context classes requested.
+     * 
+     * @return the context classes
+     */
+    @JsonProperty("acrs")
+    @Nonnull @Unmodifiable @NotLive public List<String> getAcrs() {
+        return acrs;
+    }
+
+    /**
+     * Set the context classes requested.
+     * 
+     * @param refs context class references
+     * 
+     * @return the updated object
+     */
+    @Nonnull public StateData setAcrs(@Nullable final List<String> refs) {
+        if (refs != null) {
+            acrs = CollectionSupport.copyToList(refs);
+        } else {
+            acrs = CollectionSupport.emptyList();
+        }
+        return this;
+    }
+    /**
+     * Get the location to which the response is expected to be sent.
+     * 
+     * @return the expected response location
+     */
+    @JsonProperty("resp_loc")
+    @Nullable public String getResponseLocation() {
+        return responseLocation;
+    }
+    
+    /**
+     * Set the location to which the response is expected to be sent.
+     * 
+     * @param loc the location
+     * 
+     * @return the updated object
+     */
+    public StateData setResponseLocation(@Nullable final String loc) {
+        responseLocation = loc;
+        return this;
+    }
+
+    /**
+     * Get the resource location associated with the request.
+     * 
+     * @return the resource location
+     */
+    @JsonProperty("resource")
+    @Nullable public String getResource() {
+        return resource;
+    }
+    
+    /**
+     * Set the resource location associated with the request.
+     * 
+     * @param loc the location
+     * 
+     * @return the updated object
+     */
+    public StateData setResource(@Nullable final String loc) {
+        resource = loc;
+        return this;
+    }
+
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(acrs, authenticatingAuthority, issuer, requestTime, resource, responseLocation);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        final StateData other = (StateData) obj;
+        return Objects.equals(acrs, other.acrs)
+                && Objects.equals(authenticatingAuthority, other.authenticatingAuthority)
+                && Objects.equals(issuer, other.issuer) && Objects.equals(requestTime, other.requestTime)
+                && Objects.equals(resource, other.resource) && Objects.equals(responseLocation, other.responseLocation);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("issuer", issuer)
+                .add("authenticatingAuthority", authenticatingAuthority)
+                .add("requestTime", requestTime)
+                .add("acrs", acrs)
+                .add("reponseLocation", responseLocation)
+                .add("resource", resource)
+                .toString();
+    }
+    
+    /**
+     * A method to mask all but the last 2 characters of a string value for logging purposes.
+     * 
+     * @param value the string value to mask
+     * @return the masked string
+     */
+    protected static String mask(final String value) {
+        if (value == null) {
+            return null;
+        }
+        if (value.length() <= 4) {
+            return "****";
+        }
+        return "****" + value.substring(value.length() - 2);
+    }
+   
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java b/sp-server-api/src/main/java/net/shibboleth/sp/state/StateManager.java
similarity index 71%
copy from sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
copy to sp-server-api/src/main/java/net/shibboleth/sp/state/StateManager.java
index 83ae7d3..a6d2e23 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/StateManager.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.profile;
+package net.shibboleth.sp.state;
 
 import java.io.IOException;
 
@@ -30,41 +30,43 @@ import net.shibboleth.sp.Application;
  * <p>SAML refers to this notion as <em>RelayState</em>, while OpenID Connect just refers to it
  * as <em>state</em>.</p>
  * 
- * <p>There are multiple possible implementations of this concept, some involving cookies.</p>
- * 
- * <p>The value type is a byte array to accomodate non-Unicode data from agents.</p>
+ * <p>There are multiple possible implementations of this concept, all involving cookies to
+ * provide CSRF protection.</p>
  */
-public interface StateTokenManager {
+public interface StateManager {
     
     /**
-     * Preserves a value by transforming it into a state token.
+     * Preserves data while transforming it into a state token.
      * 
      * @param agent agent owning the state
      * @param application application owning the state
-     * @param value input value to preserve
+     * @param data data to preserve
      * 
      * @return state token representing value
      * 
      * @throws IOException if creation of token fails 
      */
     @Nonnull String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final byte[] value) throws IOException;
+            @Nonnull final StateData data) throws IOException;
 
     /**
-     * Recovers a value from a state token.
+     * Recovers stored data from a state token.
      * 
      * <p>In most implementations, the state token mapping should be cleared on successful use of this
      * method.</p>
      * 
+     * @param <T> type of state
+     * 
      * @param agent agent owning the state
      * @param application application owning the state
      * @param token state token
+     * @param type specific subtype of {@link StateData} to recover
      * 
-     * @return the recovered value, or null if unable to recover without underlying cause
+     * @return the recovered data, or null if unable to recover without underlying cause
      * 
      * @throws IOException if recovery from token fails
      */
-    @Nullable byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String token) throws IOException;
+    @Nullable <T extends StateData> T recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final String token, @Nonnull Class<T> type) throws IOException;
 
 }
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/state/package-info.java b/sp-server-api/src/main/java/net/shibboleth/sp/state/package-info.java
new file mode 100644
index 0000000..8f61e3d
--- /dev/null
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/state/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * APIs supporting state management for SSO protocols.
+ */
+package net.shibboleth.sp.state;
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
index e281d61..ab832d3 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
@@ -44,7 +44,10 @@ import net.shibboleth.sp.profile.StateTokenManager;
 
 /**
  * {@link StateTokenManager} implemented using cookies.
+ * 
+ * @deprecated
  */
+ at Deprecated
 public class CookieStateTokenManager extends AbstractStateTokenManager {
     
     /** Default cookie prefix. */
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/PassthroughStateTokenManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/PassthroughStateTokenManager.java
index 9649c8a..c00b485 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/PassthroughStateTokenManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/PassthroughStateTokenManager.java
@@ -32,7 +32,10 @@ import net.shibboleth.sp.profile.StateTokenManager;
 
 /**
  * {@link StateTokenManager} implemented as a simple pass-through that doesn't mask the data.
+ * 
+ * @deprecated
  */
+ at Deprecated
 public class PassthroughStateTokenManager extends AbstractIdentifiableInitializableComponent
         implements StateTokenManager {
     
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 aa5f095..d89a6c1 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
@@ -38,7 +38,10 @@ import net.shibboleth.sp.profile.StateTokenManager;
 
 /**
  * {@link StateTokenManager} implemented with a {@link StorageService}.
+ * 
+ * @deprecated
  */
+ at Deprecated
 public class StorageServiceStateTokenManager extends AbstractStateTokenManager {
     
     /** Class logger. */
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
similarity index 80%
copy from sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
copy to sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
index e281d61..5753412 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
@@ -12,9 +12,10 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.impl;
+package net.shibboleth.sp.state.impl;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
@@ -39,19 +40,19 @@ import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
 import net.shibboleth.shared.security.RandomIdentifierParameterSpec;
 import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.Application;
-import net.shibboleth.sp.profile.AbstractStateTokenManager;
-import net.shibboleth.sp.profile.StateTokenManager;
+import net.shibboleth.sp.state.AbstractStateManager;
+import net.shibboleth.sp.state.StateManager;
 
 /**
- * {@link StateTokenManager} implemented using cookies.
+ * {@link StateManager} implemented by storing the data directly into a cookie.
  */
-public class CookieStateTokenManager extends AbstractStateTokenManager {
+public class CookieStateManager extends AbstractStateManager {
     
     /** Default cookie prefix. */
-    @Nonnull @NotEmpty public static String DEFAULT_PREFIX = "_shibsp_state_";
+    @Nonnull @NotEmpty public static String DEFAULT_PREFIX = "_shibsp_state";
     
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(CookieStateTokenManager.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(CookieStateManager.class);
 
     /** Cookie manager. */
     @NonnullAfterInit private CookieManager cookieManager;
@@ -60,7 +61,7 @@ public class CookieStateTokenManager extends AbstractStateTokenManager {
     @Nonnull @NotEmpty private String cookiePrefix;
     
     /** Constructor. */
-    public CookieStateTokenManager() {
+    public CookieStateManager() {
         cookiePrefix = DEFAULT_PREFIX;
     }
 
@@ -91,12 +92,7 @@ public class CookieStateTokenManager extends AbstractStateTokenManager {
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (cookieManager == null) {
-            throw new ComponentInitializationException("CookieManager cannot be null");
-        }
-        
+
         if (getIdentifierGenerationStrategy() == null) {
             final RandomIdentifierParameterSpec spec = new RandomIdentifierParameterSpec(null, 6, null);
             try {
@@ -105,12 +101,19 @@ public class CookieStateTokenManager extends AbstractStateTokenManager {
                 throw new ComponentInitializationException(e);
             }
         }
+        
+        super.doInitialize();
+        
+        if (cookieManager == null) {
+            throw new ComponentInitializationException("CookieManager cannot be null");
+        }
     }
-
+    
     /** {@inheritDoc} */
-    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final byte[] value) throws IOException {
-
+    @Override
+    @Nonnull protected String doPreserve(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull final String data) throws IOException {
+        
         cookieManager.purgeStaleCookies(cookiePrefix);
         
         final Instant ts = Instant.now();
@@ -119,41 +122,39 @@ public class CookieStateTokenManager extends AbstractStateTokenManager {
         final String key = Long.toString(ts.toEpochMilli()) + '_' + generateToken();
         final String name = getCookieName(application, key);
         try {
-            cookieManager.addCookie(name, Base64Support.encodeURLSafe(value), (int) getExpiration().toSeconds());
+            cookieManager.addCookie(name, Base64Support.encodeURLSafe(data.getBytes(StandardCharsets.UTF_8)),
+                    (int) getExpiration().toSeconds());
         } catch (final EncodingException e) {
             throw new IOException(e);
         }
 
-        log.trace("Created state token mapping from '{}' to value '{}'", name, value);
+        log.trace("Created state token mapping from '{}' to value '{}'", name, data);
         
         return key;
     }
 
     /** {@inheritDoc} */
-    @Nullable public byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String token) throws IOException {
-        
-        if (token.isEmpty()) {
-            log.warn("Invalid state token: '{}'", token);
-            return null;
-        }
+    @Override
+    @Nullable protected String doRecover(@Nonnull final Agent agent, @Nonnull final Application application,
+            @Nonnull @NotEmpty final String stateToken) throws IOException {
         
-        final String cookieName = getCookieName(application, token);
+        final String cookieName = getCookieName(application, stateToken);
         final String cookieValue = cookieManager.getCookieValue(cookieName, null);
 
         if (cookieValue != null) {
-            log.trace("Recovered state token mapping from '{}' to value '{}'", token, cookieValue);
             cookieManager.unsetCookie(cookieName);
             try {
-                return Base64Support.decodeURLSafe(cookieValue);
+                final String decoded = new String(Base64Support.decodeURLSafe(cookieValue), StandardCharsets.UTF_8);
+                log.trace("Recovered state token mapping from '{}' to value '{}'", stateToken, decoded);
+                return decoded;
             } catch (final DecodingException e) {
                 throw new IOException(e);
             }
         }
 
-        log.warn("No cookie found matching state token: '{}'", token);
+        log.warn("No cookie found matching state token: '{}'", stateToken);
         return null;
-    }
+    }    
     
     /**
      * Computes the name of a new state cookie.
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/package-info.java b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/package-info.java
new file mode 100644
index 0000000..e3a6e60
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Implementation of protocol state management.
+ */
+package net.shibboleth.sp.state.impl;
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
index a7015cb..2952d7a 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
@@ -36,13 +36,13 @@ 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.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
 
 /**
  * Unit tests for {@link CookieStateTokenManager}.
  */
 @SuppressWarnings("javadoc")
-public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
+public class CookieStateTokenManagerTest extends BaseApplicationActionTest {
 
     private CookieManager cookieManager;
     private CookieStateTokenManager stateManager;
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/PassthroughStateTokenManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/PassthroughStateTokenManagerTest.java
index d20e3f3..c78bfcb 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/PassthroughStateTokenManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/PassthroughStateTokenManagerTest.java
@@ -23,13 +23,13 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
 
 /**
  * Unit tests for {@link PassthroughStateTokenManager}.
  */
 @SuppressWarnings("javadoc")
-public class PassthroughStateTokenManagerTest extends BaseAgplicationActionTest {
+public class PassthroughStateTokenManagerTest extends BaseApplicationActionTest {
 
     private PassthroughStateTokenManager stateManager;
     
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/StorageServiceStateTokenManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/StorageServiceStateTokenManagerTest.java
index dc90a45..e9f1a0c 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/StorageServiceStateTokenManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/impl/StorageServiceStateTokenManagerTest.java
@@ -25,13 +25,13 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
 
 /**
  * Unit tests for {@link StorageServiceStateTokenManager}.
  */
 @SuppressWarnings("javadoc")
-public class StorageServiceStateTokenManagerTest extends BaseAgplicationActionTest {
+public class StorageServiceStateTokenManagerTest extends BaseApplicationActionTest {
 
     private MemoryStorageService storageService;
     private StorageServiceStateTokenManager stateManager;
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgplicationActionTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseApplicationActionTest.java
similarity index 94%
rename from sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgplicationActionTest.java
rename to sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseApplicationActionTest.java
index 7b60882..7be5a60 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgplicationActionTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseApplicationActionTest.java
@@ -20,7 +20,7 @@ import net.shibboleth.sp.impl.BasicApplication;
 /**
  * Base class for unit tests that rely on set up of an agent request with an application.
  */
-public abstract class BaseAgplicationActionTest extends BaseAgentRequestTest {
+public abstract class BaseApplicationActionTest extends BaseAgentRequestTest {
 
     protected BasicApplication application;
 
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
index 85a56ee..9a89f89 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
@@ -46,7 +46,7 @@ import net.shibboleth.sp.profile.SPConstants;
  * Unit test for {@link IssueCorrelationCookie} action.
  */
 @SuppressWarnings("javadoc")
-public class IssueCorrelationCookieTest extends BaseAgplicationActionTest {
+public class IssueCorrelationCookieTest extends BaseApplicationActionTest {
 
     @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
     @Nonnull @NotEmpty private final static String TEST_ID = "123456789";
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequestTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequestTest.java
index b20b388..089ed29 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequestTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequestTest.java
@@ -43,7 +43,7 @@ import net.shibboleth.sp.profile.SPConstants;
  * Unit test for {@link IssueDiscoveryRequest} action.
  */
 @SuppressWarnings("javadoc")
-public class IssueDiscoveryRequestTest extends BaseAgplicationActionTest {
+public class IssueDiscoveryRequestTest extends BaseApplicationActionTest {
 
     /** Test discovery URL. */
     @Nonnull @NotEmpty private final static String TEST_DISCOVERY_URL = "https://ds.example.org/DS";
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/MapResourceToStateTokenTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/MapResourceToStateTokenTest.java
index 4300171..a38e959 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/MapResourceToStateTokenTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/MapResourceToStateTokenTest.java
@@ -47,7 +47,7 @@ import net.shibboleth.sp.profile.SPConstants;
  * Unit test for {@link MapResourceToStateToken} action.
  */
 @SuppressWarnings("javadoc")
-public class MapResourceToStateTokenTest extends BaseAgplicationActionTest {
+public class MapResourceToStateTokenTest extends BaseApplicationActionTest {
 
     /** Test request URL. */
     @Nonnull @NotEmpty private final static String TEST_URL = "https://sp.example.org/cgi-bin/test.cgi";
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 ddb4227..2fd2828 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
@@ -47,7 +47,7 @@ import net.shibboleth.sp.profile.SPConstants;
  * Unit test for {@link PreservePostData} action.
  */
 @SuppressWarnings("javadoc")
-public class PreservePostDataTest extends BaseAgplicationActionTest {
+public class PreservePostDataTest extends BaseApplicationActionTest {
 
     @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
     @Nonnull @NotEmpty private final static String TEST_DATA = "foo=bar&zorkmid=a+b";
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookieTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookieTest.java
index 3b37c5d..9c24163 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookieTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookieTest.java
@@ -45,7 +45,7 @@ import net.shibboleth.sp.ddf.DDF;
  * Unit test for {@link ProcessCorrelationCookie} action.
  */
 @SuppressWarnings("javadoc")
-public class ProcessCorrelationCookieTest extends BaseAgplicationActionTest {
+public class ProcessCorrelationCookieTest extends BaseApplicationActionTest {
 
     @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
     @Nonnull @NotEmpty private final static String TEST_ID = "123456789";
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
index 283c51d..8e9a1c7 100644
--- 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
@@ -59,7 +59,7 @@ import net.shibboleth.sp.profile.SPConstants;
  * Unit test for {@link RecoverPostData} action.
  */
 @SuppressWarnings("javadoc")
-public class RecoverPostDataTest extends BaseAgplicationActionTest {
+public class RecoverPostDataTest extends BaseApplicationActionTest {
 
     @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
     @Nonnull @NotEmpty private final static String TEST_DATA = "foo=bar&zorkmid=a+b&frobnitz=c:d";
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ValidateSessionInitiatorRequestTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ValidateSessionInitiatorRequestTest.java
index 5e3073f..d88be25 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ValidateSessionInitiatorRequestTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ValidateSessionInitiatorRequestTest.java
@@ -37,7 +37,7 @@ import net.shibboleth.sp.profile.InitiatorConstants;
  * Unit test for {@link ValidateSessionInitiatorRequest} action.
  */
 @SuppressWarnings("javadoc")
-public class ValidateSessionInitiatorRequestTest extends BaseAgplicationActionTest {
+public class ValidateSessionInitiatorRequestTest extends BaseApplicationActionTest {
 
     /** Test value. */
     @Nonnull @NotEmpty private final static String TEST_VALUE = "https://idp.example.org";
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
similarity index 75%
copy from sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
copy to sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
index a7015cb..a670fdc 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.impl;
+package net.shibboleth.sp.state.impl;
 
 import java.io.IOException;
 import java.time.Instant;
@@ -29,23 +29,32 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
 import jakarta.servlet.http.Cookie;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 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.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
+import net.shibboleth.sp.state.StateData;
 
 /**
- * Unit tests for {@link CookieStateTokenManager}.
+ * Unit tests for {@link CookieStateManager}.
  */
 @SuppressWarnings("javadoc")
-public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
+public class CookieStateManagerTest extends BaseApplicationActionTest {
+
+    @Nonnull @NotEmpty private static final String TEST_ISSUER = "https://sp.example.org";
 
+    @Nonnull @NotEmpty private static final String TEST_AUTHORITY = "https://idp.example.org";
+    
     private CookieManager cookieManager;
-    private CookieStateTokenManager stateManager;
+    private CookieStateManager stateManager;
     
     private MockHttpServletRequest request;
     private MockHttpServletResponse response;
@@ -71,8 +80,11 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
         });
         cookieManager.initialize();
         
-        stateManager = new CookieStateTokenManager();
+        stateManager = new CookieStateManager();
         stateManager.setId("test");
+        final ObjectMapper mapper = new ObjectMapper();
+        mapper.registerModule(new JavaTimeModule());
+        stateManager.setObjectMapper(mapper);
         stateManager.setCookieManager(cookieManager);
         
         stateManager.initialize();
@@ -96,7 +108,7 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
     public void testMissing() throws IOException {
         request.setCookies(new Cookie(getCookieName(), "foo"));
         
-        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo"));
+        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo", StateData.class));
     }
 
     @Test
@@ -109,7 +121,7 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
         }
         request.setCookies(cookies.toArray(new Cookie[12]));
         
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
+        final String token = stateManager.preserveToStateToken(agent, application, buildStateData());
         assert token != null;
         
         final Cookie[] respCookies = response.getCookies();
@@ -122,7 +134,8 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
     @Test
     public void testMapRecover() throws IOException {
         
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
+        final StateData source = buildStateData();
+        final String token = stateManager.preserveToStateToken(agent, application, source);
         assert token != null;
         
         // Move token set on response to request.
@@ -130,13 +143,13 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
         request.setCookies(response.getCookies());
         response = new MockHttpServletResponse();
         
-        final byte[] original = stateManager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(original, "foo".getBytes());
+        final StateData recovered = stateManager.recoverFromStateToken(agent, application, token, StateData.class);
+        Assert.assertEquals(source, recovered);
         
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateTokenManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
@@ -147,5 +160,13 @@ public class CookieStateTokenManagerTest extends BaseAgplicationActionTest {
         final String rand = stateManager.getIdentifierGenerationStrategy().generateIdentifier(false);
         return stateManager.getCookieName(application, now.toEpochMilli() + '_' + rand);
     }
+    
+    @Nonnull private StateData buildStateData() {
+        final StateData data = new StateData();
+        data.setRequestTime(Instant.now());
+        data.setIssuer(TEST_ISSUER);
+        data.setAuthenticationAuthority(TEST_AUTHORITY);
+        return data;
+    }
 
 }
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list