[java-plugin-shibd] branch main updated: Merge state tracking redesign from dev branch.

Codeberg noreply at shibboleth.net
Wed May 6 15:10:35 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/2683e8624d8d9ac8bb22c05b19367aef20959ee2

The following commit(s) were added to refs/heads/main by this push:
     new 2683e86  Merge state tracking redesign from dev branch.
2683e86 is described below

commit 2683e8624d8d9ac8bb22c05b19367aef20959ee2
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed May 6 11:05:11 2026 -0400

    Merge state tracking redesign from dev branch.
---
 .../idp/flows/sp/initiator/disco/disco-beans.xml   |   8 +
 .../idp/flows/sp/initiator/disco/disco-flow.xml    |   3 +
 .../idp/flows/sp/initiator/initiator-beans.xml     |   5 +-
 .../sp/logout-initiator/logout-initiator-beans.xml |   6 -
 .../sp/logout-initiator/logout-initiator-flow.xml  |   1 -
 .../flows/sp/logout/initiator/initiator-beans.xml  |  11 --
 .../session-initiator/session-initiator-beans.xml  |  11 +-
 .../session-initiator/session-initiator-flow.xml   |   2 +-
 .../sp/token-consumer/token-consumer-flow.xml      |   4 -
 .../shibboleth/idp/module/conf/sp/sp.properties    |   7 +-
 .../net/shibboleth/sp/conf/agents-system.xml       |  21 +-
 .../sp/flows/LogoutInitiatorFlowTest.java          |  19 --
 .../sp/flows/SessionInitiatorFlowTest.java         |  72 +++++++
 ...SPEnvironmentApplicationContextInitializer.java |   3 +
 .../main/java/net/shibboleth/sp/Application.java   |  11 --
 .../sp/context/TokenConsumerContext.java           |  55 ------
 .../sp/profile/AbstractStateTokenManager.java      | 114 -----------
 .../AbstractTokenConsumerResponseAction.java       |  17 ++
 .../shibboleth/sp/profile/ConsumerConstants.java   |   6 +-
 .../sp/profile/PreserveStateDataAction.java        |   3 +-
 .../net/shibboleth/sp/profile/SPConstants.java     |   3 +
 .../shibboleth/sp/profile/StateTokenManager.java   |  73 -------
 .../MessageCorrelationIDLookupFunction.java        |  33 ----
 .../shibboleth/sp/state/AbstractStateManager.java  |  12 +-
 .../net/shibboleth/sp/impl/BasicApplication.java   |  36 +---
 .../sp/impl/CookieStateTokenManager.java           | 183 ------------------
 .../sp/impl/PassthroughStateTokenManager.java      |  70 -------
 .../sp/impl/StorageServiceStateTokenManager.java   | 139 --------------
 .../sp/profile/impl/EncodeAgentResponse.java       |  50 ++++-
 .../sp/profile/impl/IssueCorrelationCookie.java    | 199 -------------------
 .../sp/profile/impl/IssueDiscoveryRequest.java     |  44 +++--
 .../sp/profile/impl/MapResourceToStateToken.java   | 156 ---------------
 .../sp/profile/impl/MapStateTokenToResource.java   | 134 -------------
 .../sp/profile/impl/PrepareAgentErrorResponse.java |  47 ++++-
 .../impl/PrepareDiscoveryStateDataContext.java     | 114 +++++++++++
 .../sp/profile/impl/PreservePostData.java          | 206 +++++++++++++++-----
 .../sp/profile/impl/ProcessCorrelationCookie.java  | 173 -----------------
 .../sp/profile/impl/RecoverStateData.java          |  80 ++++++--
 .../sp/state/impl/CookieStateManager.java          |  56 +++---
 .../sp/state/impl/StorageServiceStateManager.java  |  10 +-
 .../impl/InitializeAgentRequestContextTest.java    |  18 +-
 .../sp/impl/CookieStateTokenManagerTest.java       | 151 ---------------
 .../sp/impl/PassthroughStateTokenManagerTest.java  |  69 -------
 .../impl/StorageServiceStateTokenManagerTest.java  |  80 --------
 .../sp/profile/impl/BaseAgentRequestTest.java      |   6 -
 .../profile/impl/IssueCorrelationCookieTest.java   | 163 ----------------
 .../sp/profile/impl/IssueDiscoveryRequestTest.java |  11 +-
 .../profile/impl/MapResourceToStateTokenTest.java  | 213 ---------------------
 .../sp/profile/impl/PreservePostDataTest.java      |  72 ++++++-
 .../profile/impl/ProcessCorrelationCookieTest.java | 135 -------------
 .../sp/state/impl/CookieStateManagerTest.java      | 100 ++++++++--
 .../state/impl/StorageServiceStateManagerTest.java | 100 ++++++++--
 52 files changed, 873 insertions(+), 2442 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-beans.xml
index 0cf4bb6..42cacb4 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-beans.xml
@@ -16,6 +16,14 @@
         p:requireDiscoveryURL="true"
         p:requireRelyingPartyId="false" />
     
+    <bean id="PrepareDiscoveryStateDataContext"
+        class="net.shibboleth.sp.profile.impl.PrepareDiscoveryStateDataContext" scope="prototype" />
+    
+    <bean id="PreserveStateData"
+        class="net.shibboleth.sp.profile.PreserveStateDataAction" scope="prototype"
+        p:errorFatal="%{sp.stateToken.errorsFatal:false}"
+        p:createOutputObjects="true" />
+    
     <bean id="IssueDiscoveryRequest"
         class="net.shibboleth.sp.profile.impl.IssueDiscoveryRequest" scope="prototype"
         p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-flow.xml
index e3d0241..b857c4b 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/disco/disco-flow.xml
@@ -5,6 +5,9 @@
 
     <action-state id="DiscoverySessionInitiator">
         <evaluate expression="ValidateSessionInitiatorRequest" />
+        <evaluate expression="PrepareDiscoveryStateDataContext" />
+        <evaluate expression="PreserveStateData" />
+        <evaluate expression="PreservePostData" />
         <evaluate expression="IssueDiscoveryRequest" />
         
         <evaluate expression="'proceed'" />
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-beans.xml
index 85815b8..b2c3f71 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/initiator-beans.xml
@@ -7,15 +7,12 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-initiator" />
-    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.initiator:SPAgent.SessionInitiator}" />
-
     <bean id="PreservePostData"
         class="net.shibboleth.sp.profile.impl.PreservePostData" scope="prototype"
         p:errorFatal="%{sp.postData.errorsFatal:false}"
         p:lifetime="%{sp.postData.lifetime:PT15M}"
         p:storageService-ref="#{'%{sp.postData.StorageService:shibboleth.StorageService}'.trim()}"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.postData.cookiePrefix:__Host-shibsp_post_}'.trim()}" />
+        p:cookiePrefix="#{'%{sp.postData.cookiePrefix:__Host-shibsp_post}'.trim()}" />
     
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-beans.xml
index f197e34..330ceac 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-beans.xml
@@ -10,10 +10,4 @@
     <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-initiator" />
     <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.initiator:SPAgent.SessionInitiator}" />
 
-    <bean id="MapResourceToStateToken" 
-        class="net.shibboleth.sp.profile.impl.MapResourceToStateToken" scope="prototype"
-        p:createOutputObjects="true"
-        p:targetRequired="false"
-        p:errorFatal="%{sp.stateToken.errorsFatal:false}" />
-        
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-flow.xml
index b9da789..3cfff81 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-initiator/logout-initiator-flow.xml
@@ -16,7 +16,6 @@
         <on-entry>
             <evaluate expression="new net.shibboleth.shared.collection.ReflectionSafeIterator(opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.sp.context.AgentRequestContext)).getApplication().getLogoutInitiators(opensamlProfileRequestContext).iterator())" result="flowScope.LogoutInitiatorIterator" />
         </on-entry>
-        <evaluate expression="MapResourceToStateToken" />
         <evaluate expression="'proceed'" />
         
         <!-- Branch to child flow for actual work. -->
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/initiator-beans.xml
index 85815b8..8aae2de 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/initiator-beans.xml
@@ -6,16 +6,5 @@
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
-
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-initiator" />
-    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.initiator:SPAgent.SessionInitiator}" />
-
-    <bean id="PreservePostData"
-        class="net.shibboleth.sp.profile.impl.PreservePostData" scope="prototype"
-        p:errorFatal="%{sp.postData.errorsFatal:false}"
-        p:lifetime="%{sp.postData.lifetime:PT15M}"
-        p:storageService-ref="#{'%{sp.postData.StorageService:shibboleth.StorageService}'.trim()}"
-        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.postData.cookiePrefix:__Host-shibsp_post_}'.trim()}" />
     
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
index 6ce382b..9e01348 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-beans.xml
@@ -9,10 +9,9 @@
 
     <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-initiator" />
     <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.initiator:SPAgent.SessionInitiator}" />
-
-    <bean id="MapResourceToStateToken" 
-        class="net.shibboleth.sp.profile.impl.MapResourceToStateToken" scope="prototype"
-        p:createOutputObjects="true"
-        p:errorFatal="%{sp.stateToken.errorsFatal:false}" />
-        
+    
+    <!-- Default configuration pulls state token from input message if present. -->
+    <bean id="RecoverStateData"
+        class="net.shibboleth.sp.profile.impl.RecoverStateData" scope="prototype" />
+    
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
index 1cf23bb..5490cd9 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-initiator/session-initiator-flow.xml
@@ -16,7 +16,7 @@
         <on-entry>
             <evaluate expression="new net.shibboleth.shared.collection.ReflectionSafeIterator(opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.sp.context.AgentRequestContext)).getApplication().getSessionInitiators(opensamlProfileRequestContext).iterator())" result="flowScope.SessionInitiatorIterator" />
         </on-entry>
-        <evaluate expression="MapResourceToStateToken" />
+        <evaluate expression="RecoverStateData" />
         <evaluate expression="'proceed'" />
         
         <!-- Branch to child flow for actual work. -->
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-flow.xml
index 8bff417..bb6d54f 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-flow.xml
@@ -24,10 +24,6 @@
         <transition on="proceed" to="EncodeAgentResponse" />
     </subflow-state>
     
-    <action-state id="NoPotentialFlow">
-        <evaluate expression="'NoPotentialFlow'" />
-    </action-state>
-    
     <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/token-consumer/token-consumer-beans.xml" />
 
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index cf09a9f..377f5e8 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -30,7 +30,7 @@ sp.service.agents.checkInterval = PT5M
 # Controls storage back-end for storage-based state tokens
 #sp.stateToken.StorageService = shibboleth.StorageService
 # Cookie prefix when using cookie-backed state
-#sp.stateToken.cookiePrefix = __Host-shibsp_state_
+#sp.stateToken.cookiePrefix = __Host-shibsp_state
 # Lifetime to preserve state before discarding
 #sp.stateToken.lifetime = PT30M
 # Whether to seal/encrypt data stored by state manager
@@ -38,14 +38,11 @@ sp.service.agents.checkInterval = PT5M
 # Whether to check for replay of state tokens
 #sp.stateToken.checkReplay = false
 
-# Request/response correlation control
-#sp.correlation.cookiePrefix = __Host-shibsp_req_
-
 # POST data preservation controls
 #sp.postData.preservation = false
 #sp.postData.limit = 1048576
 #sp.postData.lifetime = PT5M
-#sp.postData.cookiePrefix = __Host-shibsp_post_
+#sp.postData.cookiePrefix = __Host-shibsp_post
 #sp.postData.StorageService = shibboleth.StorageService
 #sp.postData.errorsFatal = false
 
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
index f24a6cc..590e1ed 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
@@ -63,28 +63,11 @@
         p:unverifiedConfiguration-ref="DefaultUnverifiedRelyingPartyConfiguration"
         p:relyingPartyConfigurations-ref="shibboleth.sp.RelyingPartyOverrides"
         p:discoveryServiceLookupStrategy="#{getObject('%{sp.discoveryURLFunction:DefaultDiscoveryServiceStrategy}'.trim())}"
-        p:stateTokenManager-ref="#{'%{sp.stateToken.Manager:shibboleth.sp.CookieStateTokenManager}'.trim()}"
-        p:stateManager-ref="shibboleth.sp.CookieStateManager" />
-        <!--
-        // TODO: make this property-driven once we retire the older interface
-        
         p:stateManager-ref="#{'%{sp.stateToken.Manager:shibboleth.sp.CookieStateManager}'.trim()}" />
-         -->
 
     <bean id="shibboleth.sp.Application" class="net.shibboleth.sp.impl.BasicApplication" abstract="true"
         p:metricName="net.shibboleth.sp.relyingparty.configurations" />
 
-    <bean id="shibboleth.sp.PassthroughStateTokenManager" class="net.shibboleth.sp.impl.PassthroughStateTokenManager" lazy-init="true" />
-
-    <!-- Deprecated, will be removed once we redo the flows. -->
-
-    <bean id="shibboleth.sp.StorageStateTokenManager" class="net.shibboleth.sp.impl.StorageServiceStateTokenManager" lazy-init="true"
-        p:storageService-ref="#{'%{sp.stateToken.StorageService:shibboleth.StorageService}'.trim()}" />
-
-    <bean id="shibboleth.sp.CookieStateTokenManager" class="net.shibboleth.sp.impl.CookieStateTokenManager" lazy-init="true"
-        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:__Host-shibsp_state_}'.trim()}" />
-
     <!-- State Managers -->
 
     <bean id="shibboleth.sp.StateManager" abstract="true"
@@ -101,12 +84,12 @@
     <bean id="shibboleth.sp.StorageAndCookieStateManager" parent="shibboleth.sp.StorageOnlyStateManager"
         class="net.shibboleth.sp.state.impl.StorageServiceStateManager" lazy-init="true"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:__Host-shibsp_state_}'.trim()}" />
+        p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:__Host-shibsp_state}'.trim()}" />
 
     <bean id="shibboleth.sp.CookieStateManager" parent="shibboleth.sp.StateManager"
         class="net.shibboleth.sp.state.impl.CookieStateManager" lazy-init="true"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:__Host-shibsp_state_}'.trim()}" />
+        p:cookiePrefix="#{'%{sp.stateToken.cookiePrefix:__Host-shibsp_state}'.trim()}" />
 
     <!-- More traditional beans akin to IdP service. -->
 
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutInitiatorFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutInitiatorFlowTest.java
index a01df5c..5ca64af 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutInitiatorFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutInitiatorFlowTest.java
@@ -92,23 +92,4 @@ public class LogoutInitiatorFlowTest extends AbstractSPFlowTest {
         assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
     }
 
-    
-    /**
-     * Test flow with no initiator subflows installed.
-     * @throws IOException 
-     */
-    @Test
-    public void testNoFlowsNoTarget() throws IOException {
-        setDefaultAuth();
-        
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
-        setRequest("POST", input);
-
-        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertFlowExecutionResult(result, FLOW_ID);
-        assertFlowExecutionOutcome(result.getOutcome());
-        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
-    }
-
 }
\ No newline at end of file
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionInitiatorFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionInitiatorFlowTest.java
index 5ca67a4..37eaf3c 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionInitiatorFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionInitiatorFlowTest.java
@@ -21,11 +21,17 @@ import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.EventIds;
 import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
 /**
@@ -35,6 +41,12 @@ public class SessionInitiatorFlowTest extends AbstractSPFlowTest {
     
     /** Flow ID. */
     @Nonnull public static final String FLOW_ID = "sp/session-initiator";
+    
+    /** Discovery service. */
+    @Nonnull public static final String DISCO_URL = "https://ds.example.org/DS";
+    
+    /** Return URL for discovery. */
+    @Nonnull public static final String DISCO_RETURN_URL = "https://sp.example.org/Login?DS=1";
 
     protected SessionInitiatorFlowTest() {
         super(FLOW_ID);
@@ -91,4 +103,64 @@ public class SessionInitiatorFlowTest extends AbstractSPFlowTest {
         assertFlowExecutionOutcome(result.getOutcome());
         assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
     }
+
+    /**
+     * Test flow with no initiator subflows installed but discovery triggered and no target resource.
+     * @throws IOException 
+     */
+    @Test
+    public void testDiscoveryNoTarget() throws IOException {
+        setDefaultAuth();
+
+        final Escaper esc = UrlEscapers.urlFormParameterEscaper();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
+        input.addmember(InitiatorConstants.DISCOVERY_RETURN_URL).string(esc.escape(DISCO_RETURN_URL));
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        setRequest("POST", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        final DDF output = assertOutputMessageSuccess(result);
+        final String url = new String(
+                output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME).getmember(RemotedHttpServletResponse.REDIRECT).unsafe_string(),
+                StandardCharsets.UTF_8);
+        assert url != null;
+        
+        Assert.assertEquals(url, DISCO_URL + "?return=" + esc.escape(DISCO_RETURN_URL) + "&entityID=" + esc.escape("https://sp.example.org"));
+    }
+
+    /**
+     * Test flow with no initiator subflows installed but discovery triggered and a target resource.
+     * @throws IOException 
+     */
+    @Test
+    public void testDiscoveryWithTarget() throws IOException {
+        setDefaultAuth();
+
+        final Escaper esc = UrlEscapers.urlFormParameterEscaper();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
+        input.addmember(InitiatorConstants.DISCOVERY_RETURN_URL).string(esc.escape(DISCO_RETURN_URL));
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        input.addmember(SPConstants.TARGET).unsafe_string("https://sp.example.org".getBytes(StandardCharsets.UTF_8));
+        setRequest("POST", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+                
+        final DDF output = assertOutputMessageSuccess(result);
+        final String url = new String(
+                output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME).getmember(RemotedHttpServletResponse.REDIRECT).unsafe_string(),
+                StandardCharsets.UTF_8);
+        assert url != null;
+        
+        Assert.assertTrue(url.startsWith(DISCO_URL + "?return=" + esc.escape(DISCO_RETURN_URL + "&state=")));
+    }
+
 }
\ No newline at end of file
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/TestSPEnvironmentApplicationContextInitializer.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/TestSPEnvironmentApplicationContextInitializer.java
index c602e84..21bd025 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/TestSPEnvironmentApplicationContextInitializer.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/TestSPEnvironmentApplicationContextInitializer.java
@@ -37,6 +37,9 @@ public class TestSPEnvironmentApplicationContextInitializer extends TestEnvironm
         // Avoids some complex mocking for the default StateManager bean.
         mock.setProperty("sp.stateToken.sealed", "false");
         
+        // Set to allow disco flow to run.
+        mock.setProperty("sp.discoveryURL", "https://ds.example.org/DS");
+        
         mock.setProperty("sp.storageService", "shibboleth.StorageService");
         mock.setProperty("sp.session.storageService", "shibboleth.StorageService");
         mock.setProperty("idp.additionalProperties",
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index 34901c9..e8e14d8 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -30,7 +30,6 @@ import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
 import net.shibboleth.profile.relyingparty.RelyingPartyConfigurationResolver;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.service.ReloadableService;
-import net.shibboleth.sp.profile.StateTokenManager;
 import net.shibboleth.sp.state.StateManager;
 
 /**
@@ -102,16 +101,6 @@ public interface Application extends RelyingPartyConfigurationResolver {
      * @return ordered list of unprefixed flow IDs
      */
     @Nonnull List<String> getLogoutInitiators(@Nullable final ProfileRequestContext profileRequestContext);
-    
-    /**
-     * Gets the {@link StateTokenManager} to use for thie application.
-     * 
-     * @return the manager to use
-     * 
-     * @deprecated
-     */
-    @Deprecated
-    @Nonnull StateTokenManager getStateTokenManager();
 
     /**
      * Gets the {@link StateManager} to use for thie application.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/context/TokenConsumerContext.java b/sp-server-api/src/main/java/net/shibboleth/sp/context/TokenConsumerContext.java
deleted file mode 100644
index 2b09eff..0000000
--- a/sp-server-api/src/main/java/net/shibboleth/sp/context/TokenConsumerContext.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.primitive.StringSupport;
-
-/**
- * Dedicated context for a token consumer operation's state.
- */
-public class TokenConsumerContext extends BaseContext {
-    
-    /** A message ID to use for correlating responses against. */
-    @Nullable @NotEmpty private String messageCorrelationID;
-    
-    /**
-     * Gets the message ID to correlate responses against.
-     * 
-     * @return original request message ID
-     */
-    @Nullable @NotEmpty public String getMessageCorrelationID() {
-        return messageCorrelationID;
-    }
-    
-    /**
-     * Sets the message ID to correlate responses against.
-     * 
-     * @param id original request message ID
-     * 
-     * @return this context
-     */
-    @Nonnull public TokenConsumerContext setMessageCorrelationID(@Nullable @NotEmpty String id) {
-        messageCorrelationID = StringSupport.trimOrNull(id);
-        
-        return this;
-    }
-    
-}
\ 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
deleted file mode 100644
index 5580661..0000000
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractStateTokenManager.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.time.Duration;
-
-import javax.annotation.Nonnull;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
-
-/**
- * Base class for {@link StateTokenManager} implementations.
- * 
- * @deprecated
- */
- at Deprecated
-public abstract class AbstractStateTokenManager extends AbstractIdentifiableInitializableComponent
-        implements StateTokenManager {
-    
-    /** Identifier generation. */
-    @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
-    
-    /** Expiration for state token. */
-    @Nonnull private Duration expiration;
-    
-    /**
-     * Constructor.
-     */
-    @SuppressWarnings("null")
-    public AbstractStateTokenManager() {
-        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");
-    }
-    
-    /**
-     * 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 (identifierStrategy == null) {
-            identifierStrategy = IdentifierGenerationStrategy.getInstance(ProviderType.SECURE);
-        }
-    }
-    
-    /**
-     * 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/profile/AbstractTokenConsumerResponseAction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/AbstractTokenConsumerResponseAction.java
index f841454..915a463 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
@@ -15,6 +15,7 @@
 package net.shibboleth.sp.profile;
 
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -134,6 +135,11 @@ public abstract class AbstractTokenConsumerResponseAction extends AbstractApplic
             output.addmember(ConsumerConstants.SESSION_OPAQUE).structure().add(sessionData);
         }
         
+        final Instant overrideExp = getSessionNotOnOrAfter(profileRequestContext);
+        if (overrideExp != null) {
+            output.addmember(ConsumerConstants.SESSION_NOTONORAFTER).longinteger(overrideExp.getEpochSecond());
+        }
+        
         final RemotedHttpServletResponse remotedResponse = agentRequestContext.getRemotedHttpServletResponse();
         assert remotedResponse != null;
         
@@ -168,5 +174,16 @@ public abstract class AbstractTokenConsumerResponseAction extends AbstractApplic
      * @return opaque session data as a {@link DDF} node, or null if no session data is provided
      */
     @Nullable protected abstract DDF getSessionData(@Nonnull final ProfileRequestContext profileRequestContext);
+    
+    /**
+     * Get a timestamp for the expiration of the session directed by the authentication token, if any.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return session expiration time or null
+     */
+    @Nullable protected Instant getSessionNotOnOrAfter(@Nonnull final ProfileRequestContext profileRequestContext) {
+        return null;
+    }
 
 }
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/ConsumerConstants.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/ConsumerConstants.java
index 5ee9ff4..2c903ad 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/ConsumerConstants.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/ConsumerConstants.java
@@ -32,12 +32,12 @@ public final class ConsumerConstants {
     /** Opaque session data member. */
     @Nonnull @NotEmpty public static final String SESSION_OPAQUE = "session.opaque";
 
+    /** NotOnOrAfter session member. */
+    @Nonnull @NotEmpty public static final String SESSION_NOTONORAFTER = "session.notonorafter";
+    
     /** Session attributes data member. */
     @Nonnull @NotEmpty public static final String SESSION_ATTRIBUTES = "session.attributes";
 
-    /** Passive indicator output parameter name. */
-    @Nonnull @NotEmpty public static final String PASSIVE = "passive";
-
     /** Private constructor. */
     private ConsumerConstants() {
         
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
index d0fcd0c..a706783 100644
--- 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
@@ -60,7 +60,7 @@ public class PreserveStateDataAction extends AbstractApplicationAction {
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(PreserveStateDataAction.class);
 
-    /** Strategy used to create the {@link StateDataContext} to presrve. */
+    /** Strategy used to locate the {@link StateDataContext} to preserve. */
     @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
     
     /** Whether an error constructing a state token is fatal. */
@@ -138,6 +138,7 @@ public class PreserveStateDataAction extends AbstractApplicationAction {
                 final String token = ensureApplication().getStateManager().preserveToStateToken(
                         ensureAgent(), ensureApplication(), data);
                 stateDataContext.setStateToken(token);
+                processToken(profileRequestContext, token);
                 log.debug("{} State data preserved to token: {}", getLogPrefix(), token);
             } catch (final IOException e) {
                 log.warn("{} Exception preserving state data", getLogPrefix(), e);
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/SPConstants.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/SPConstants.java
index 30b7ccf..b0101bb 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/SPConstants.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/profile/SPConstants.java
@@ -35,6 +35,9 @@ public final class SPConstants {
     /** Event parameter. */
     @Nonnull @NotEmpty public static final String EVENT = "event";
 
+    /** Original Event parameter. */
+    @Nonnull @NotEmpty public static final String ORIGINAL_EVENT = "original_event";
+    
     /** Transaction ID parameter. */
     @Nonnull @NotEmpty public static final String TXID = "txid";
     
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
deleted file mode 100644
index f542405..0000000
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/StateTokenManager.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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 javax.annotation.Nullable;
-
-import net.shibboleth.sp.Agent;
-import net.shibboleth.sp.Application;
-
-/**
- * Interface to a service that manages "state" tokens, used in most SSO protocols
- * to manage stateful request/response correlation and to limit exposure of the
- * resource URLs accessed by clients to allow recovery of the URL for final redirection.
- * 
- * <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>
- * 
- * @deprecated
- */
- at Deprecated
-public interface StateTokenManager {
-    
-    /**
-     * Preserves a value by transforming it into a state token.
-     * 
-     * @param agent agent owning the state
-     * @param application application owning the state
-     * @param value input value 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;
-
-    /**
-     * Recovers a value from a state token.
-     * 
-     * <p>In most implementations, the state token mapping should be cleared on successful use of this
-     * method.</p>
-     * 
-     * @param agent agent owning the state
-     * @param application application owning the state
-     * @param token state token
-     * 
-     * @return the recovered value, 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;
-
-}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/MessageCorrelationIDLookupFunction.java b/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/MessageCorrelationIDLookupFunction.java
deleted file mode 100644
index 1aacff1..0000000
--- a/sp-server-api/src/main/java/net/shibboleth/sp/profile/context/navigate/MessageCorrelationIDLookupFunction.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * 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.context.navigate;
-
-import javax.annotation.Nullable;
-
-import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
-
-import net.shibboleth.sp.context.TokenConsumerContext;
-
-/**
- * Lookup strategy for accessing message correlation ID in {@link TokenConsumerContext}.
- */
-public class MessageCorrelationIDLookupFunction implements ContextDataLookupFunction<TokenConsumerContext,String> {
-
-    /** {@inheritDoc} */
-    @Nullable public String apply(@Nullable final TokenConsumerContext input) {
-        return input != null ? input.getMessageCorrelationID() : null;
-    }
-
-}
\ 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 4cba0e6..3cac4ab 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
@@ -214,9 +214,9 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
             if (localDataSealer != null) {
                 log.debug("State data will be sealed before preservation");
                 return doPreserve(agent, application, localDataSealer.wrap(serializedState,
-                        Instant.now().plus(expiration)));
+                        Instant.now().plus(expiration)), true);
             } else {
-                return doPreserve(agent, application, serializedState);
+                return doPreserve(agent, application, serializedState, false);
             }
         } catch (final JsonProcessingException | DataSealerException e) {
             throw new IOException("Error preserving state", e);
@@ -238,7 +238,7 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
             }
         }
         
-        String data = doRecover(agent, application, token);
+        String data = doRecover(agent, application, token, dataSealer != null);
         if (data == null) {
             return null;
         }
@@ -281,13 +281,14 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
      * @param agent agent owning the state
      * @param application application owning the state
      * @param data data to preserve
+     * @param sealed whether the data was sealed (and thus base64-encoded)
      * 
      * @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;
+            @Nonnull final String data, final boolean sealed) throws IOException;
 
     /**
      * Subclasses implement this method to recover the stored data in whatever way is necessary and return
@@ -301,13 +302,14 @@ public abstract class AbstractStateManager extends AbstractIdentifiableInitializ
      * @param agent agent owning the state
      * @param application application owning the state
      * @param stateToken the state token to map from/clear
+     * @param sealed whether the data was sealed (and thus base64-encoded)
      * 
      * @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;
+            @Nonnull @NotEmpty final String stateToken, final boolean sealed) throws IOException;
     
     /**
      * Generate a state token.
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 5f01f6f..379d41a 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -58,7 +58,6 @@ import net.shibboleth.shared.service.ReloadableService;
 import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.Application;
 import net.shibboleth.sp.ProtocolSupportService;
-import net.shibboleth.sp.profile.StateTokenManager;
 import net.shibboleth.sp.state.StateManager;
 import net.shibboleth.spring.security.CredentialHolder;
 
@@ -97,9 +96,6 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
     /** Logout initiator list lookup strategy. */
     @NonnullAfterInit private Function<ProfileRequestContext,List<String>> logoutInitiatorLookupStrategy;
     
-    /** State token management. */
-    @Nullable private StateTokenManager stateTokenManager;
-
     /** State management. */
     @Nullable private StateManager stateManager;
     
@@ -382,35 +378,7 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
         logoutInitiatorLookupStrategy = Constraint.isNotNull(strategy,
                 "Logout initiators lookup strategy cannot be null");
     }
-    
-    /** {@inheritDoc} */
-    @Nonnull public StateTokenManager getStateTokenManager() {
-        checkComponentActive();
-        if (stateTokenManager != null) {
-            return stateTokenManager;
-        }
-        
-        if (parent != null && allowInheritance) {
-            return parent.getStateTokenManager();
-        }
-        
-        throw new ConstraintViolationException("StateTokenManager cannot be null");
-    }
-    
-    /**
-     * Set the {@link StateTokenManager} to use.
-     * 
-     * @param manager state token manager
-     * 
-     * @deprecated
-     */
-    @Deprecated
-    public void setStateTokenManager(@Nonnull final StateTokenManager manager) {
-        checkSetterPreconditions();
         
-        stateTokenManager = Constraint.isNotNull(manager, "StateTokenManager cannot be null");
-    }
-    
     /** {@inheritDoc} */
     @Nonnull public StateManager getStateManager() {
         checkComponentActive();
@@ -644,9 +612,7 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
             return;
         }
         
-        if (stateTokenManager == null) {
-            throw new ComponentInitializationException("StateTokenManager cannot be null");
-        } else if (stateManager == null) {
+        if (stateManager == null) {
             throw new ComponentInitializationException("StateManager cannot be null");
         } else if (transcodingRegistry == null) {
             throw new ComponentInitializationException("AttributeTranscoderRegistry cannot be null");
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
deleted file mode 100644
index ab832d3..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/CookieStateTokenManager.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.NoSuchAlgorithmException;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.DecodingException;
-import net.shibboleth.shared.codec.EncodingException;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.net.CookieManager;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
-import net.shibboleth.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;
-
-/**
- * {@link StateTokenManager} implemented using cookies.
- * 
- * @deprecated
- */
- at Deprecated
-public class CookieStateTokenManager extends AbstractStateTokenManager {
-    
-    /** Default cookie prefix. */
-    @Nonnull @NotEmpty public static String DEFAULT_PREFIX = "_shibsp_state_";
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(CookieStateTokenManager.class);
-
-    /** Cookie manager. */
-    @NonnullAfterInit private CookieManager cookieManager;
-    
-    /** Fixed prefix for cookie names. */
-    @Nonnull @NotEmpty private String cookiePrefix;
-    
-    /** Constructor. */
-    public CookieStateTokenManager() {
-        cookiePrefix = DEFAULT_PREFIX;
-    }
-
-    /**
-     * Set the fixed prefix to use for the cookies.
-     * 
-     * <p>Defaults to "_shibsp_state".</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");
-    }
-    
-    /**
-     * Set the {@link CookieManager} to use.
-     * 
-     * @param manager instance to use
-     */
-    public void setCookieManager(@Nonnull final CookieManager manager) {
-        checkSetterPreconditions();
-        
-        cookieManager = Constraint.isNotNull(manager, "CookieManager cannot be null");
-    }
-    
-    /** {@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 {
-                setIdentifierGenerationStrategy(IdentifierGenerationStrategy.getInstance(ProviderType.SECURE, spec));
-            } catch (final InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
-                throw new ComponentInitializationException(e);
-            }
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final byte[] value) throws IOException {
-
-        cookieManager.purgeStaleCookies(cookiePrefix);
-        
-        final Instant ts = Instant.now();
-        assert ts != null;
-        
-        final String key = Long.toString(ts.toEpochMilli()) + '_' + generateToken();
-        final String name = getCookieName(application, key);
-        try {
-            cookieManager.addCookie(name, Base64Support.encodeURLSafe(value), (int) getExpiration().toSeconds());
-        } catch (final EncodingException e) {
-            throw new IOException(e);
-        }
-
-        log.trace("Created state token mapping from '{}' to value '{}'", name, value);
-        
-        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;
-        }
-        
-        final String cookieName = getCookieName(application, token);
-        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);
-            } catch (final DecodingException e) {
-                throw new IOException(e);
-            }
-        }
-
-        log.warn("No cookie found matching state token: '{}'", token);
-        return null;
-    }
-    
-    /**
-     * Computes the name of a new state cookie.
-     * 
-     * @param application the application
-     * @param uniquePortion unique portion of name
-     * 
-     * @return cookie name
-     */
-    @Nonnull public String getCookieName(@Nonnull final Application application, @Nonnull final String uniquePortion) {
-        
-        // Format is prefix_appId_timestamp_random
-        // The timestamp allows them to be sorted for staleness.
-        
-        final StringBuilder builder = new StringBuilder(cookiePrefix);
-        builder.append('_')
-            .append(application.getId())
-            .append('_')
-            .append(uniquePortion);
-        
-        return builder.toString();
-    }
-    
-}
\ No newline at end of file
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
deleted file mode 100644
index c00b485..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/PassthroughStateTokenManager.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.DecodingException;
-import net.shibboleth.shared.codec.EncodingException;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.Agent;
-import net.shibboleth.sp.Application;
-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 {
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(PassthroughStateTokenManager.class);
-    
-    /** {@inheritDoc} */
-    @Override
-    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final byte[] value) throws IOException {
-        
-        try {
-            return Base64Support.encodeURLSafe(value);
-        } catch (final EncodingException e) {
-            throw new IOException(e);
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    @Nullable public byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String token) throws IOException {
-        
-        try {
-            return Base64Support.decodeURLSafe(token);
-        } catch (final DecodingException e) {
-            log.warn("Unable to Base64-decode state token: {}", token);
-            return null;
-        }
-    }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index d89a6c1..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/StorageServiceStateTokenManager.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.time.Instant;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.storage.StorageRecord;
-import org.opensaml.storage.StorageService;
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.DecodingException;
-import net.shibboleth.shared.codec.EncodingException;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.Agent;
-import net.shibboleth.sp.Application;
-import net.shibboleth.sp.profile.AbstractStateTokenManager;
-import net.shibboleth.sp.profile.StateTokenManager;
-
-/**
- * {@link StateTokenManager} implemented with a {@link StorageService}.
- * 
- * @deprecated
- */
- at Deprecated
-public class StorageServiceStateTokenManager extends AbstractStateTokenManager {
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(StorageServiceStateTokenManager.class);
-    
-    /** Storage back-end. */
-    @NonnullAfterInit private StorageService storageService;
-    
-    /**
-     * 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");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        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
-    @Nonnull public String preserveToStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final byte[] value) throws IOException {
-        
-        final String context = getContext(agent, application);
-        final String key = generateToken();
-        
-        String encoded;
-        try {
-            encoded = Base64Support.encode(value, false);
-        } catch (final EncodingException e) {
-            throw new IOException(e);
-        }
-        
-        if (storageService.create(context, key, encoded, Instant.now().plus(getExpiration()).toEpochMilli())) {
-            log.trace("Created state token mapping ('{}', '{}') to value '{}'", context, key, encoded);
-            return key;
-        }
-        
-        // Should only happen if the randomizer failed...
-        throw new IOException("Unable to create storage record for state token");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    @Nullable public byte[] recoverFromStateToken(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String token) throws IOException {
-        
-        final String context = getContext(agent, application);
-        final StorageRecord<String> record = storageService.read(context, token);
-        if (record != null) {
-            log.trace("Recovered state token mapping ('{}', '{}') to value '{}'", context, token, record.getValue());
-            try {
-                storageService.delete(context, token);
-            } catch (final IOException e) {
-                log.warn("Unable to delete state token ('{}', '{}') from storage", context, token, e);
-            }
-            try {
-                return Base64Support.decode(record.getValue());
-            } catch (final DecodingException e) {
-                throw new IOException(e);
-            }
-        }
-        
-        return null;
-    }
-    
-    /**
-     * Compute the storage context to use.
-     * 
-     * @param agent calling agent
-     * @param application calling application
-     * 
-     * @return storage context for request
-     */
-    @SuppressWarnings("null")
-    @Nonnull private String getContext(@Nonnull final Agent agent, @Nonnull final Application application) {
-        final StringBuilder builder = new StringBuilder(StorageServiceStateTokenManager.class.getName());
-        builder.append('!').append(agent.getId()).append('!').append(application.getId());
-        return builder.toString();
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
index f6ac184..1658d6b 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/EncodeAgentResponse.java
@@ -16,9 +16,11 @@ package net.shibboleth.sp.profile.impl;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -27,18 +29,23 @@ import org.slf4j.Logger;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.profile.AbstractAgentRequestAction;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.state.StateData;
 
 /**
  * A profile action to encode an agent response from the output {@link DDF} in the
  * {@link AgentRequestContext}.
  * 
  * <p>This also ensures the final output is a structure, and contains an "event" member,
- * setting it to "success" if not already set.</p> 
+ * setting it to "success" if not already set, and also populates target from a {@link StateDataContext}
+ * if available.</p> 
  * 
  * <p>If enabled as a feature for the agent, the value of the session ID is also placed
  * in a structure member in the output before encoding to relay it to the agent.</p>
@@ -47,16 +54,37 @@ import net.shibboleth.sp.profile.AbstractAgentRequestAction;
  * @event {@link EventIds#INVALID_PROFILE_CTX}
  * @event {@link EventIds#INVALID_MESSAGE}
  * @event {@link EventIds#IO_ERROR}
- * @pre <pre>AgentRequestContext.getOutput().getmember("event").isstring()</pre>
+ * 
+ * @post <pre>AgentRequestContext.getOutput().getmember("event").isstring()</pre>
  */
 public class EncodeAgentResponse extends AbstractAgentRequestAction {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(EncodeAgentResponse.class);
 
+    /** Strategy used to create the {@link StateDataContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+    
     /** Cached context containing the output message. */
     @NonnullBeforeExec private DDF outputMessage;
     
+    /** Constructor. */
+    public EncodeAgentResponse() {
+        stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
+    }
+    
+    /**
+     * Sets the strategy used to lookup the {@link StateDataContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setStateDataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -94,8 +122,22 @@ public class EncodeAgentResponse extends AbstractAgentRequestAction {
             return;
         }
         
-        if (outputMessage.getmember("event").isnull()) {
-            outputMessage.addmember("event").string("success");
+        if (outputMessage.getmember(SPConstants.EVENT).isnull()) {
+            outputMessage.addmember(SPConstants.EVENT).string("success");
+        }
+        
+        // Populate target if necessary.
+        if (outputMessage.getmember(SPConstants.TARGET).isnull()) {
+            final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+            if (stateDataContext != null) {
+                final StateData data = stateDataContext.getStateData();
+                if (data != null) {
+                    final byte[] target = data.getRawResource();
+                    if (target != null) {
+                        outputMessage.addmember(SPConstants.TARGET).unsafe_string(target);
+                    }
+                }
+            }
         }
         
         final Agent agent = ensureAgentRequestContext().getAgent();
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
deleted file mode 100644
index f635187..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookie.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * 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.util.function.Function;
-
-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 com.google.common.escape.Escaper;
-import com.google.common.net.UrlEscapers;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
-import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.net.CookieManager;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
-import net.shibboleth.sp.profile.AbstractApplicationAction;
-import net.shibboleth.sp.profile.SPConstants;
-
-/**
- * Action that issues a cookie used to record state about a request for
- * later enforcement/evaluation.
- * 
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_MESSAGE}
- */
-public class IssueCorrelationCookie extends AbstractApplicationAction {
-    
-    /** Default cookie prefix. */
-    @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "shibsp_req_";
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(IssueCorrelationCookie.class);
-    
-    /** Cookie manager. */
-    @NonnullAfterInit private CookieManager cookieManager;
-    
-    /** Cookie prefix. */
-    @Nonnull private String cookiePrefix;
-
-    /** Whether an error constructing a correlation cookie is fatal. */
-    private boolean errorFatal;
-    
-    /** Lookup strategy for request message ID. */
-    @NonnullAfterInit private Function<ProfileRequestContext,String> requestIDLookupStrategy;
-
-    /** State token vakue used in cookie name. */
-    @NonnullBeforeExec private String stateToken;
-    
-    /** Request ID. */
-    @NonnullBeforeExec private String requestID;
-    
-    /** Constructor. */
-    public IssueCorrelationCookie() {
-        cookiePrefix = DEFAULT_COOKIE_PREFIX;
-    }
-    
-    /**
-     * Sets the {@link CookieManager} to use.
-     * 
-     * @param manager cookie manager instance
-     */
-    public void setCookieManager(@Nonnull final CookieManager manager) {
-        checkSetterPreconditions();
-        
-        cookieManager = Constraint.isNotNull(manager, "CookieManager cannot be null");
-    }
-    
-    /**
-     * Sets the cookie prefix.
-     * 
-     * <p>Defaults to {@link #DEFAULT_COOKIE_PREFIX}.</p>
-     * 
-     * @param prefix cookie prefix
-     */
-    public void setCookiePrefix(@Nonnull @NotEmpty final String prefix) {
-        checkSetterPreconditions();
-        
-        cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
-    }
-    
-    /**
-     * Sets whether an error computing a state token should result in a fatal event.
-     * 
-     * <p>Defaults to false.</p>
-     * 
-     * @param flag flag to set
-     */
-    public void setErrorFatal(final boolean flag) {
-        checkSetterPreconditions();
-        
-        errorFatal = flag;
-    }
-    
-    /**
-     * Sets the lookup strategy for obtaining the request message's ID.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setRequestIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
-        checkSetterPreconditions();
-        
-        requestIDLookupStrategy = Constraint.isNotNull(strategy, "Request ID lookup strategy cannot be null");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (cookieManager == null) {
-            throw new ComponentInitializationException("CookieManager cannot be null");
-        } else if (requestIDLookupStrategy == null) {
-            throw new ComponentInitializationException("Request ID lookup strategy cannot be null");
-        }
-    }    
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-
-        requestID = requestIDLookupStrategy.apply(profileRequestContext);
-        
-        if (requestID == null) {
-            log.debug("{} No request message ID available, skipping creation of correlation cookie", getLogPrefix());
-            return false;
-        }
-        
-        final DDF input = ensureAgentRequestContext().getInput();
-        if (input != null) {
-            stateToken = input.getmember(SPConstants.STATE).string();
-        }
-        
-        if (stateToken == null) {
-            if (requestID != null) {
-                if (errorFatal) {
-                    log.warn("{} Input was missing {} parameter", getLogPrefix(), SPConstants.STATE);
-                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-                } else {
-                    log.debug("{} Input was missing {} parameter, skipping creation of correlation cookie", getLogPrefix(),
-                            SPConstants.STATE);
-                }
-            }
-            return false;
-        }
-        
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        ensureOutputObjects();
-        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
-        // We do the crazy stuff to accomodate the cookies being set or unset.
-        try {
-            RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
-                    agentRequestContext.getRemotedHttpServletResponse());
-
-            cookieManager.purgeStaleCookies(cookiePrefix);
-            
-            log.debug("{} Tracking request ID {} against state token {}", getLogPrefix(), requestID, stateToken);
-
-            final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
-            cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), escaper.escape(requestID));
-        } finally {
-            RemotedHttpServletRequestResponseContext.clearCurrent();
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequest.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequest.java
index 52d9ddc..97b44ba 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequest.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/IssueDiscoveryRequest.java
@@ -21,6 +21,7 @@ import java.util.function.Predicate;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -35,6 +36,7 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
 import net.shibboleth.sp.profile.AbstractApplicationAction;
@@ -61,6 +63,9 @@ public class IssueDiscoveryRequest extends AbstractApplicationAction {
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(IssueDiscoveryRequest.class);
 
+    /** Strategy used to create the {@link StateDataContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+    
     /** Strategy used to obtain the request issuer value. */
     @Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
     
@@ -79,15 +84,25 @@ public class IssueDiscoveryRequest extends AbstractApplicationAction {
     /** The agent handler URL to return control to. */
     @NonnullBeforeExec private String returnURL;
     
-    /** State token. */
-    @Nullable private String state;
-    
     /** Constructor. */
     public IssueDiscoveryRequest() {
+        stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
         issuerLookupStrategy = new ApplicationDefaultIssuerStrategy();
         passiveCondition = new DefaultPassiveCondition();
     }
     
+    /**
+     * Sets the strategy used to lookup the {@link StateDataContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setStateDataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+    }
+    
     /**
      * Set the strategy used to locate the issuer value to use.
      * 
@@ -97,7 +112,7 @@ public class IssueDiscoveryRequest extends AbstractApplicationAction {
         checkSetterPreconditions();
         issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -126,9 +141,7 @@ public class IssueDiscoveryRequest extends AbstractApplicationAction {
             log.error("{} No input message from agent", getLogPrefix());
             return false;
         }
-        
-        state = input.getmember(SPConstants.STATE).string();
-        
+
         // This is checked earlier in the flow, so can be treated as an outright error here.
         returnURL = input.getmember(InitiatorConstants.DISCOVERY_RETURN_URL).string();
         if (returnURL == null) {
@@ -141,15 +154,18 @@ public class IssueDiscoveryRequest extends AbstractApplicationAction {
     }
     
     /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        
-        // We have to add the state parameter (usually placed there by our calling flow) to the query
-        // string of the already-URL-encoded return URL. We URL-encode the character sequence
-        // "&state=<value>" and append that to the existing URL. This ensures the agent will get our
-        // state token back from the DS to provide to us afterwards.  
-        
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {        
+
         final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+                
+        // Check for state token in context.
+        final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+        final String state = stateDataContext != null ? stateDataContext.getStateToken() : null;
         if (state != null) {
+            // We have to add a state parameter to the query string of the already-URL-encoded return URL.
+            // We URL-encode the character sequence "&state=<value>" and append that to the existing URL.
+            // This ensures the agent will get our state token back from the DS to provide to the Hub afterwards.
+            
             returnURL += escaper.escape('&' + SPConstants.STATE + '=' + escaper.escape(state));
             
              // At this point, the return value is already encoded, and thus does not need to
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapResourceToStateToken.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapResourceToStateToken.java
deleted file mode 100644
index d09afca..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapResourceToStateToken.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * 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 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.primitive.LoggerFactory;
-import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
-import net.shibboleth.sp.profile.AbstractApplicationAction;
-import net.shibboleth.sp.profile.SPConstants;
-
-/**
- * Action that maps a target/resource URL into a state token.
- * 
- * <p>The existence of a {@link SPConstants#STATE} member will cause the action
- * to be skipped, while the absence of {@link SPConstants#TARGET} will result in
- * conditional failure.</p>
- * 
- * <p>Errors may be ignored or result in an {@link EventIds#IO_ERROR} event.</p>
- * 
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_MESSAGE}
- * @event {@link EventIds#IO_ERROR}
- */
-public class MapResourceToStateToken extends AbstractApplicationAction {
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(MapResourceToStateToken.class);
-
-    /** Whether an error constructing a state token is fatal. */
-    private boolean errorFatal;
-    
-    /** Whether to require target parameter in the absence of a state token. */
-    private boolean targetRequired;
-    
-    /** Agent input. */
-    @NonnullBeforeExec private DDF input;
-
-    /** Target resource to operate on. */
-    @NonnullBeforeExec private byte[] target;
-    
-    /** Constructor. */
-    public MapResourceToStateToken() {
-        targetRequired = true;
-    }
-    
-    /**
-     * 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;
-    }
-
-    /**
-     * Sets whether the {@link SPConstants#TARGET} parameter is required if no state token
-     * exists already.
-     * 
-     * <p>Defaults to true.</p>
-     * 
-     * @param flag flag to set
-     */
-    public void setTargetRequired(final boolean flag) {
-        checkSetterPreconditions();
-        
-        targetRequired = flag;
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-        
-        input = ensureAgentRequestContext().getInput();
-        if (input == null) {
-            log.debug("{} Input message was absent", getLogPrefix());
-            return false;
-        }
-        
-        if (input.getmember(SPConstants.STATE).isstring()) {
-            log.debug("{} Input message already contains {} parameter", getLogPrefix(), SPConstants.STATE);
-            return false;
-        }
-        
-        target = input.getmember(SPConstants.TARGET).unsafe_string();
-        if (target == null) {
-            if (targetRequired) {
-                log.warn("{} Input message did not contain {} member", getLogPrefix(), SPConstants.TARGET);
-                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-            }
-            return false;
-        }
-        
-        ensureAgentRequestContext().setTargetURL(target);
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        ensureOutputObjects();
-        
-        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
-        // We do the crazy stuff to accomodate cookie-backed state management.
-        try {
-            RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
-                    agentRequestContext.getRemotedHttpServletResponse());
-            
-            try {
-                final String token = ensureApplication().getStateTokenManager().preserveToStateToken(
-                        ensureAgent(), ensureApplication(), target);
-                input.addmember(SPConstants.STATE).string(token);
-                log.debug("{} Target resource preserved to state token: {}", getLogPrefix(), token);
-            } catch (final IOException e) {
-                log.warn("{} Exception preserving target resource to state token", getLogPrefix(), e);
-                if (errorFatal) {
-                    ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
-                }
-            }
-        } finally {
-            RemotedHttpServletRequestResponseContext.clearCurrent();
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapStateTokenToResource.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapStateTokenToResource.java
deleted file mode 100644
index 99ae50b..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/MapStateTokenToResource.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * 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.util.function.Function;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
-import net.shibboleth.sp.profile.AbstractApplicationAction;
-import net.shibboleth.sp.profile.SPConstants;
-
-/**
- * Action that maps a state token into a target/resource URL.
- * 
- * <p>The state token comes from a pluggable function.</p>
- * 
- * <p>The existence of {@link AgentRequestContext#getTargetURL()} will cause the action
- * to be skipped but the absence of a state token will not result in failure.</p>
- * 
- * <p>As a side effect for downstream actions, the state token will be added to the
- * input message in a {@link SPConstants#STATE} field.</p>
- * 
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_MESSAGE}
- * @event {@link EventIds#IO_ERROR}
- */
-public class MapStateTokenToResource extends AbstractApplicationAction {
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(MapStateTokenToResource.class);
-    
-    /** Lookup strategy for state token. */
-    @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
-    
-    /**
-     * Sets the lookup strategy to obtain the protocol specific state token. 
-     *
-     * @param strategy lookup strategy
-     */
-    public void setStateTokenLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
-        checkSetterPreconditions();
-        stateTokenLookupStrategy = Constraint.isNotNull(strategy, "State token lookup strategy cannot be null");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (stateTokenLookupStrategy == null) {
-            throw new ComponentInitializationException("State token lookup strategy cannot be null");
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-        
-        if (ensureAgentRequestContext().getTargetURL() != null) {
-            log.debug("{} Target URL already populated, skipping state token processing", getLogPrefix());
-            return false;
-        }
-
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        ensureOutputObjects();
-        
-        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
-        // We do the crazy stuff to accomodate cookie-backed state management
-        // (and to get the relevant state token in the first place).
-        try {
-            RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
-                    agentRequestContext.getRemotedHttpServletResponse());
-            
-            final String token = stateTokenLookupStrategy.apply(profileRequestContext);
-            if (token == null) {
-                log.debug("{} No state token returned from lookup strategy, nothing to do", getLogPrefix());
-                return;
-            }
-            
-            final DDF input = agentRequestContext.getInput();
-            if (input != null) {
-                input.addmember(SPConstants.STATE).string(token);
-            }
-            
-            final byte[] target = ensureApplication().getStateTokenManager().recoverFromStateToken(
-                    ensureAgent(), ensureApplication(), token);
-            if (target != null) {
-                agentRequestContext.setTargetURL(target);
-                log.debug("{} Requested resource recovered from state token: {}", getLogPrefix(), target);
-            } else {
-                log.warn("{} Unable to recover requested resource from state token", getLogPrefix());
-            }
-        } catch (final IOException e) {
-            log.warn("{} Exception recovering requested resource from state token", getLogPrefix(), e);
-        } finally {
-            RemotedHttpServletRequestResponseContext.clearCurrent();
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareAgentErrorResponse.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareAgentErrorResponse.java
index 61d4a7e..3ba6a6e 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareAgentErrorResponse.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareAgentErrorResponse.java
@@ -24,11 +24,14 @@ import org.opensaml.profile.context.EventContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.CurrentOrPreviousEventLookup;
 
+import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.profile.AbstractAgentRequestAction;
 import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.state.StateData;
 
 /**
  * A profile action to encode the current event (typically an error) into an agent response in a
@@ -45,10 +48,14 @@ public class PrepareAgentErrorResponse extends AbstractAgentRequestAction {
     /** Strategy function for access to {@link EventContext} to check. */
     @Nonnull private Function<ProfileRequestContext,EventContext> eventContextLookupStrategy;
     
+    /** Strategy used to create the {@link StateDataContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+    
     /** Constructor. */
     public PrepareAgentErrorResponse() {
         setAgentRequestContextLookupStrategy(new ChildContextLookup<>(AgentRequestContext.class, true));
         eventContextLookupStrategy = new CurrentOrPreviousEventLookup();
+        stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
     }
 
     /**
@@ -62,6 +69,18 @@ public class PrepareAgentErrorResponse extends AbstractAgentRequestAction {
         eventContextLookupStrategy = Constraint.isNotNull(strategy, "EventContext lookup strategy cannot be null");
     }
     
+    /**
+     * Sets the strategy used to lookup the {@link StateDataContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setStateDataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -77,15 +96,31 @@ public class PrepareAgentErrorResponse extends AbstractAgentRequestAction {
         final EventContext eventCtx = eventContextLookupStrategy.apply(profileRequestContext);
         final Object event = eventCtx != null ? eventCtx.getEvent() : null;
         if (event != null) {
-            final String eventString = event.toString();
-            output.addmember(SPConstants.EVENT).string(eventString);
+            output.addmember(SPConstants.EVENT).string(event.toString());
         } else {
             output.addmember(SPConstants.EVENT).string(EventIds.MESSAGE_PROC_ERROR);
         }
-        
-        final byte[] target = agentContext.getTargetURL();
-        if (target != null) {
-            output.addmember(SPConstants.TARGET).unsafe_string(target);
+
+        // We use the StateData, if it exists, as a last-ditch source of the target parameter
+        // and to determine whether to override the event to NoPassive.
+        final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+        final StateData data = stateDataContext != null ? stateDataContext.getStateData() : null;
+
+        final Boolean passive = data != null ? data.getPassive() : null;
+        if (passive != null && passive) {
+            // Remap event and capture the original.
+            output.addmember(SPConstants.ORIGINAL_EVENT).string(output.getmember(SPConstants.EVENT).string());
+            output.getmember(SPConstants.EVENT).string(AuthnEventIds.NO_PASSIVE);
+        }
+
+        // Populate target if necessary.
+        if (output.getmember(SPConstants.TARGET).isnull()) {
+            if (data != null) {
+                final byte[] target = data.getRawResource();
+                if (target != null) {
+                    output.addmember(SPConstants.TARGET).unsafe_string(target);
+                }
+            }
         }
     }
 
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareDiscoveryStateDataContext.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareDiscoveryStateDataContext.java
new file mode 100644
index 0000000..4d1ab30
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/PrepareDiscoveryStateDataContext.java
@@ -0,0 +1,114 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+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.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.state.StateData;
+
+/**
+ * Extracts target resource URL from input message and prepares a {@link StateDataContext} for
+ * subsequent preservation handling.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * 
+ * @post StateDataContext exists and {@link StateDataContext#getStateData()} != null and
+ *      {@link StateData#getResource()} != null
+ */
+public class PrepareDiscoveryStateDataContext extends AbstractApplicationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PrepareDiscoveryStateDataContext.class);
+    
+    /** Strategy used to create the {@link StateDataContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextCreationStrategy;
+    
+    /** Input message. */
+    @NonnullBeforeExec private byte[] target;
+    
+    /** Constructor. */
+    public PrepareDiscoveryStateDataContext() {
+        stateDataContextCreationStrategy = new ChildContextLookup<>(StateDataContext.class, true);
+    }
+    
+    /**
+     * Sets the strategy used to create the {@link StateDataContext}.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setStateDataContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextCreationStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final DDF input = ensureAgentRequestContext().getInput();
+        if (input == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            log.error("{} No input message from agent", getLogPrefix());
+            return false;
+        }
+
+        target = input.getmember(SPConstants.TARGET).unsafe_string();
+        if (target == null) {
+            log.debug("{} No target parameter from agent, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {        
+
+        final StateDataContext context = stateDataContextCreationStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.error("{} Failed to create StateDataContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        
+        final StateData data = new StateData();
+        data.setRawResource(target);
+        
+        context.setStateData(data);
+    }
+
+}
\ No newline at end of file
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 1909e8d..ef90e40 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
@@ -18,16 +18,19 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 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 com.google.common.escape.Escaper;
 import com.google.common.net.UrlEscapers;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -42,6 +45,7 @@ import net.shibboleth.shared.primitive.StringSupport;
 import net.shibboleth.shared.security.IdentifierGenerationStrategy;
 import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
 import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
@@ -52,6 +56,12 @@ 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.
  * 
+ * <p>The state token to bind to is collected from a {@link StateDataContext} accessed via lookup strategy.</p>
+ * 
+ * <p>The action also has support for detecting data previously preserved by this action by checking for
+ * a {@link SPConstants#STATE} member in the input message and using that to recover the data and clean up
+ * that record so it can be re-associated with the new state token found in the context.</p>
+ * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_MESSAGE}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
@@ -60,11 +70,14 @@ import net.shibboleth.sp.profile.SPConstants;
 public class PreservePostData extends AbstractApplicationAction {
     
     /** Default cookie prefix. */
-    @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_post_";
+    @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_post";
     
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(PreservePostData.class);
     
+    /** Strategy used to create the {@link StateDataContext} to bind to. */
+    @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextLookupStrategy;
+    
     /** Identifier generation. */
     @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
     
@@ -86,14 +99,27 @@ public class PreservePostData extends AbstractApplicationAction {
     /** State token vakue used in cookie name. */
     @NonnullBeforeExec private String stateToken;
     
-    /** POST data to preserve. */
-    @NonnullBeforeExec private byte[] postData;
+    /** POST data to preserve in record. */
+    @NonnullBeforeExec private String postData;
     
     /** Constructor. */
     public PreservePostData() {
-        lifetime = Duration.ofMinutes(5);
+        stateDataContextLookupStrategy = new ChildContextLookup<>(StateDataContext.class);
+        lifetime = Duration.ofMinutes(30);
         cookiePrefix = DEFAULT_COOKIE_PREFIX;
     }    
+
+    /**
+     * Sets the strategy used to lookup the {@link StateDataContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setStateDataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+    }
     
     /**
      * Set {@link IdentifierGenerationStrategy} to use.
@@ -196,28 +222,15 @@ public class PreservePostData extends AbstractApplicationAction {
             return false;
         }
         
-        // First see if anything is even there.
-        final DDF input = ensureAgentRequestContext().getInput();
-        if (input != null) {
-            postData = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
-                    .getmember(RemotedHttpServletRequest.BODY)
-                    .unsafe_string();
-        }
-                
+        postData = getSubmittedData();
         if (postData == null) {
-            log.debug("{} No POST data to preserve", getLogPrefix());
-            return false;
-        }
-        
-        assert input != null;
-        final String contentType = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
-                .getmember(RemotedHttpServletRequest.CONTENT_TYPE)
-                .string();
-        if (!"application/x-www-form-urlencoded".equals(contentType)) {
-            log.warn("{} Unsupported content type: {}", getLogPrefix(), contentType);
-            return false;
+            postData = getPreviousData(profileRequestContext);
+            if (postData == null) {
+                log.debug("{} No POST data to preserve", getLogPrefix());
+                return false;
+            }
         }
-        
+                
         // Check permission.
         
         if (!ensureAgent().isSupportsPostPreservation()) {
@@ -226,26 +239,21 @@ public class PreservePostData extends AbstractApplicationAction {
         }
         
         long limit = ensureAgent().getPostLimit();
-        if (limit > 0 && postData.length > limit) {
+        if (limit > 0 && postData.length() > limit) {
             log.warn("{} POST data supplied for preservation, but size {} exceeds agent limit", getLogPrefix(),
-                    postData.length);
+                    postData.length());
             return false;
         }
         
         // Check for state token for cookie correlation.
-        
-        if (input != null) {
-            stateToken = input.getmember(SPConstants.STATE).string();
-        }
-        
+        final StateDataContext stateDataContext = stateDataContextLookupStrategy.apply(profileRequestContext);
+        stateToken = stateDataContext != null ? stateDataContext.getStateToken() : null;
         if (stateToken == null) {
             if (errorFatal) {
-                log.warn("{} Input was missing {} parameter, failing due to POST data preservationr",
-                        getLogPrefix(), SPConstants.STATE);
+                log.warn("{} Context missing state token to bind to, failing POST data preservationr", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
             } else {
-                log.warn("{} Input was missing {} parameter, skipping POST data preservation", getLogPrefix(),
-                        SPConstants.STATE);
+                log.warn("{} Context missing state token to bind to, skipping POST data preservation", getLogPrefix());
             }
             return false;
         }
@@ -266,17 +274,13 @@ public class PreservePostData extends AbstractApplicationAction {
 
             cookieManager.purgeStaleCookies(cookiePrefix);
 
-            log.debug("{} Preserving {} bytes of POST data against state token {}", getLogPrefix(), postData.length,
+            log.debug("{} Preserving {} bytes of POST data against state token {}", getLogPrefix(), postData.length(),
                     stateToken);
             
-            // 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);
             
-            if (!storageService.create(ensureAgent().getId() + ".PostData", key, encoded,
+            if (!storageService.create(ensureAgent().getId() + ".PostData", key, postData,
                     Instant.now().plus(lifetime).toEpochMilli())) {
                 log.warn("{} Collision attempting to create storage record for POST data under key {}", getLogPrefix(), key);
                 if (errorFatal) {
@@ -290,8 +294,7 @@ public class PreservePostData extends AbstractApplicationAction {
             }
 
             // Save off cookie. Name is decorated with state token prefix, value is the storage key.
-            final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
-            cookieManager.addCookie(cookiePrefix + escaper.escape(stateToken), escaper.escape(key));
+            cookieManager.addCookie(getCookieName(stateToken), UrlEscapers.urlFormParameterEscaper().escape(key));
             
         } catch (final IOException e) {
             log.error("{} Error creating storage record for POST data", getLogPrefix(), e);
@@ -303,4 +306,119 @@ public class PreservePostData extends AbstractApplicationAction {
         }
     }
 
+    /**
+     * Gets data submitted with this request, if any.
+     * 
+     * @return submitted data or null
+     */
+    @Nullable private String getSubmittedData() {
+        
+        // First see if anything fresh is present, indicating a POST.
+        byte[] raw = null;
+        
+        final DDF input = ensureAgentRequestContext().getInput();
+        if (input != null) {
+            raw = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+                    .getmember(RemotedHttpServletRequest.BODY)
+                    .unsafe_string();
+        }
+                
+        if (raw == null) {
+            return null;
+        }
+        
+        assert input != null;
+        final String contentType = input.getmember(RemotedHttpServletRequest.STRUCTURE_NAME)
+                .getmember(RemotedHttpServletRequest.CONTENT_TYPE)
+                .string();
+        if (!"application/x-www-form-urlencoded".equals(contentType)) {
+            log.warn("{} Unsupported content type: {}", getLogPrefix(), contentType);
+            return null;
+        }
+        
+        // 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.
+        return new String(raw, StandardCharsets.UTF_8);
+    }
+    
+    /**
+     * Checks for previously preserved data tracked against a state token submitted by the agent.
+     * 
+     * <p>This case is triggered by use of discovery, which would have preserved the data against the
+     * state token issued at that time, which we have since recovered and disposed of.</p>
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return previous data or null
+     */
+    @Nullable private String getPreviousData(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+        
+        final DDF input = agentRequestContext.getInput();
+        String previousStateToken = input != null ? input.getmember(SPConstants.STATE).string() : null;
+        if (previousStateToken == null) {
+            return null;
+        }
+
+        // Implement an "inline" recovery of the data to get it back and clear the original storage record.
+
+        if (previousStateToken.length() > 16) {
+            previousStateToken = previousStateToken.substring(0, 16);
+        }
+        
+        // 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(getCookieName(previousStateToken), null);
+            if (key == null) {
+                log.debug("{} No recovery cookie for state token {}, skipping POST recovery", getLogPrefix(),
+                        previousStateToken);
+                return null;
+            }
+            
+            // Unset the cookie.
+            cookieManager.unsetCookie(getCookieName(previousStateToken));
+            
+            // 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;
+    }
+    
+    /**
+     * Computes the cookie name based on the configured prefix and state token.
+     * 
+     * <p>Package private access for unit tests.</p>
+     * 
+     * @param stateToken the state token
+     * 
+     * @return the cookie name
+     */
+    @Nonnull String getCookieName(@Nonnull final String stateToken) {
+        return cookiePrefix + '_' + UrlEscapers.urlFormParameterEscaper().escape(stateToken);
+    }
+    
 }
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookie.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookie.java
deleted file mode 100644
index 8dc0fa0..0000000
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookie.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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.util.function.Function;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import com.google.common.escape.Escaper;
-import com.google.common.net.UrlEscapers;
-
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
-import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.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.sp.context.AgentRequestContext;
-import net.shibboleth.sp.context.TokenConsumerContext;
-import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
-import net.shibboleth.sp.profile.AbstractApplicationAction;
-
-/**
- * Action that processes a previously issued correlation cookie submitted with the request and
- * extracts the passive status and message ID for use by subsequent validation steps.
- * 
- * <p>The information captured is stored in a {@link TokenConsumerContext} child of the
- * {@link AgentRequestContext}.</p>
- * 
- * <p>The absence of state or the cookie are not treated as an error or fatal at this stage
- * due to the existence of unsolicited responses.</p>
- * 
- * @event {@link EventIds#PROCEED_EVENT_ID}
- * @event {@link EventIds#INVALID_MESSAGE}
- */
-public class ProcessCorrelationCookie extends AbstractApplicationAction {
-    
-    /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(ProcessCorrelationCookie.class);
-    
-    /** Cookie manager. */
-    @NonnullAfterInit private CookieManager cookieManager;
-    
-    /** Cookie prefix. */
-    @Nonnull private String cookiePrefix;
-        
-    /** Lookup strategy for state token value. */
-    @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
-    
-    /** State token accompanying request. */
-    @NonnullBeforeExec private String stateToken;
-    
-    /** Constructor. */
-    public ProcessCorrelationCookie() {
-        cookiePrefix = IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX;
-    }
-    
-    /**
-     * 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 IssueCorrelationCookie#DEFAULT_COOKIE_PREFIX}.</p>
-     * 
-     * @param prefix cookie prefix
-     */
-    public void setCookiePrefix(@Nonnull @NotEmpty final String prefix) {
-        checkSetterPreconditions();
-        
-        cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
-    }
-    
-    /**
-     * Sets the lookup strategy for obtaining the request message's ID.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setStateTokenLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
-        checkSetterPreconditions();
-        
-        stateTokenLookupStrategy = Constraint.isNotNull(strategy, "Request ID lookup strategy cannot be null");
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (cookieManager == null) {
-            throw new ComponentInitializationException("CookieManager cannot be null");
-        } else if (stateTokenLookupStrategy == null) {
-            throw new ComponentInitializationException("State token lookup strategy cannot be null");
-        }
-    }    
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-
-        stateToken = stateTokenLookupStrategy.apply(profileRequestContext);
-        if (stateToken == null) {
-            log.debug("{} No state token found in request, skipping correlation cookie processing", getLogPrefix());
-            return false;
-        }
-        
-        return true;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
-        ensureOutputObjects();
-        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
-        // We do the crazy stuff to accomodate the cookies being read and unset.
-        try {
-            RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
-                    agentRequestContext.getRemotedHttpServletResponse());
-
-            final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
-            final String cookieName = cookiePrefix + escaper.escape(stateToken);
-
-            final String value = cookieManager.getCookieValue(cookieName, null);
-            if (value == null) {
-                log.debug("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
-                return;
-            }
-
-            cookieManager.unsetCookie(cookieName);
-
-            final String decoded = URISupport.doURLDecode(value);
-            
-            log.debug("{} Extracted request ID '{}' for state token '{}'", getLogPrefix(), decoded, stateToken);
-
-            agentRequestContext.ensureSubcontext(TokenConsumerContext.class).setMessageCorrelationID(decoded);
-        } finally {
-            RemotedHttpServletRequestResponseContext.clearCurrent();
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverStateData.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverStateData.java
index 449e6ea..0f3cefa 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverStateData.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RecoverStateData.java
@@ -18,6 +18,7 @@ import java.io.IOException;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
@@ -32,19 +33,24 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
 import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
 import net.shibboleth.sp.state.StateData;
 
 /**
  * Action that maps a state token into a {@link StateData} (or subclass) object.
  * 
- * <p>The state token comes from a pluggable function but its absence will not result
- * in failure.</p>
+ * <p>The state token comes from a pluggable function.</p>
  * 
  * <p>The token and the recovered object will be set into a {@link StateDataContext}
  * created by a pluggable strategy.</p>
  * 
+ * <p>Errors resulting from the absence of a state token or a failure to recover associated state 
+ * may be ignored or result in an {@link EventIds#IO_ERROR} event. For example, if the protocol supports 
+ * unsolicited requests, a failure to retrieve prior state may be expected and should be ignored.</p>
+ * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
  * @event {@link EventIds#IO_ERROR}
@@ -54,7 +60,7 @@ import net.shibboleth.sp.state.StateData;
 public class RecoverStateData extends AbstractApplicationAction {
     
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(RecoverStateData.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RecoverStateData.class);
     
     /** Strategy used to create the {@link StateDataContext} to populate. */
     @Nonnull private Function<ProfileRequestContext,StateDataContext> stateDataContextCreationStrategy;
@@ -62,6 +68,9 @@ public class RecoverStateData extends AbstractApplicationAction {
     /** Lookup strategy for state token. */
     @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
     
+    /** Whether any failure to recover previously stored state should result in a fatal event. */
+    private boolean errorFatal;
+    
     /** Specific class to be recovered. */
     @Nonnull private Class<? extends StateData> stateDataClass;
     
@@ -72,6 +81,19 @@ public class RecoverStateData extends AbstractApplicationAction {
     public RecoverStateData() {
         stateDataContextCreationStrategy = new ChildContextLookup<>(StateDataContext.class, true);
         stateDataClass = StateData.class;
+        errorFatal = false;
+    }
+    
+    /**
+     * Sets the strategy used to create the {@link StateDataContext}.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setStateDataContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+        checkSetterPreconditions();
+        stateDataContextCreationStrategy =
+                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
     }
     
     /**
@@ -85,15 +107,16 @@ public class RecoverStateData extends AbstractApplicationAction {
     }
     
     /**
-     * Sets the strategy used to create the {@link StateDataContext}.
+     * Sets whether any failure to recover previously stored state should result in a fatal event.
      * 
-     * @param strategy creation strategy
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
      */
-    public void setStateDataContextCreationStrategy(
-            @Nonnull final Function<ProfileRequestContext,StateDataContext> strategy) {
+    public void setErrorFatal(final boolean flag) {
         checkSetterPreconditions();
-        stateDataContextCreationStrategy =
-                Constraint.isNotNull(strategy, "StateDataContext creation strategy cannot be null");
+        
+        errorFatal = flag;
     }
     
     /**
@@ -112,7 +135,7 @@ public class RecoverStateData extends AbstractApplicationAction {
         super.doInitialize();
         
         if (stateTokenLookupStrategy == null) {
-            throw new ComponentInitializationException("State token lookup strategy cannot be null");
+            stateTokenLookupStrategy = new DefaultStateTokenLookupStrategy();
         }
     }
 
@@ -125,8 +148,14 @@ public class RecoverStateData extends AbstractApplicationAction {
 
         stateToken = stateTokenLookupStrategy.apply(profileRequestContext);
         if (stateToken == null) {
-            log.debug("{} No state token returned from lookup strategy, nothing to do", getLogPrefix());
-            return false;
+            if (!errorFatal) {
+                log.debug("{} No state token returned from lookup strategy, nothing to do", getLogPrefix());
+                return false;
+            } else {
+                log.warn("{} No state token returned from lookup strategy, fatal error", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+                return false;
+            }
         }
         
         return true;
@@ -162,12 +191,39 @@ public class RecoverStateData extends AbstractApplicationAction {
                 log.debug("{} State data recovered from token: {}", getLogPrefix(), stateToken);
             } else {
                 log.warn("{} Unable to recover data from state token", getLogPrefix());
+                if (errorFatal) {
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+                }
             }
         } catch (final IOException e) {
             log.warn("{} Exception recovering data from state token", getLogPrefix(), e);
+            if (errorFatal) {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            }
         } finally {
             RemotedHttpServletRequestResponseContext.clearCurrent();
         }
     }
+    
+    /**
+     * Default lookup strategy locates state token from Agent request.
+     */
+    public static final class DefaultStateTokenLookupStrategy implements Function<ProfileRequestContext,String> {
+
+        /** {@inheritDoc} */
+        @Nullable public String apply(@Nullable final ProfileRequestContext input) {
+            if (input != null) {
+                final AgentRequestContext agentRequestContext = input.getSubcontext(AgentRequestContext.class);
+                if (agentRequestContext != null) {
+                    final DDF inputMessage = agentRequestContext.getInput();
+                    if (inputMessage != null) {
+                        return inputMessage.getmember(SPConstants.STATE).string();
+                    }
+                }
+            }
+            return null;
+        }
+        
+    }
 
 }
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
index 5753412..d1fef30 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/CookieStateManager.java
@@ -16,8 +16,6 @@ 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;
 
 import javax.annotation.Nonnull;
@@ -25,6 +23,8 @@ import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 
+import com.google.common.net.UrlEscapers;
+
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.codec.Base64Support;
@@ -33,11 +33,9 @@ import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.net.URISupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy;
-import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
-import net.shibboleth.shared.security.RandomIdentifierParameterSpec;
 import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.Application;
 import net.shibboleth.sp.state.AbstractStateManager;
@@ -92,16 +90,6 @@ public class CookieStateManager extends AbstractStateManager {
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
-
-        if (getIdentifierGenerationStrategy() == null) {
-            final RandomIdentifierParameterSpec spec = new RandomIdentifierParameterSpec(null, 6, null);
-            try {
-                setIdentifierGenerationStrategy(IdentifierGenerationStrategy.getInstance(ProviderType.SECURE, spec));
-            } catch (final InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
-                throw new ComponentInitializationException(e);
-            }
-        }
-        
         super.doInitialize();
         
         if (cookieManager == null) {
@@ -112,7 +100,7 @@ public class CookieStateManager extends AbstractStateManager {
     /** {@inheritDoc} */
     @Override
     @Nonnull protected String doPreserve(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String data) throws IOException {
+            @Nonnull final String data, final boolean sealed) throws IOException {
         
         cookieManager.purgeStaleCookies(cookiePrefix);
         
@@ -121,11 +109,17 @@ public class CookieStateManager extends AbstractStateManager {
         
         final String key = Long.toString(ts.toEpochMilli()) + '_' + generateToken();
         final String name = getCookieName(application, key);
-        try {
-            cookieManager.addCookie(name, Base64Support.encodeURLSafe(data.getBytes(StandardCharsets.UTF_8)),
+        
+        if (sealed) {
+            cookieManager.addCookie(name, UrlEscapers.urlFormParameterEscaper().escape(data),
                     (int) getExpiration().toSeconds());
-        } catch (final EncodingException e) {
-            throw new IOException(e);
+        } else {
+            try {
+                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, data);
@@ -136,20 +130,26 @@ public class CookieStateManager extends AbstractStateManager {
     /** {@inheritDoc} */
     @Override
     @Nullable protected String doRecover(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull @NotEmpty final String stateToken) throws IOException {
+            @Nonnull @NotEmpty final String stateToken, final boolean sealed) throws IOException {
         
         final String cookieName = getCookieName(application, stateToken);
         final String cookieValue = cookieManager.getCookieValue(cookieName, null);
 
         if (cookieValue != null) {
             cookieManager.unsetCookie(cookieName);
-            try {
-                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);
+
+            final String decoded;
+            if (sealed) {
+                decoded = URISupport.doURLDecode(cookieValue);
+            } else {
+                try {
+                    decoded = new String(Base64Support.decodeURLSafe(cookieValue), StandardCharsets.UTF_8);
+                } catch (final DecodingException e) {
+                    throw new IOException(e);
+                }
             }
+            log.trace("Recovered state token mapping from '{}' to value '{}'", stateToken, decoded);
+            return decoded;
         }
 
         log.warn("No cookie found matching state token: '{}'", stateToken);
@@ -171,7 +171,7 @@ public class CookieStateManager extends AbstractStateManager {
         
         final StringBuilder builder = new StringBuilder(cookiePrefix);
         builder.append('_')
-            .append(application.getId())
+            .append(application.getApplicationId())
             .append('_')
             .append(uniquePortion);
         
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/StorageServiceStateManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/StorageServiceStateManager.java
index 59858f7..96ba264 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/StorageServiceStateManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/state/impl/StorageServiceStateManager.java
@@ -121,7 +121,7 @@ public class StorageServiceStateManager extends AbstractStateManager {
     /** {@inheritDoc} */
     @Override
     @Nonnull protected String doPreserve(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull final String data) throws IOException {
+            @Nonnull final String data, final boolean sealed) throws IOException {
         
         final String context = getContext(agent, application);
         final String key = generateToken();
@@ -130,7 +130,7 @@ public class StorageServiceStateManager extends AbstractStateManager {
         
         String encoded;
         try {
-            encoded = getDataSealer() != null ? data : Base64Support.encode(data.getBytes(StandardCharsets.UTF_8), false);
+            encoded = sealed ? data : Base64Support.encode(data.getBytes(StandardCharsets.UTF_8), false);
         } catch (final EncodingException e) {
             throw new IOException(e);
         }
@@ -164,7 +164,7 @@ public class StorageServiceStateManager extends AbstractStateManager {
     /** {@inheritDoc} */
     @Override
     @Nullable protected String doRecover(@Nonnull final Agent agent, @Nonnull final Application application,
-            @Nonnull @NotEmpty final String stateToken) throws IOException {
+            @Nonnull @NotEmpty final String stateToken, final boolean sealed) throws IOException {
         
         final String key;
         final String context = getContext(agent, application);
@@ -194,7 +194,7 @@ public class StorageServiceStateManager extends AbstractStateManager {
                 log.warn("Unable to delete state record ('{}', '{}') from storage", context, key, e);
             }
             try {
-                final String decoded = getDataSealer() != null ? record.getValue() :
+                final String decoded = sealed ? record.getValue() :
                     new String(Base64Support.decode(record.getValue()), StandardCharsets.UTF_8);
                 log.trace("Recovered state record ('{}', '{}') with value '{}'", context, key, decoded);
                 return decoded;
@@ -236,7 +236,7 @@ public class StorageServiceStateManager extends AbstractStateManager {
         
         final StringBuilder builder = new StringBuilder(cookiePrefix);
         builder.append('_')
-            .append(application.getId())
+            .append(application.getApplicationId())
             .append('_')
             .append(uniquePortion);
         
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/authn/impl/InitializeAgentRequestContextTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/authn/impl/InitializeAgentRequestContextTest.java
index ea93a8a..5a9c685 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/authn/impl/InitializeAgentRequestContextTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/authn/impl/InitializeAgentRequestContextTest.java
@@ -34,7 +34,6 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.testing.MockReloadableService;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.impl.BasicAgent;
-import net.shibboleth.sp.impl.PassthroughStateTokenManager;
 import net.shibboleth.sp.impl.ProtocolSupportServiceManager;
 import net.shibboleth.sp.state.impl.StorageServiceStateManager;
 import net.shibboleth.sp.testing.MockAgentResolver;
@@ -62,21 +61,16 @@ public class InitializeAgentRequestContextTest {
         agent.setAttributeFilter(new MockReloadableService<>(null));
         agent.setAttributeTranscoderRegistry(new MockReloadableService<>(null));
         
-        final PassthroughStateTokenManager manager = new PassthroughStateTokenManager();
-        manager.setId("test");
-        manager.initialize();
-        agent.setStateTokenManager(manager);
-        
         final MemoryStorageService storage = new MemoryStorageService();
         storage.setId("test");
         storage.initialize();
         
-        final StorageServiceStateManager manager2 = new StorageServiceStateManager();
-        manager2.setId("test");
-        manager2.setObjectMapper(new JsonMapper());
-        manager2.setStorageService(storage);
-        manager2.initialize();
-        agent.setStateManager(manager2);
+        final StorageServiceStateManager manager = new StorageServiceStateManager();
+        manager.setId("test");
+        manager.setObjectMapper(new JsonMapper());
+        manager.setStorageService(storage);
+        manager.initialize();
+        agent.setStateManager(manager);
         
         agent.initialize();
         
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
deleted file mode 100644
index 2952d7a..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/CookieStateTokenManagerTest.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.annotation.Nonnull;
-
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.testng.Assert;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import jakarta.servlet.http.Cookie;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-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.BaseApplicationActionTest;
-
-/**
- * Unit tests for {@link CookieStateTokenManager}.
- */
- at SuppressWarnings("javadoc")
-public class CookieStateTokenManagerTest extends BaseApplicationActionTest {
-
-    private CookieManager cookieManager;
-    private CookieStateTokenManager stateManager;
-    
-    private MockHttpServletRequest request;
-    private MockHttpServletResponse response;
-    
-    @BeforeClass
-    public void setUp() throws ComponentInitializationException {
-        
-        cookieManager = new CookieManager();
-        cookieManager.setCookiePath("/");
-        cookieManager.setSameSite(SameSiteValue.None);
-        cookieManager.setCookieLimit(10);
-        cookieManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
-            @Nonnull public HttpServletRequest get() {
-                assert request != null;
-                return request;
-            }
-        });
-        cookieManager.setHttpServletResponseSupplier(new NonnullSupplier<HttpServletResponse>() {
-            @Nonnull public HttpServletResponse get() {
-                assert response != null;
-                return response;
-            }
-        });
-        cookieManager.initialize();
-        
-        stateManager = new CookieStateTokenManager();
-        stateManager.setId("test");
-        stateManager.setCookieManager(cookieManager);
-        
-        stateManager.initialize();
-    }
-    
-    @AfterClass
-    public void tearDown() {
-        stateManager.destroy();
-        cookieManager.destroy();
-    }
-    
-    @BeforeMethod
-    public void beforeMethod() throws ComponentInitializationException {
-        super.beforeMethod();
-        
-        request = new MockHttpServletRequest();
-        response = new MockHttpServletResponse();
-    }
-    
-    @Test
-    public void testMissing() throws IOException {
-        request.setCookies(new Cookie(getCookieName(), "foo"));
-        
-        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo"));
-    }
-
-    @Test
-    public void testPurge() throws IOException, InterruptedException {
-        
-        final List<Cookie> cookies = new ArrayList<>(12);
-        for (int i = 0; i < 12; ++i) {
-            cookies.add(new Cookie(getCookieName(), "foo" + i));
-            Thread.sleep(250);
-        }
-        request.setCookies(cookies.toArray(new Cookie[12]));
-        
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
-        assert token != null;
-        
-        final Cookie[] respCookies = response.getCookies();
-        Assert.assertEquals(respCookies.length, 3);
-        Assert.assertEquals(respCookies[0].getMaxAge(), 0);
-        Assert.assertEquals(respCookies[1].getMaxAge(), 0);
-        Assert.assertEquals(respCookies[2].getMaxAge(), stateManager.getExpiration().toSeconds());
-    }
-
-    @Test
-    public void testMapRecover() throws IOException {
-        
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
-        assert token != null;
-        
-        // Move token set on response to request.
-        request = new MockHttpServletRequest();
-        request.setCookies(response.getCookies());
-        response = new MockHttpServletResponse();
-        
-        final byte[] original = stateManager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(original, "foo".getBytes());
-        
-        // 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].getValue(), null);
-        Assert.assertEquals(cookies[0].getMaxAge(), 0);
-        Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
-    }
-    
-    @Nonnull private String getCookieName() {
-        final Instant now = Instant.now();
-        final String rand = stateManager.getIdentifierGenerationStrategy().generateIdentifier(false);
-        return stateManager.getCookieName(application, now.toEpochMilli() + '_' + rand);
-    }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index c78bfcb..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/PassthroughStateTokenManagerTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-
-import org.testng.Assert;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
-
-/**
- * Unit tests for {@link PassthroughStateTokenManager}.
- */
- at SuppressWarnings("javadoc")
-public class PassthroughStateTokenManagerTest extends BaseApplicationActionTest {
-
-    private PassthroughStateTokenManager stateManager;
-    
-    @BeforeClass
-    public void setUp() throws ComponentInitializationException {
-        
-        stateManager = new PassthroughStateTokenManager();
-        stateManager.setId("test");
-        stateManager.initialize();
-    }
-    
-    @AfterClass
-    public void tearDown() {
-        stateManager.destroy();
-    }
-    
-    @BeforeMethod
-    public void beforeMethod() throws ComponentInitializationException {
-        super.beforeMethod();
-    }
-    
-    @Test
-    public void testInvalid() throws IOException {
-        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "123"));
-    }
-    
-    @Test
-    public void testMapRecover() throws IOException {
-        
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
-        assert token != null;
-        
-        final byte[] original = stateManager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(original, "foo".getBytes());
-    }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index e9f1a0c..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/impl/StorageServiceStateTokenManagerTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.impl;
-
-import java.io.IOException;
-import java.time.Duration;
-
-import org.opensaml.storage.impl.MemoryStorageService;
-import org.testng.Assert;
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
-
-/**
- * Unit tests for {@link StorageServiceStateTokenManager}.
- */
- at SuppressWarnings("javadoc")
-public class StorageServiceStateTokenManagerTest extends BaseApplicationActionTest {
-
-    private MemoryStorageService storageService;
-    private StorageServiceStateTokenManager stateManager;
-    
-    @BeforeClass
-    public void setUp() throws ComponentInitializationException {
-        storageService = new MemoryStorageService();
-        storageService.setId("test");
-        storageService.setCleanupInterval(Duration.ZERO);
-        storageService.initialize();
-        
-        stateManager = new StorageServiceStateTokenManager();
-        stateManager.setStorageService(storageService);
-        stateManager.setId("test");
-        stateManager.initialize();
-    }
-    
-    @AfterClass
-    public void tearDown() {
-        stateManager.destroy();
-        storageService.destroy();
-    }
-    
-    @BeforeMethod
-    public void beforeMethod() throws ComponentInitializationException {
-        super.beforeMethod();
-    }
-    
-    @Test
-    public void testMissing() throws IOException {
-        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo"));
-    }
-    
-    @Test
-    public void testMapRecover() throws IOException {
-        
-        final String token = stateManager.preserveToStateToken(agent, application, "foo".getBytes());
-        assert token != null;
-        
-        final byte[] original = stateManager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(original, "foo".getBytes());
-
-        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, token));
-    }
-
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgentRequestTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgentRequestTest.java
index 91bdae5..4ff1a39 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgentRequestTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/BaseAgentRequestTest.java
@@ -27,7 +27,6 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.testing.MockReloadableService;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.impl.BasicAgent;
-import net.shibboleth.sp.impl.PassthroughStateTokenManager;
 import net.shibboleth.sp.impl.ProtocolSupportServiceManager;
 import net.shibboleth.sp.state.impl.StorageServiceStateManager;
 
@@ -57,11 +56,6 @@ public abstract class BaseAgentRequestTest extends OpenSAMLInitBaseTestCase {
         agent.setAttributeFilter(new MockReloadableService<>(null));
         agent.setAttributeTranscoderRegistry(new MockReloadableService<>(null));
         
-        final PassthroughStateTokenManager manager = new PassthroughStateTokenManager();
-        manager.setId("test");
-        manager.initialize();
-        agent.setStateTokenManager(manager);
-
         final MemoryStorageService storage = new MemoryStorageService();
         storage.setId("test");
         storage.initialize();
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
deleted file mode 100644
index 9a89f89..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/IssueCorrelationCookieTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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.util.ArrayList;
-import java.util.List;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-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.DataProvider;
-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.DecodingException;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.net.CookieManager;
-import net.shibboleth.shared.net.CookieManager.SameSiteValue;
-import net.shibboleth.shared.primitive.NonnullSupplier;
-import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.profile.SPConstants;
-
-/**
- * Unit test for {@link IssueCorrelationCookie} action.
- */
- at SuppressWarnings("javadoc")
-public class IssueCorrelationCookieTest extends BaseApplicationActionTest {
-
-    @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
-    @Nonnull @NotEmpty private final static String TEST_ID = "123456789";
-
-    private String requestId;
-    
-    private DDF input;
-    private MockHttpServletRequest request;
-    private MockHttpServletResponse response;
-    
-    private CookieManager cookieManager;
-    private IssueCorrelationCookie action;
-        
-    /**
-     * Set up test.
-     * 
-     * @throws ComponentInitializationException
-     */
-    @BeforeMethod
-    public void setUp() throws ComponentInitializationException {
-        super.beforeMethod();
-        
-        request = new MockHttpServletRequest();
-        response = new MockHttpServletResponse();
-        
-        cookieManager = new CookieManager();
-        cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
-        cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
-        cookieManager.setCookieLimit(10);
-        cookieManager.setSameSite(SameSiteValue.None);
-        cookieManager.setMaxAge(-1);
-        cookieManager.initialize();
-        
-        action = new IssueCorrelationCookie();
-        action.setCookieManager(cookieManager);
-        action.setRequestIDLookupStrategy(new Function<>() {
-            public String apply(ProfileRequestContext t) {
-                return requestId;
-            }
-        });
-        
-        action.setErrorFatal(true);
-        action.initialize();
-
-        input = new DDF(null).structure();
-        arc.setInput(input);
-    }
-    
-    /**
-     * Tear down test.
-     */
-    @AfterMethod
-    public void tearDown() {
-        action.destroy();
-        cookieManager.destroy();
-    }
-
-    @DataProvider
-    Object[][] correlationData() {
-        return new Object[][] {
-            new Object[] { null, TEST_ID},
-            new Object[] { TEST_STATE, null},
-            new Object[] { TEST_STATE, TEST_ID},
-        };
-    }
-        
-    @Test(dataProvider="correlationData")
-    public void testAction(final String state, final String id) {
-        evaluateAction(state, id);
-    }
-    
-    private void evaluateAction(final String state, final String id) {
-        requestId = id;
-        if (state != null) {
-            input.addmember(SPConstants.STATE).string(state);
-        }
-        
-        final Event event = action.execute(src);
-        
-        if (state != null) {
-            ActionTestingSupport.assertProceedEvent(event);
-        } else {
-            ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
-            return;
-        }
-        
-        if (id == null) {
-            Assert.assertEquals(response.getCookies().length, 0);
-            return;
-        }
-        
-        final Cookie cookie = response.getCookie(IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX + state);
-        assert cookie != null;
-        Assert.assertEquals(cookie.getValue(), requestId);
-        Assert.assertEquals(cookie.getMaxAge(), -1);
-        Assert.assertEquals(cookie.getAttribute("SameSite"), SameSiteValue.None.getValue());
-    }
-    
-    @Test
-    public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
-        
-        final List<Cookie> cookies = new ArrayList<>(12);
-        for (int i = 0; i < 12; ++i) {
-            cookies.add(new Cookie(IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
-            Thread.sleep(250);
-        }
-        request.setCookies(cookies.toArray(new Cookie[12]));
-        
-        evaluateAction(TEST_STATE, TEST_ID);
-        
-        Assert.assertEquals(response.getCookies().length, 3);
-    }
-
-}
\ No newline at end of file
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 089ed29..e809910 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
@@ -33,11 +33,11 @@ import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 import net.shibboleth.sp.messaging.impl.RemotedlHttpServletResponseSupplier;
 import net.shibboleth.sp.profile.InitiatorConstants;
-import net.shibboleth.sp.profile.SPConstants;
 
 /**
  * Unit test for {@link IssueDiscoveryRequest} action.
@@ -155,10 +155,12 @@ public class IssueDiscoveryRequestTest extends BaseApplicationActionTest {
     @Test
     public void testState() throws ComponentInitializationException {
         final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.STATE).string("foo bar");
         input.addmember(InitiatorConstants.DISCOVERY_RETURN_URL).string(escaper.escape("https://sp.example.org/handler?DS=1"));
         arc.setInput(input);
 
+        // Populate state for action.
+        prc.ensureSubcontext(StateDataContext.class).setStateToken("foo bar");
+        
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         
@@ -170,15 +172,16 @@ public class IssueDiscoveryRequestTest extends BaseApplicationActionTest {
         Assert.assertEquals(redirect, buildDiscoveryURL(TEST_DISCOVERY_URL, TEST_RESPONSE_URL, "foo bar", false));
     }
 
-
     @Test
     public void testPassive() throws ComponentInitializationException {
         final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.STATE).string("foo bar");
         input.addmember(InitiatorConstants.DISCOVERY_RETURN_URL).string(escaper.escape("https://sp.example.org/handler?DS=1"));
         input.addmember("prompt").string("none");
         arc.setInput(input);
 
+        // Populate state for action.
+        prc.ensureSubcontext(StateDataContext.class).setStateToken("foo bar");
+        
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         
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
deleted file mode 100644
index a38e959..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/MapResourceToStateTokenTest.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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.nio.charset.StandardCharsets;
-import java.time.Duration;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.action.EventIds;
-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 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.primitive.NonnullSupplier;
-import net.shibboleth.sp.ddf.DDF;
-import net.shibboleth.sp.impl.CookieStateTokenManager;
-import net.shibboleth.sp.impl.PassthroughStateTokenManager;
-import net.shibboleth.sp.impl.StorageServiceStateTokenManager;
-import net.shibboleth.sp.profile.SPConstants;
-
-/**
- * Unit test for {@link MapResourceToStateToken} action.
- */
- at SuppressWarnings("javadoc")
-public class MapResourceToStateTokenTest extends BaseApplicationActionTest {
-
-    /** Test request URL. */
-    @Nonnull @NotEmpty private final static String TEST_URL = "https://sp.example.org/cgi-bin/test.cgi";
-
-    /** Test query string. */
-    @Nonnull @NotEmpty private final static String TEST_QUERY = TEST_URL + "?foo=bar%20baz&frobnitz=zorkmid";
-
-    private MapResourceToStateToken action;
-        
-    /**
-     * Set up test.
-     * 
-     * @throws ComponentInitializationException
-     */
-    @BeforeMethod
-    public void setUp() throws ComponentInitializationException {
-        super.beforeMethod();
-        
-        action = new MapResourceToStateToken();
-        action.initialize();
-    }
-    
-    /**
-     * Tear down test.
-     */
-    @AfterMethod
-    public void tearDown() {
-        action.destroy();
-    }
-
-    @Test
-    public void testNoTarget() {
-        final DDF input = new DDF(null).structure();
-        arc.setInput(input);
-        
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
-    }
-
-    @Test
-    public void testStateExists() {
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.STATE).string("foo");
-        arc.setInput(input);
-        
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        Assert.assertEquals(input.getmember(SPConstants.STATE).string(), "foo");
-    }
-
-    @Test
-    public void testPassthroughURL() throws ComponentInitializationException, DecodingException {
-        
-        final PassthroughStateTokenManager manager = new PassthroughStateTokenManager();
-        manager.setId("test");
-        manager.initialize();
-        
-        application.setStateTokenManager(manager);
-        application.initialize();
-        
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.TARGET).unsafe_string(TEST_URL.getBytes(StandardCharsets.UTF_8));
-        arc.setInput(input);
-
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        final String token = input.getmember(SPConstants.STATE).string();
-        assert token != null;
-        
-        Assert.assertEquals(Base64Support.decodeURLSafe(token), TEST_URL.getBytes());
-    }
-
-    @Test
-    public void testPassthroughURLAndQuery() throws ComponentInitializationException, DecodingException {
-        
-        final PassthroughStateTokenManager manager = new PassthroughStateTokenManager();
-        manager.setId("test");
-        manager.initialize();
-        
-        application.setStateTokenManager(manager);
-        application.initialize();
-        
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.TARGET).unsafe_string(TEST_QUERY.getBytes());
-        arc.setInput(input);
-
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        final String token = input.getmember(SPConstants.STATE).string();
-        assert token != null;
-        
-        Assert.assertEquals(Base64Support.decodeURLSafe(token), TEST_QUERY.getBytes(StandardCharsets.UTF_8));
-    }
-
-    @Test
-    public void testStorageURL() throws ComponentInitializationException, DecodingException, IOException {
-        
-        final MemoryStorageService storage = new MemoryStorageService();
-        storage.setId("test");
-        storage.setCleanupInterval(Duration.ZERO);
-        storage.initialize();
-        
-        final StorageServiceStateTokenManager manager = new StorageServiceStateTokenManager();
-        manager.setId("test");
-        manager.setStorageService(storage);
-        manager.initialize();
-        
-        application.setStateTokenManager(manager);
-        application.initialize();
-        
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.TARGET).unsafe_string(TEST_URL.getBytes(StandardCharsets.UTF_8));
-        arc.setInput(input);
-
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        final String token = input.getmember(SPConstants.STATE).string();
-        assert token != null;
-        
-        final byte[] value = manager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(value, TEST_URL.getBytes(StandardCharsets.UTF_8));
-    }
-    
-    @Test
-    public void testCookieURL() throws ComponentInitializationException, DecodingException, IOException {
-        
-        final MockHttpServletRequest request = new MockHttpServletRequest();
-        final MockHttpServletResponse response = new MockHttpServletResponse();
-        
-        final CookieManager cookieManager = new CookieManager();
-        cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
-        cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
-        cookieManager.initialize();
-        
-        final CookieStateTokenManager manager = new CookieStateTokenManager();
-        manager.setId("test");
-        manager.setCookieManager(cookieManager);
-        manager.initialize();
-        
-        application.setStateTokenManager(manager);
-        application.initialize();
-        
-        final DDF input = new DDF(null).structure();
-        input.addmember(SPConstants.TARGET).unsafe_string(TEST_URL.getBytes(StandardCharsets.UTF_8));
-        arc.setInput(input);
-
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        final String token = input.getmember(SPConstants.STATE).string();
-        assert token != null;
-        
-        request.setCookies(response.getCookies());
-        
-        final byte[] value = manager.recoverFromStateToken(agent, application, token);
-        Assert.assertEquals(value, TEST_URL.getBytes(StandardCharsets.UTF_8));
-    }
-
-}
\ 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 2fd2828..a9fb9cf 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
@@ -33,12 +33,16 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.net.CookieManager;
 import net.shibboleth.shared.net.CookieManager.SameSiteValue;
 import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.sp.context.StateDataContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.profile.SPConstants;
@@ -75,8 +79,16 @@ public class PreservePostDataTest extends BaseApplicationActionTest {
         response = new MockHttpServletResponse();
         
         cookieManager = new CookieManager();
-        cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
-        cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
+        cookieManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
+            @Nonnull public HttpServletRequest get() {
+                return request;
+            }
+        });
+        cookieManager.setHttpServletResponseSupplier(new NonnullSupplier<HttpServletResponse>() {
+            @Nonnull public HttpServletResponse get() {
+                return response;
+            }
+        });
         cookieManager.setCookieLimit(10);
         cookieManager.setSameSite(SameSiteValue.None);
         cookieManager.setMaxAge(-1);
@@ -94,7 +106,6 @@ public class PreservePostDataTest extends BaseApplicationActionTest {
         action.initialize();
 
         input = new DDF(null).structure();
-        input.addmember(SPConstants.STATE).string(TEST_STATE);
         final DDF http = input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME);
         http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
         http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(TEST_DATA.getBytes());
@@ -146,18 +157,20 @@ public class PreservePostDataTest extends BaseApplicationActionTest {
     
     @Test
     public void testNoStateToken() {
-        input.addmember(SPConstants.STATE).remove();
         final Event event = action.execute(src);
         ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
     }
     
     @Test
     public void testSuccess() throws IOException {
+        
+        prc.ensureSubcontext(StateDataContext.class).setStateToken(TEST_STATE);
+        
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         Assert.assertEquals(response.getCookies().length, 1);
         
-        final Cookie cookie = response.getCookie(PreservePostData.DEFAULT_COOKIE_PREFIX + TEST_STATE);
+        final Cookie cookie = response.getCookie(action.getCookieName(TEST_STATE));
         assert cookie != null;
         Assert.assertEquals(cookie.getMaxAge(), -1);
         Assert.assertEquals(cookie.getAttribute("SameSite"), SameSiteValue.None.getValue());
@@ -172,18 +185,63 @@ public class PreservePostDataTest extends BaseApplicationActionTest {
         
     @Test
     public void testPurge() throws ComponentInitializationException, InterruptedException {
-        
+
         final List<Cookie> cookies = new ArrayList<>(12);
         for (int i = 0; i < 12; ++i) {
-            cookies.add(new Cookie(PreservePostData.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
+            cookies.add(new Cookie(PreservePostData.DEFAULT_COOKIE_PREFIX + '_' + i, "foo" + i));
             Thread.sleep(250);
         }
         request.setCookies(cookies.toArray(new Cookie[12]));
         
+        prc.ensureSubcontext(StateDataContext.class).setStateToken(TEST_STATE);
+        
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
         
         Assert.assertEquals(response.getCookies().length, 3);
     }
 
+    @Test
+    public void testPreviousRecovery() throws IOException {
+        
+        // First run with inline state to preserve data.
+        testSuccess();
+        
+        Cookie cookie = response.getCookie(action.getCookieName(TEST_STATE));
+        assert cookie != null;
+        Assert.assertEquals(cookie.getMaxAge(), -1);
+        
+        final String originalKey = cookie.getValue();
+
+        // Now re-run with that state supplied via input message and mutate new state.
+
+        Constraint.isNotNull(arc.getInput(), "Input was null").addmember(SPConstants.STATE).string(TEST_STATE);
+        Constraint.isNotNull(arc.getInput(), "Input was null").getmember(RemotedHttpServletRequest.STRUCTURE_NAME).destroy();
+        prc.ensureSubcontext(StateDataContext.class).setStateToken(TEST_STATE + "2");
+        
+        // Transfer cookies from response to new request.
+        request = new MockHttpServletRequest();
+        request.setCookies(response.getCookies());
+        response = new MockHttpServletResponse();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(response.getCookies().length, 2);
+        
+        cookie = response.getCookie(action.getCookieName(TEST_STATE + "2"));
+        assert cookie != null;
+        Assert.assertEquals(cookie.getMaxAge(), -1);
+        Assert.assertEquals(cookie.getAttribute("SameSite"), SameSiteValue.None.getValue());
+        
+        final String key = cookie.getValue();
+        
+        final StorageRecord<String> record = storageService.read(agent.getId() + ".PostData", key);
+        assert record != null;
+        Assert.assertEquals(record.getVersion(), 1);
+        Assert.assertEquals(record.getValue(), TEST_DATA);
+        
+        // Make sure original record is gone.
+        Assert.assertNull(storageService.read(agent.getId() + ".PostData", originalKey));
+    }
+    
 }
\ No newline at end of file
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
deleted file mode 100644
index 9c24163..0000000
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/profile/impl/ProcessCorrelationCookieTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.util.function.Function;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-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.DataProvider;
-import org.testng.annotations.Test;
-
-import com.google.common.escape.Escaper;
-import com.google.common.net.UrlEscapers;
-
-import jakarta.servlet.http.Cookie;
-import net.shibboleth.idp.profile.testing.ActionTestingSupport;
-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.context.TokenConsumerContext;
-import net.shibboleth.sp.ddf.DDF;
-
-/**
- * Unit test for {@link ProcessCorrelationCookie} action.
- */
- at SuppressWarnings("javadoc")
-public class ProcessCorrelationCookieTest extends BaseApplicationActionTest {
-
-    @Nonnull @NotEmpty private final static String TEST_STATE = "foo";
-    @Nonnull @NotEmpty private final static String TEST_ID = "123456789";
-
-    private String state;
-    
-    private DDF input;
-    private MockHttpServletRequest request;
-    private MockHttpServletResponse response;
-    
-    private CookieManager cookieManager;
-    private ProcessCorrelationCookie action;
-        
-    /**
-     * Set up test.
-     * 
-     * @throws ComponentInitializationException
-     */
-    @BeforeMethod
-    public void setUp() throws ComponentInitializationException {
-        super.beforeMethod();
-        
-        request = new MockHttpServletRequest();
-        response = new MockHttpServletResponse();
-        
-        cookieManager = new CookieManager();
-        cookieManager.setHttpServletRequestSupplier(NonnullSupplier.of(request));
-        cookieManager.setHttpServletResponseSupplier(NonnullSupplier.of(response));
-        cookieManager.setCookieLimit(10);
-        cookieManager.setSameSite(SameSiteValue.None);
-        cookieManager.setMaxAge(-1);
-        cookieManager.initialize();
-        
-        action = new ProcessCorrelationCookie();
-        action.setCookieManager(cookieManager);
-        action.setStateTokenLookupStrategy(new Function<>() {
-            public String apply(ProfileRequestContext t) {
-                return state;
-            }
-        });
-        action.initialize();
-    }
-    
-    /**
-     * Tear down test.
-     */
-    @AfterMethod
-    public void tearDown() {
-        action.destroy();
-        cookieManager.destroy();
-    }
-
-    @DataProvider
-    Object[][] correlationData() {
-        return new Object[][] {
-            new Object[] {false, null, null},
-            new Object[] {false, TEST_STATE, null},
-            new Object[] {true, null, null},
-            new Object[] {true, TEST_STATE, TEST_ID},
-        };
-    }
-        
-    @Test(dataProvider="correlationData")
-    public void testAction(final Boolean createCookie, final String token, final String id) {
-        state = token;
-
-        if (createCookie && token != null) {
-            final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
-            final Cookie cookie = new Cookie(IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX + escaper.escape(token), id);
-            request.setCookies(cookie);
-        }
-
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertProceedEvent(event);
-        
-        Assert.assertEquals(response.getCookies().length, createCookie && token != null ? 1 : 0);
-
-        final TokenConsumerContext tcc = arc.getSubcontext(TokenConsumerContext.class);
-        if (id != null) {
-            assert tcc != null;
-            Assert.assertEquals(tcc.getMessageCorrelationID(), id);
-        } else {
-            Assert.assertNull(tcc);
-        }
-    }
-    
-}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
index 8689881..9352e26 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/CookieStateManagerTest.java
@@ -22,11 +22,11 @@ import java.util.List;
 
 import javax.annotation.Nonnull;
 
+import org.springframework.core.io.ClassPathResource;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.testng.Assert;
 import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -41,8 +41,12 @@ 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.resource.Resource;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.impl.BasicKeystoreKeyStrategy;
 import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
 import net.shibboleth.sp.state.StateData;
+import net.shibboleth.sp.testing.TestResourceConverter;
 
 /**
  * Unit tests for {@link CookieStateManager}.
@@ -57,15 +61,17 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
     // Unicode character at the end of that filename...
     @Nonnull @NotEmpty private static final String TEST_RESOURCE = "https://sp.example.org/secure/foo☯.cgi";
     
+    private Resource keystoreResource;
+    private Resource versionResource;
+    
+    private DataSealer sealer;
     private CookieManager cookieManager;
     private CookieStateManager stateManager;
     
     private MockHttpServletRequest request;
     private MockHttpServletResponse response;
     
-    @BeforeClass
-    public void setUp() throws ComponentInitializationException {
-        
+    public void setUp(final boolean sealed) throws ComponentInitializationException {
         cookieManager = new CookieManager();
         cookieManager.setCookiePath("/");
         cookieManager.setSameSite(SameSiteValue.None);
@@ -83,30 +89,57 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
             }
         });
         cookieManager.initialize();
-        
+                
         stateManager = new CookieStateManager();
         stateManager.setId("test");
 
+        if (sealed) {
+            ClassPathResource resource =
+                    new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.jks");
+            Assert.assertTrue(resource.exists());
+            keystoreResource = TestResourceConverter.of(resource);
+
+            resource =
+                    new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.kver");
+            Assert.assertTrue(resource.exists());
+            versionResource = TestResourceConverter.of(resource);
+
+            final BasicKeystoreKeyStrategy strategy = new BasicKeystoreKeyStrategy();
+            strategy.setKeyAlias("secret");
+            strategy.setKeyPassword("kpassword");
+            strategy.setKeystorePassword("password");
+            strategy.setKeystoreResource(keystoreResource);
+            strategy.setKeyVersionResource(versionResource);
+            strategy.initialize();
+            
+            final DataSealer sealer = new DataSealer();
+            sealer.setKeyStrategy(strategy);
+            sealer.initialize();
+            
+            stateManager.setDataSealer(sealer);
+        }
+        
         final ObjectMapper mapper = new ObjectMapper();
         mapper.registerModule(new JavaTimeModule());
-        stateManager.setObjectMapper(mapper);
-        
+        stateManager.setObjectMapper(mapper);        
         stateManager.setHttpServletRequestSupplier(new NonnullSupplier<HttpServletRequest>() {
             @Nonnull public HttpServletRequest get() {
                 assert request != null;
                 return request;
             }
         });
-        
         stateManager.setCookieManager(cookieManager);
-        
         stateManager.initialize();
     }
-    
+
     @AfterClass
     public void tearDown() {
         stateManager.destroy();
         cookieManager.destroy();
+        if (sealer != null) {
+            sealer.destroy();
+        }
+        
     }
     
     @BeforeMethod
@@ -118,14 +151,17 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
     }
     
     @Test
-    public void testMissing() throws IOException {
+    public void testMissing() throws IOException, ComponentInitializationException {
+        setUp(false);
+        
         request.setCookies(new Cookie(getCookieName(), "foo"));
         
         Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo", StateData.class));
     }
 
     @Test
-    public void testPurge() throws IOException, InterruptedException {
+    public void testPurge() throws IOException, InterruptedException, ComponentInitializationException {
+        setUp(false);
         
         final List<Cookie> cookies = new ArrayList<>(12);
         for (int i = 0; i < 12; ++i) {
@@ -145,7 +181,33 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
     }
 
     @Test
-    public void testSuccess() throws IOException {
+    public void testSuccessSealed() throws IOException, ComponentInitializationException {
+        setUp(true);
+        
+        final StateData source = buildStateData();
+        final String token = stateManager.preserveToStateToken(agent, application, source);
+        assert token != null;
+        
+        // Move token set on response to request.
+        request = new MockHttpServletRequest();
+        request.setCookies(response.getCookies());
+        response = new MockHttpServletResponse();
+        
+        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(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
+        Assert.assertEquals(cookies[0].getValue(), null);
+        Assert.assertEquals(cookies[0].getMaxAge(), 0);
+        Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
+    }
+    
+    @Test
+    public void testSuccess() throws IOException, ComponentInitializationException {
+        setUp(false);
         
         final StateData source = buildStateData();
         final String token = stateManager.preserveToStateToken(agent, application, source);
@@ -162,14 +224,15 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
     }
 
     @Test
-    public void testSuccessWithAddress() throws IOException {
+    public void testSuccessWithAddress() throws IOException, ComponentInitializationException {
+        setUp(false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -188,14 +251,15 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
     }
 
     @Test
-    public void testFailureWithAddress() throws IOException {
+    public void testFailureWithAddress() throws IOException, ComponentInitializationException {
+        setUp(false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -214,7 +278,7 @@ public class CookieStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/StorageServiceStateManagerTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/StorageServiceStateManagerTest.java
index 366747e..f83c6d0 100644
--- a/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/StorageServiceStateManagerTest.java
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/state/impl/StorageServiceStateManagerTest.java
@@ -22,6 +22,7 @@ import java.time.Instant;
 import javax.annotation.Nonnull;
 
 import org.opensaml.storage.impl.MemoryStorageService;
+import org.springframework.core.io.ClassPathResource;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.testng.Assert;
@@ -41,8 +42,12 @@ 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.resource.Resource;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.impl.BasicKeystoreKeyStrategy;
 import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
 import net.shibboleth.sp.state.StateData;
+import net.shibboleth.sp.testing.TestResourceConverter;
 
 /**
  * Unit tests for {@link StorageServiceStateManager}.
@@ -60,6 +65,10 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
     private MockHttpServletRequest request;
     private MockHttpServletResponse response;
     
+    private Resource keystoreResource;
+    private Resource versionResource;
+    
+    private DataSealer sealer;
     private CookieManager cookieManager;
     private MemoryStorageService storageService;
     
@@ -87,11 +96,35 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
         storageService.setId("test");
         storageService.setCleanupInterval(Duration.ZERO);
         storageService.initialize();
+    
+        ClassPathResource resource =
+                new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.jks");
+        Assert.assertTrue(resource.exists());
+        keystoreResource = TestResourceConverter.of(resource);
+
+        resource =
+                new ClassPathResource("/net/shibboleth/sp/profile/impl/SealerKeyStore.kver");
+        Assert.assertTrue(resource.exists());
+        versionResource = TestResourceConverter.of(resource);
+
+        final BasicKeystoreKeyStrategy strategy = new BasicKeystoreKeyStrategy();
+        strategy.setKeyAlias("secret");
+        strategy.setKeyPassword("kpassword");
+        strategy.setKeystorePassword("password");
+        strategy.setKeystoreResource(keystoreResource);
+        strategy.setKeyVersionResource(versionResource);
+        strategy.initialize();
+        
+        sealer = new DataSealer();
+        sealer.setKeyStrategy(strategy);
+        sealer.initialize();
     }
     
     @AfterClass
     public void tearDown() {
+        cookieManager.destroy();
         storageService.destroy();
+        sealer.destroy();
     }
     
     @BeforeMethod
@@ -104,13 +137,13 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
     
     @Test
     public void testMissingNoCookie() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(false);
+        final var stateManager = getStateManager(false, false);
         Assert.assertNull(stateManager.recoverFromStateToken(agent, application, "foo", StateData.class));
     }
     
     @Test
     public void testSuccessNoCookie() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(false);
+        final var stateManager = getStateManager(false, false);
         
         final StateData source = buildStateData();
         
@@ -123,9 +156,24 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
         Assert.assertNull(stateManager.recoverFromStateToken(agent, application, token, StateData.class));
     }
 
+    @Test
+    public void testSuccessNoCookieSealed() throws IOException, ComponentInitializationException {
+        final var stateManager = getStateManager(false, true);
+        
+        final StateData source = buildStateData();
+        
+        final String token = stateManager.preserveToStateToken(agent, application, source);
+        assert token != null;
+        
+        final StateData recovered = stateManager.recoverFromStateToken(agent, application, token, StateData.class);
+        Assert.assertEquals(source, recovered);
+
+        Assert.assertNull(stateManager.recoverFromStateToken(agent, application, token, StateData.class));
+    }
+    
     @Test
     public void testSuccessNoCookieWithAddress() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(false);
+        final var stateManager = getStateManager(false, false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -144,7 +192,7 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
 
     @Test
     public void testFailureNoCookieWithAddress() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(false);
+        final var stateManager = getStateManager(false, false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -163,7 +211,7 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
     
     @Test
     public void testSuccessWithCookie() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(true);
+        final var stateManager = getStateManager(true, false);
         
         final StateData source = buildStateData();
         
@@ -181,7 +229,33 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
+        Assert.assertEquals(cookies[0].getValue(), null);
+        Assert.assertEquals(cookies[0].getMaxAge(), 0);
+        Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
+    }
+
+    @Test
+    public void testSuccessWithCookieSealed() throws IOException, ComponentInitializationException {
+        final var stateManager = getStateManager(true, true);
+        
+        final StateData source = buildStateData();
+        
+        final String token = stateManager.preserveToStateToken(agent, application, source);
+        assert token != null;
+        
+        // Move cookie set on response to request.
+        request = new MockHttpServletRequest();
+        request.setCookies(response.getCookies());
+        response = new MockHttpServletResponse();
+        
+        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(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
@@ -189,7 +263,7 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
     
     @Test
     public void testSuccessWithCookieWithAddress() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(true);
+        final var stateManager = getStateManager(true, false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -209,7 +283,7 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
@@ -217,7 +291,7 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
 
     @Test
     public void testFailureWithCookieWithAddress() throws IOException, ComponentInitializationException {
-        final var stateManager = getStateManager(true);
+        final var stateManager = getStateManager(true, false);
         
         final StateData source = buildStateData();
         source.setClientAddress("192.168.1.1");
@@ -237,13 +311,13 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
         // Check that old token is unset.
         final Cookie[] cookies = response.getCookies();
         Assert.assertEquals(cookies.length, 1);
-        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "test" + '_' + token);
+        Assert.assertEquals(cookies[0].getName(), CookieStateManager.DEFAULT_PREFIX + '_' + "default" + '_' + token);
         Assert.assertEquals(cookies[0].getValue(), null);
         Assert.assertEquals(cookies[0].getMaxAge(), 0);
         Assert.assertEquals(cookies[0].getAttribute("SameSite"), SameSiteValue.None.getValue());
     }
     
-    @Nonnull private StorageServiceStateManager getStateManager(final boolean useCookie)
+    @Nonnull private StorageServiceStateManager getStateManager(final boolean useCookie, final boolean sealed)
             throws ComponentInitializationException {
         
         final var stateManager = new StorageServiceStateManager();
@@ -254,6 +328,10 @@ public class StorageServiceStateManagerTest extends BaseApplicationActionTest {
             stateManager.setCookieManager(cookieManager);
         }
         
+        if (sealed) {
+            stateManager.setDataSealer(sealer);
+        }
+        
         final ObjectMapper mapper = new ObjectMapper();
         mapper.registerModule(new JavaTimeModule());
         stateManager.setObjectMapper(mapper);

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


More information about the commits mailing list