[java-plugin-shibd-oidc] branch main updated: JSHIBDOIDC-5 - Add correlation cookie issuance

Codeberg noreply at shibboleth.net
Fri Apr 10 16:37:30 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-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/0b8003ee0ae0cfe5608644718574c3ff26296bfd

The following commit(s) were added to refs/heads/main by this push:
     new 0b8003e  JSHIBDOIDC-5 - Add correlation cookie issuance
0b8003e is described below

commit 0b8003ee0ae0cfe5608644718574c3ff26296bfd
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Apr 10 17:13:37 2026 +0100

    JSHIBDOIDC-5 - Add correlation cookie issuance
    
     - Seal and base64URL-encode OAuth state using DataSealer to prevent
    tampering
     - Preserve authentication and authorization context via
    StateTokenManager as sealed JSON. Previously this was done via the
    cookie manager
     - Change name of CSRF token 'nonce' to request forgery protection rfp.
     - Add preserved authnetication and authorization context state token to
    correleation cookie alongside rfp value, for later recovery of state value
     - Decode and unseal OAuth state on response (via a decoder strategy);
    recover resource URL, correlation cookie, and authn context
     - Improve recovery of cookies and values, set into appropriate contexts
     - Needs more unit tests
    
    https://shibboleth.atlassian.net/browse/JSHIBDOIDC-5
---
 .../oidc/context/AuthnRequestStateDataContext.java |  33 ++-
 .../context/CorrelationCookieStateContext.java     |  54 ++++
 ...nStateFromCorrelationCookieLookupFunction.java} |  23 +-
 ...omAuthenticationRequestStateLookupFunction.java |  53 ----
 ...=> RFPFromCorrelationCookieLookupFunction.java} |  23 +-
 ...n.java => RFPFromOAuthStateLookupFunction.java} |   6 +-
 .../RequestCorrelationFromStateLookupFunction.java | 184 ++++++++++++
 ....java => StateFromJSONStateLookupFunction.java} |   2 +-
 .../shibboleth/sp/oidc/profile/OIDCConstants.java  |  12 +-
 .../sp/consumer/oidc/code/query/query-beans.xml    |   4 +-
 .../idp/flows/sp/consumer/oidc/oidc-beans.xml      | 120 +++++---
 .../idp/flows/sp/consumer/oidc/oidc-flow.xml       |   8 +-
 .../idp/flows/sp/initiator/oidc/oidc-beans.xml     | 323 +++++++++++----------
 .../idp/flows/sp/initiator/oidc/oidc-flow.xml      |  10 +-
 .../shibboleth/idp/flows/sp/oidc-common-beans.xml  |  51 ++++
 .../oidc/flows/OIDCSessionInitiatorFlowTest.java   |   7 +-
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   | 167 +++++++----
 .../shibboleth/sp/oidc/flows/TestConstants.java    | 126 +++++---
 ...DCEnvironmentApplicationContextInitializer.java |   4 +
 .../impl/DecodeStateAsJsonObjectConsumer.java      | 107 ++++++-
 .../sp/oidc/profile/impl/IssueStateCookie.java     |   7 +-
 .../profile/impl/MapStateTokenToStateValue.java    | 167 +++++++++++
 ...teToken.java => MapStateValueToStateToken.java} |  98 +++----
 .../sp/oidc/profile/impl/OIDCSupport.java          |  26 +-
 ...essStateCookie.java => ResolveStateCookie.java} |  24 +-
 ...AuthenticationRequestToPeerContextConsumer.java |  40 +--
 .../impl/SetAuthenticationStateTokenConsumer.java  |  50 ++++
 ...SetCorrelationCookieValueToContextConsumer.java | 184 ++++++++++++
 .../impl/SetNonceValueToTokenContextConsumer.java  |  53 ----
 .../sp/oidc/profile/impl/StateLookupStrategy.java  |  89 +++++-
 .../oidc/profile/impl/ValidateResponseState.java   |  68 +++--
 .../sp/oidc/profile/impl/IssueStateCookieTest.java |  18 +-
 ...CookieTest.java => ResolveStateCookieTest.java} |   8 +-
 ...enticationRequestToPeerContextConsumerTest.java |  12 +-
 34 files changed, 1548 insertions(+), 613 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java
index 4bdaf58..99fd52b 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java
@@ -21,19 +21,42 @@ import org.opensaml.messaging.context.BaseContext;
 import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
 
 /**
- * A context to hold recovered state information about the original authentication request.
+ * A context to hold the token used to recover state information about the original authentication request, alongside
+ * any recovered information. 
+ * 
+ * <p>On initiation requests, it is likely only the token will be populated. In consumer flows, it is likely both the
+ * token and the recovered state is populated.</p>
  */
 public class AuthnRequestStateDataContext extends BaseContext {
     
+    /** The token used as a key to the stored authentication request state.*/
+    @Nullable private String token;
+    
     /** The authentication state data. */
     @Nullable private AuthenticationRequestStateData authnState;
     
     /**
-     * Sets the authentication state recovered from the authentication request.
+     * Set the token used as a key to the stored authentication request state.
+     * 
+     * @param tokenIn the token to set.
+     */
+    @Nonnull public AuthnRequestStateDataContext setToken(@Nullable final String tokenIn) {
+        token = tokenIn;
+        return this;
+    }
+    /**
+     * Return the token used as a key to the stored authentication request state.
      * 
-     * @param state authnentication state to set
+     * @return the token.
+     */
+    @Nullable public String getToken() {
+        return token;
+    }
+    
+    /**
+     * Sets the authentication state recovered from the authentication request.
      * 
-     * @return this context 
+     * @param state The authnState to set
      */
     @Nonnull public AuthnRequestStateDataContext setAuthnState(@Nullable final AuthenticationRequestStateData state) {      
         authnState = state;
@@ -49,4 +72,4 @@ public class AuthnRequestStateDataContext extends BaseContext {
         return authnState;
     }
 
-}
+}
\ No newline at end of file
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java
new file mode 100644
index 0000000..a206e07
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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.oidc.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.minidev.json.JSONObject;
+
+/**
+ * A context to hold the browser bound correlation cookie referenced from the OAuth state. The cookie is assumed to hold
+ * a JSON object.
+ */
+public class CorrelationCookieStateContext extends BaseContext {
+    
+    /** The correlation cookie value as a JSON object.*/
+    @Nullable private JSONObject value;
+    
+    /**
+     * Set the correlation cookie value JSON.
+     * 
+     * @param cookieValue the correlation cookie value JSON.
+     * 
+     * @return this context
+     */
+    @Nonnull public CorrelationCookieStateContext setValue(@Nullable final JSONObject cookieValue) {
+        value = cookieValue;
+        return this;
+    }
+    
+    /**
+     * Get the cookie value as a JSON object.
+     * 
+     * @return the cookie value as a JSON object, or null if not set.
+     */
+    @Nullable public JSONObject getCorrelationCookieValue() {
+        return value;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
similarity index 57%
copy from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
copy to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
index 7c87ce8..115d708 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
@@ -20,29 +20,34 @@ import javax.annotation.Nullable;
 
 import net.minidev.json.JSONObject;
 import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.oidc.context.OAuthStateContext;
+import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
 import net.shibboleth.sp.oidc.profile.OIDCConstants;
 
 /**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A function that extracts the authentication state token from the correlation cookie, if present, and returns it.
  */
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class AuthenticationStateFromCorrelationCookieLookupFunction 
+    implements Function<CorrelationCookieStateContext,String> {
 
     /** {@inheritDoc} */
-    @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
+    @Nullable public String apply(@Nullable final CorrelationCookieStateContext stateContext) {
         if (stateContext == null) {
             return null;
         }
-        final JSONObject stateJson = stateContext.getStateJson();
-        if (stateJson == null) {
+        final JSONObject correlationCookie = stateContext.getCorrelationCookieValue();
+        if (correlationCookie == null) {
             return null;
         }
-        final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
-        if (StringSupport.trimOrNull(nonceFromState) != null) {
-            return nonceFromState;
+        final String authnStateToken = correlationCookie.getAsString(OIDCConstants.AUTHN_STATE_FIELD);
+        if (StringSupport.trimOrNull(authnStateToken) != null) {
+            return authnStateToken;
         }
         return null;
         
     }
+    
+    
 
 }
+
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java
deleted file mode 100644
index 97f345c..0000000
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java
+++ /dev/null
@@ -1,53 +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.oidc.messaging.navigate;
-
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import net.minidev.json.JSONObject;
-import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.profile.OIDCConstants;
-
-/**
- *  A {@link Function} that returns a nonce found from the state if the state was encoded as a JSON object.
- */
-public class NonceFromAuthenticationRequestStateLookupFunction implements Function<OIDCAuthenticationRequest,String> {
-    
-    /** Class logger. */
-    @Nonnull
-    private final Logger log = LoggerFactory.getLogger(NonceFromAuthenticationRequestStateLookupFunction.class);
-
-    /** {@inheritDoc} */
-    @Nullable public String apply(@Nullable final OIDCAuthenticationRequest request) {
-        if (request != null) {
-            final JSONObject stateJson = request.getStateJson();
-            if (stateJson != null) {
-                final Object nonceFromOAuthStateAsObject = stateJson.get(OIDCConstants.NONCE_FIELD);
-                if (nonceFromOAuthStateAsObject instanceof final String nonce) {
-                    return nonce;
-                }
-            }
-        }
-        return null;
-    }
-
-}
-
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
similarity index 58%
copy from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
copy to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
index 7c87ce8..6dde325 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
@@ -20,29 +20,34 @@ import javax.annotation.Nullable;
 
 import net.minidev.json.JSONObject;
 import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.oidc.context.OAuthStateContext;
+import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
 import net.shibboleth.sp.oidc.profile.OIDCConstants;
 
 /**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A function that extracts the Request Forgery Protection nonce from the correlation cookie, if present, and 
+ * returns it.
  */
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class RFPFromCorrelationCookieLookupFunction implements Function<CorrelationCookieStateContext,String> {
 
     /** {@inheritDoc} */
-    @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
+    @Nullable public String apply(@Nullable final CorrelationCookieStateContext stateContext) {
         if (stateContext == null) {
             return null;
         }
-        final JSONObject stateJson = stateContext.getStateJson();
-        if (stateJson == null) {
+        final JSONObject correlationCookie = stateContext.getCorrelationCookieValue();
+        if (correlationCookie == null) {
             return null;
         }
-        final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
-        if (StringSupport.trimOrNull(nonceFromState) != null) {
-            return nonceFromState;
+        final String rfpToken = correlationCookie.getAsString(OIDCConstants.RFP_FIELD);
+        if (StringSupport.trimOrNull(rfpToken) != null) {
+            return rfpToken;
         }
         return null;
         
     }
+    
+    
 
 }
+
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
similarity index 86%
rename from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
rename to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
index 7c87ce8..2f22c2a 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
@@ -24,9 +24,9 @@ import net.shibboleth.sp.oidc.context.OAuthStateContext;
 import net.shibboleth.sp.oidc.profile.OIDCConstants;
 
 /**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A lookup function that extracts the request forgery protection value from a Base64URL encoded JSON object.
  */
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class RFPFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
 
     /** {@inheritDoc} */
     @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
@@ -37,7 +37,7 @@ public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateCon
         if (stateJson == null) {
             return null;
         }
-        final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
+        final String nonceFromState = stateJson.getAsString(OIDCConstants.RFP_FIELD);
         if (StringSupport.trimOrNull(nonceFromState) != null) {
             return nonceFromState;
         }
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java
new file mode 100644
index 0000000..8187070
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java
@@ -0,0 +1,184 @@
+/*
+ * 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.oidc.messaging.navigate;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.util.StandardCharset;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.OIDCConstants;
+
+/**
+ *  A {@link Function} that creates a request correlation cookie value with the token/key of the stored authentication 
+ *  request state, and the request forgery protection (RFP) value that was stored in the OAuth state parameter. 
+ *  The resulting cookie value is Base64URL encoded JSON string (noting the sealer will already base64 encode the
+ *  sealed String). 
+ */
+public class RequestCorrelationFromStateLookupFunction extends AbstractIdentifiableInitializableComponent 
+        implements Function<ProfileRequestContext,String> {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(RequestCorrelationFromStateLookupFunction.class);
+    
+    /** A strategy to locate the {@link AuthnRequestStateDataContext}.*/
+    @Nonnull 
+    private final Function<ProfileRequestContext, AuthnRequestStateDataContext> authnRequestDataStateLookupStrategy;
+    
+    /** Strategy to locate the authentication request. */
+    @Nonnull 
+    private final Function<ProfileRequestContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** 
+     * If the dataSealer is provided should it be used to seal the authentication request state? Defaults to true, 
+     * that is, if the dataSealer is provided, always seal state.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> sealState;
+    
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param dataLookuptrategy a strategy to locate the {@link AuthnRequestStateDataContext}
+     * @param authnRequestStrategy a strategy to locate the {@link OIDCAuthenticationRequest}
+     */
+    public RequestCorrelationFromStateLookupFunction(@ParameterName(name="authenticationRequestDataStateLookupStrategy")
+            @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> dataLookuptrategy,
+                @ParameterName(name="authenticationRequestLookupStrategy")
+                    @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationRequest> authnRequestStrategy) {
+        
+        authnRequestDataStateLookupStrategy = 
+                Constraint.isNotNull(dataLookuptrategy, "AuthnRequestStateDataContext lookup strategy cannot be null");
+        authenticationRequestLookupStrategy = 
+                Constraint.isNotNull(authnRequestStrategy, "OIDCAuthenticationRequest lookup strategy cannot be null");
+        sealState = PredicateSupport.alwaysTrue();
+    }
+    
+    
+    /**
+     * Sets {@link DataSealer} to use.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        checkSetterPreconditions();
+        
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set the predicate to determine whether to seal the state.
+     * 
+     * @param predicate the seal state predicate to set.
+     */
+    public void setSealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        
+        sealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+    }
+    
+    /**
+     * Set the flag to determine whether to seal the state.
+     * 
+     * @param flag the flag to set.
+     */
+    public void setSealState(final boolean flag) {
+        checkSetterPreconditions();
+        
+        sealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext prc) {
+        final AuthnRequestStateDataContext stateContext = authnRequestDataStateLookupStrategy.apply(prc);
+        
+        if (stateContext == null || stateContext.getToken() == null) {
+            log.error("No authentication request state found, can not create correlation cookie");
+            return null;
+        }
+        
+        final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(prc);
+        
+        String csrfNonce = null;
+        if (authnRequest != null) {
+            final JSONObject stateJson = authnRequest.getStateJson();
+            if (stateJson != null) {
+                final Object nonceFromOAuthStateAsObject = stateJson.get(OIDCConstants.RFP_FIELD);
+                if (nonceFromOAuthStateAsObject instanceof final String nonce) {
+                    csrfNonce = nonce;
+                }
+            }
+        }
+        if (csrfNonce == null) {
+            log.error("Unable to generate request correlation cookie, no request forgery protection value found");
+            return null;
+        }
+        
+        final JSONObject stateObject = new JSONObject();
+        stateObject.appendField(OIDCConstants.AUTHN_STATE_FIELD, stateContext.getToken());
+        stateObject.appendField(OIDCConstants.RFP_FIELD, csrfNonce);
+        final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+        if (stateJsonString == null || stateJsonString.isEmpty()) {
+            return null;
+        }
+        try {
+            final DataSealer localDataSealer = dataSealer;
+            if (localDataSealer != null && sealState.test(prc)) {
+                log.debug("{}: Request correlation cookie is sealed", getId());
+                final String sealed = localDataSealer.wrap(stateJsonString);
+                final byte[] sealedAsBytes = sealed.getBytes(StandardCharset.UTF_8);
+                assert sealedAsBytes != null;
+                return Base64Support.encodeURLSafe(sealedAsBytes); 
+            } else {
+                log.warn("{}: Request correlation cookie was NOT sealed, either DataSealer is not configured or "
+                        + "sealing predicate returned false. Sealing should be enabled in production", getId());
+                final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+                assert stateJsonAsBytes != null;
+                return Base64Support.encodeURLSafe(stateJsonAsBytes);
+            }
+            
+        } catch (final EncodingException | DataSealerException e) {
+            log.error("Unable to generate request correlation cookie", e);
+            return null;
+        }
+
+    }
+
+}
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
similarity index 94%
rename from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java
rename to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
index f32b992..b79528e 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
@@ -27,7 +27,7 @@ import net.shibboleth.sp.oidc.profile.OIDCConstants;
  * Lookup strategy that extracts the 'state' parameter from the OAuth 2.0 state JSON Object in the state context. This
  * state is used to carry a reference to the resource URL. 
  */
-public class ResourceStateFromJSONStateLookupFunction implements Function<OAuthStateContext,String> {
+public class StateFromJSONStateLookupFunction implements Function<OAuthStateContext,String> {
 
     /** {@inheritDoc} */
     @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
index 934a69b..20dc9c9 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
@@ -28,10 +28,16 @@ public final class OIDCConstants {
 
     }    
     
-    /** Name of the state field to add to the state JSON object.*/
+    /** 
+     * Name of the state field to add to the state JSON object. The state field represents the current 'state'
+     * value of the DDF.
+     */
     @Nonnull @NotEmpty public static final String STATE_FIELD = "state";
     
-    /** Name of the nonce field to add to the state JSON object.*/
-    @Nonnull @NotEmpty public static final String NONCE_FIELD = "nonce";
+    /** Name of the authentication state token field that references the authentication state in the token manager.*/
+    @Nonnull @NotEmpty public static final String AUTHN_STATE_FIELD = "authnState";
+    
+    /** Name of the Request Forgery Protection (nonce) field.*/
+    @Nonnull @NotEmpty public static final String RFP_FIELD = "rfp";
 
 }
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
index 3a1ba1d..3983c6d 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
@@ -15,7 +15,9 @@
                 p:postDecodeStrategyFailureIsError="true"                
                 p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier">
                 <property name="postDecodeStrategy">
-                    <bean class="net.shibboleth.sp.oidc.profile.decoding.impl.DecodeStateAsJsonObjectConsumer"/>
+                    <bean class="net.shibboleth.sp.oidc.profile.decoding.impl.DecodeStateAsJsonObjectConsumer"
+                        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+                        p:unsealStatePredicate="#{getObject('%{sp.oidc.sealOAuthStatePredicate:}') ?: ((%{sp.oidc.sealOAuthState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
                 </property>
             </bean>
         </constructor-arg>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index fb8a000..3b89cb7 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -16,20 +16,43 @@
 
 
     <!--      
-        The State parameter can carry target URL like SAML relayState, but it must also be used to prevent CSRF in OAuth.
-        So we can attempt to map it from the state parameter. 
+        Map the state reference from the OAuth state parameter to the stored off resource URL. 
     -->
     <bean id="MapStateTokenToResource"
         class="net.shibboleth.sp.profile.impl.MapStateTokenToResource" scope="prototype"
         p:stateTokenLookupStrategy-ref="StateFromStateLookup" 
         p:createOutputObjects="true"/>
-
-    <!-- Retrieve the state used to map to cookie values, taken from the JSON object contained in the OAuth 2.0 state parameter -->
+        
+    <!-- 
+        Retrieve the correlation cookie from the state token (reference) in the OAuth state parameter. The cookie is used 
+        to protect against request forgery, relate response to request, and contains the authn state token used to recover 
+        authentication state.
+        
+        Store the cookie value into the context for later inspection.
+     -->
+    <bean id="ProcessCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ResolveStateCookie" scope="prototype"
+        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
+        p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-shibsp_req_}"
+        p:createOutputObjects="true"
+        p:errorFatal="true"
+        p:stateTokenLookupStrategy-ref="StateFromStateLookup">
+        <property name="cookieValueConsumerStrategy">
+            <bean class="net.shibboleth.sp.oidc.profile.impl.SetCorrelationCookieValueToContextConsumer"
+                p:correlationCookieStateContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContextFromInboundOAuthStateContext"
+                p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+                p:unsealStatePredicate="#{getObject('%{sp.oidc.sealCorrelationCookiePredicate:}') ?: ((%{sp.oidc.sealCorrelationCookie:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
+        </property>
+    </bean>
+    
+    <!-- 
+        Retrieve the state token from the the OAuth 2.0 state parameter which is used to map to the correlation cookie 
+        and the resource URL. The state parameter value will be decoded into the OAuth state context by this point.
+    -->
     <bean id="StateFromStateLookup" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
             <bean parent="shibboleth.Functions.Compose">
                 <constructor-arg name="g">
-                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.ResourceStateFromJSONStateLookupFunction"/>
+                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.StateFromJSONStateLookupFunction"/>
                 </constructor-arg>
                 <constructor-arg name="f">     
                     <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -40,26 +63,13 @@
         <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
     </bean>
     
-    <!-- Correlation cookie is used to  -->
-    <bean id="ProcessNonceCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessStateCookie" scope="prototype"
-        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-shibsp_req_}"
-        p:createOutputObjects="true"
-        p:errorFatal="true"
-        p:stateTokenLookupStrategy-ref="StateFromStateLookup">
-        <property name="cookieValueConsumerStrategy">
-			<bean class="net.shibboleth.sp.oidc.profile.impl.SetNonceValueToTokenContextConsumer"/>
-		</property>
-    </bean>
-    
-    <!-- TODO should this be a cookie or go through the state manager-->
-    <bean id="ProcessAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessStateCookie" scope="prototype"
-        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="%{sp.authnstate.cookiePrefix:__Host-shibsp_authnstate_}"
-        p:createOutputObjects="true"
+    <!--  
+        Retrieve the stored authentication state/value from the storage manager using the authn state token in the correlation cookie     
+    -->
+    <bean id="RecoverAuthenticationState" class="net.shibboleth.sp.oidc.profile.impl.MapStateTokenToStateValue" scope="prototype"
         p:errorFatal="true"
-        p:stateTokenLookupStrategy-ref="StateFromStateLookup">
-        <property name="cookieValueConsumerStrategy">
+        p:stateTokenLookupStrategy-ref="AuthenticationStateFromCorrelationCookieLookup">
+        <property name="stateValueConsumer">
             <bean class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationRequestToPeerContextConsumer"
                 p:objectMapper-ref="shibboleth.JSONObjectMapper"
                 p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
@@ -68,28 +78,45 @@
         </property>
     </bean>
     
+    <bean id="AuthenticationStateFromCorrelationCookieLookup" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.AuthenticationStateFromCorrelationCookieLookupFunction"/>
+                </constructor-arg>
+                <constructor-arg name="f" ref="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext"/>     
+            </bean>
+        </constructor-arg>
+        <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
+    </bean>
+
     <bean id="ValidateAuthenticationResponseResult" scope="prototype"
         class="net.shibboleth.sp.oidc.profile.impl.ValidateAuthenticationResponseResult" />
         
     <bean id="ValidateResponseStateMatchesRequest" scope="prototype"
         class="net.shibboleth.sp.oidc.profile.impl.ValidateResponseState"
-        p:nonceTokenLookupStrategy-ref="NonceFromStateLookup" />
-        
-     <bean id="InitializeRelyingPartyContextFromOIDCPeer"
-        class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
+        p:rfpTokenFromOAuthStateLookupStrategy-ref="RFPFromOAuthStateLookup" 
+        p:rfpFromCookieLookupStrategy-ref="RFPFromCorrelationCookieLookup"/>
         
-    <bean id="SelectRelyingPartyConfiguration"
-        class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
+   <!-- Get the Request Forgery Protection value from the correlation cookie  -->
+   <bean id="RFPFromCorrelationCookieLookup" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.RFPFromCorrelationCookieLookupFunction"/>
+                </constructor-arg>
+                <constructor-arg name="f" ref="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext"/>     
+            </bean>
+        </constructor-arg>
+        <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
+    </bean>
         
-    <bean id="SelectProfileConfiguration"
-        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
-        p:profileId-ref="shibboleth.sp.oidc.ProfileId" />        
-
-    <bean id="NonceFromStateLookup" parent="shibboleth.Functions.Compose">
+    <!-- Get the Request Forgery Protection value from the OAuth 2.0 state parameter -->
+    <bean id="RFPFromOAuthStateLookup" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
             <bean parent="shibboleth.Functions.Compose">
                 <constructor-arg name="g">
-                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.NonceFromOAuthStateLookupFunction"/>
+                    <bean class="net.shibboleth.sp.oidc.messaging.navigate.RFPFromOAuthStateLookupFunction"/>
                 </constructor-arg>
                 <constructor-arg name="f">     
                     <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -99,8 +126,18 @@
         </constructor-arg>
         <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
     </bean>
+        
+     <bean id="InitializeRelyingPartyContextFromOIDCPeer"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
+        
+    <bean id="SelectRelyingPartyConfiguration"
+        class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
+        
+    <bean id="SelectProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:profileId-ref="shibboleth.sp.oidc.ProfileId" />        
     
-    <!-- Build the request based on the inbound context -->
+    <!-- Build the Token endpoint client authentication method based on the inbound context -->
     <bean id="InitializeOAuth2ClientAuthenticationContextHandler" parent="WebFlowInboundMessageHandlerAdaptor"
         scope="prototype">
          <constructor-arg>
@@ -194,7 +231,6 @@
         </property>
     </bean>
     
-     <!-- ID TOKEN Signature Validation -->
     <bean id="IDTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
         <constructor-arg>
             <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -409,7 +445,7 @@
         class="net.shibboleth.sp.oidc.profile.impl.RequestedACRValidationActivationCondition"
         c:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty"/>
     
-    <!-- - End ID Token Claims Validation -->
+    <!-- End ID Token Claims Validation -->
     
     <bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
     
@@ -468,9 +504,9 @@
     </bean>
     
     <!-- 
-    Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
-    the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
-    have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
+        Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
+        the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
+        have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
     -->
     <bean id="UserInfoTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
         <constructor-arg>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
index defda79..024b1d0 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
@@ -6,11 +6,11 @@
     <action-state id="DoProfileWork">
 <!--        <evaluate expression="PopulateMetricContext" />-->
        <evaluate expression="DecodeMessage" />
+       <evaluate expression="ValidateAuthenticationResponseResult" /> <!-- TODO: If this fails the state below is not cleared! -->
        <evaluate expression="MapStateTokenToResource" />
-       <evaluate expression="ProcessNonceCorrelationCookie" />
-       <evaluate expression="ProcessAuthnStateCookie" />
-       <evaluate expression="ValidateResponseStateMatchesRequest" />
-       <evaluate expression="ValidateAuthenticationResponseResult" />
+       <evaluate expression="ProcessCorrelationCookie"/>       
+       <evaluate expression="RecoverAuthenticationState" />
+       <evaluate expression="ValidateResponseStateMatchesRequest" />       
        
        <evaluate expression="ProviderMetadataLookup" />        
        <evaluate expression="InitializeRelyingPartyContextFromOIDCPeer" />
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
index b9aa165..58d70d2 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
@@ -6,56 +6,54 @@
                            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">
-    
+
     <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml" />
-        
+
     <!-- TODO Check we need the thread-local variants of these in the chains we are using them ^^ -->
-    
+
     <util:constant id="shibboleth.sp.oidc.ProfileId"
         static-field="net.shibboleth.oidc.profile.config.OIDCSSOProfileConfiguration.PROFILE_ID" />
     <!-- end -->
-    
-    
+
+
     <bean id="ValidateSessionInitiatorRequest"
-        class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest" scope="prototype"
+        class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest" 
+        scope="prototype" 
         p:flowId="oidc"
-        p:requireDiscoveryURL="false"
+        p:requireDiscoveryURL="false" 
         p:requireRelyingPartyId="true" />
-        
-    <!-- Prepare the OIDC Peer Entity with the relying party ID (the OP identifier)-->
-    <bean id="PrepareInboundMessageContext" 
+
+    <!-- Prepare the OIDC Peer Entity with the relying party ID (the OP identifier) -->
+    <bean id="PrepareInboundMessageContext"
         class="net.shibboleth.sp.oidc.profile.impl.PrepareOIDCInboundMessageContext" scope="prototype"
         p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
     </bean>
-    
+
     <bean id="InitializeRelyingPartyContextFromOIDCPeer"
         class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
-        
+
     <bean id="SelectRelyingPartyConfiguration"
         class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
-        
-    <bean id="SelectProfileConfiguration"
-        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
-        p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
-        
-<!--     <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
-        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" /> -->
-        
-   <!-- TODO, self context -->
-   <bean id="InitializeOutboundMessageContext"
-        class="net.shibboleth.sp.oidc.profile.impl.InitializeOutboundMessageContext"
-        scope="prototype" />
+
+    <bean id="SelectProfileConfiguration" class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration"
+        scope="prototype" p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
+
+    <!-- <bean id="InitializeOutboundMessageContext" class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" 
+        scope="prototype" p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" /> -->
+
+    <!-- TODO, self context -->
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeOutboundMessageContext" scope="prototype" />
 
     <bean id="InitializeOAuth2ClientContext" scope="prototype"
         class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"
-        p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple"/>
-        
+        p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple" />
+
     <bean id="InitializeAuthorizationRequest"
-        class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype"/>
-        
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype" />
+
     <!-- Construct a suitable outbound authentication request -->
-    
+
     <bean id="BuildAuthenticationRequest" parent="WebFlowOutboundMessageHandlerAdaptor" scope="prototype">
         <constructor-arg>
             <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -65,98 +63,115 @@
                         <bean id="AddResponseType" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResponseTypeHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseTypeLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseTypeLookupStrategy"
+                                    scope="prototype" />
                             </property>
-                        </bean> 
+                        </bean>
                         <bean id="AddResponseMode" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResponseModeHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseModeLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseModeLookupStrategy"
+                                    scope="prototype" />
                             </property>
-                        </bean>    
-                            
+                        </bean>
+
                         <bean id="AddMaxAge" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddMaxAgeHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.MaxAgeLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.MaxAgeLookupStrategy"
+                                    scope="prototype" />
                             </property>
-                        </bean> 
+                        </bean>
                         <bean id="AddDisplay" scope="prototype"
-                            class="net.shibboleth.oidc.profile.messaging.handler.impl.AddDisplayHandler" >
+                            class="net.shibboleth.oidc.profile.messaging.handler.impl.AddDisplayHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.DisplayParameterLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.DisplayParameterLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddScopes" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddScopesHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.ScopeLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ScopeLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddNonce" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddNonceHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.NonceLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.NonceLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddEndpointURI" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddEndpointURIHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.AuthorizationEndpointLookupStrategy" scope="prototype"/>
+                                <bean
+                                    class="net.shibboleth.sp.oidc.profile.impl.AuthorizationEndpointLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddLoginHintHandler" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddLoginHintHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.LoginHintLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.LoginHintLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddRequestedClaims" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddRequestedClaimsHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.RequestedClaimsLookupStrategy" scope="prototype"/>
-                            </property>                            
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.RequestedClaimsLookupStrategy"
+                                    scope="prototype" />
+                            </property>
                         </bean>
                         <bean id="AddPKCECodeVerifierAndChallenge" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddPKCECodeVerifierAndChallenge">
-                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.PKCEOptionsLookupStrategy" scope="prototype"/>
+                            <property name="parameterValueLookupStrategy">
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.PKCEOptionsLookupStrategy"
+                                    scope="prototype" />
                             </property>
-                        </bean>                                                  
+                        </bean>
                         <bean id="AddRedirectURI" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddRedirectURIHandler">
                             <property name="parameterValueLookupStrategy">
-                                 <bean class="net.shibboleth.sp.oidc.profile.impl.RedirectUriLookupStrategy" scope="prototype" />   
-                            </property>                         
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.RedirectUriLookupStrategy"
+                                    scope="prototype" />
+                            </property>
                         </bean>
                         <bean id="AddAuthenticationContextClassReferences" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddAuthenticationContextClassReferencesHandler">
                             <property name="parameterValueLookupStrategy">
-                                 <bean class="net.shibboleth.sp.oidc.profile.impl.AuthenticationContextClassRefLookupStrategy" scope="prototype" />   
-                            </property> 
+                                <bean
+                                    class="net.shibboleth.sp.oidc.profile.impl.AuthenticationContextClassRefLookupStrategy"
+                                    scope="prototype" />
+                            </property>
                         </bean>
                         <bean id="AddForceAuthentication" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddForceAuthenticationHandler">
                             <property name="ParameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.ForceAuthnParameterLookupStrategy"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ForceAuthnParameterLookupStrategy" />
                             </property>
                         </bean>
                         <bean id="AddPrompt" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddPromptHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.PromptLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.PromptLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddResourceIndicators" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResourceHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResourceLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.ResourceLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                         <bean id="AddUiLocales" scope="prototype"
                             class="net.shibboleth.oidc.profile.messaging.handler.impl.AddUiLocalesHandler">
                             <property name="parameterValueLookupStrategy">
-                                <bean class="net.shibboleth.sp.oidc.profile.impl.UiLocalesLookupStrategy" scope="prototype"/>
+                                <bean class="net.shibboleth.sp.oidc.profile.impl.UiLocalesLookupStrategy"
+                                    scope="prototype" />
                             </property>
                         </bean>
                     </list>
@@ -166,40 +181,40 @@
         <property name="errorEvent">
             <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
         </property>
-    </bean>    
-        
+    </bean>
+
     <bean id="RequestObjectRequiredAndSupportedPredicate" scope="prototype"
-        class="net.shibboleth.sp.oidc.config.logic.RequestObjectRequiredAndSupported" />    
-    
+        class="net.shibboleth.sp.oidc.config.logic.RequestObjectRequiredAndSupported" />
+
     <bean id="PopulateRequestObjectSignatureSigningParameters" scope="prototype"
         class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
         c:strategy-ref="shibboleth.MessageContextLookup.Inbound" p:noResultIsError="true"
         p:configurationLookupStrategy-ref="RequestObjectSignatureSigningConfigurationLookup"
         p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
         p:signatureSigningParametersResolver-ref="RequestObjectSignatureSigningParametersResolver">
-            <property name="activationCondition">
-                <bean id="SignRequestObjectProxyCondition"
-                    class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
-                    p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
-            </property>
+        <property name="activationCondition">
+            <bean id="SignRequestObjectProxyCondition"
+                class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
+                p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
     </bean>
-    
+
     <bean id="RequestObjectSignatureSigningParametersResolver" scope="prototype"
         class="net.shibboleth.oidc.security.jose.impl.RelyingPartySigningParametersResolver"
         p:providerMetadataAlgorithmLookupStrategy-ref="RequestObjectSupportedSignatureSigningAlgorithms" />
-        
+
     <bean id="RequestObjectSupportedSignatureSigningAlgorithms" scope="prototype"
         class="net.shibboleth.sp.oidc.metadata.impl.RequestObjectSupportedSignatureSigningAlgorithms" />
-    
-    
+
+
     <bean id="RequestObjectSignatureSigningConfigurationLookup" lazy-init="true" scope="prototype"
         class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
-        
-    
+
+
     <!-- if the activation condition succeeds, encryption is not optional -->
     <bean id="PopulateRequestObjectEncryptionParameters"
         class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters" scope="prototype"
-        p:encryptionOptional="false"
+        p:encryptionOptional="false" 
         p:forFriendlyName="Request Object"
         p:configurationLookupStrategy-ref="RequestObjectEncryptionConfigurationLookup"
         p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
@@ -211,7 +226,7 @@
         </property>
     
     </bean>
-    
+
     <bean id="EncryptionParametersResolver" scope="prototype"
         class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
         <property name="keyTransportEncryptionAlgorithmsLookupStrategy">
@@ -219,46 +234,67 @@
                 class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataKeyTransportEncryptionAlgorithmsLookupStrategy">
                 <constructor-arg>
                     <bean
-                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction" 
-                        c:keyName="request_object_encryption_alg_values_supported"/>
+                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
+                        c:keyName="request_object_encryption_alg_values_supported" />
                 </constructor-arg>
             </bean>
         </property>
         <property name="dataEncryptionAlgorithmsLookupStrategy">
-            <bean class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataDataEncryptionAlgorithmsLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataDataEncryptionAlgorithmsLookupStrategy">
                 <constructor-arg>
                     <bean
-                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction" 
-                        c:keyName="request_object_encryption_enc_values_supported"/>
+                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
+                        c:keyName="request_object_encryption_enc_values_supported" />
                 </constructor-arg>
             </bean>
         </property>
     </bean>
-    
+
     <bean id="RequestObjectEncryptionConfigurationLookup" lazy-init="true" scope="prototype"
         class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
-    
-    <bean id="BuildRequestObject" class="net.shibboleth.sp.oidc.profile.impl.BuildRequestObject"
-        scope="prototype"
+
+    <bean id="BuildRequestObject" class="net.shibboleth.sp.oidc.profile.impl.BuildRequestObject" scope="prototype"
         p:claimsSetIsValidPredicate="#{getObject('shibboleth.sp.oidc.RequestObjectClaimsSetIsValidPredicate')}"
-        p:requestObjectToBeSignedPredicate-ref="SignRequestObjectCondition" 
-        p:customClaimsStrategy="#{getObject('shibboleth.sp.oidc.CustomRequestObjectClaimsStrategy')}"/>
-        
+        p:requestObjectToBeSignedPredicate-ref="SignRequestObjectCondition"
+        p:customClaimsStrategy="#{getObject('shibboleth.sp.oidc.CustomRequestObjectClaimsStrategy')}" />
+
     <bean id="SignRequestObjectCondition" scope="prototype"
         class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
         p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
-        
-        
-    <bean id="HandleOutboundMessage"
-            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
-            c:messageHandler-ref="PreEncodeMessageHandler"
-            c:executionDirection="OUTBOUND">
+
+   <!-- 
+        Store off required authentication state via the storage token manager. The token returned from the storage manager
+        is referenced in the correlation cookie for later recovery.
+   -->
+    <bean id="StoreAuthenticationState"
+        class="net.shibboleth.sp.oidc.profile.impl.MapStateValueToStateToken" scope="prototype"
+        p:createOutputObjects="true" 
+        p:errorFatal="%{sp.stateToken.errorsFatal:true}"
+        p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy"
+        p:stateTokenConsumer-ref="AuthenticationStateTokenConsumer" />
+
+    <!-- A consumer that stores off the authentication state token (key) -->
+    <bean id="AuthenticationStateTokenConsumer"
+        class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationStateTokenConsumer" />
+
+    <!-- A strategy for encoding and sealing any state from the authentication request needed for validating the response -->
+    <bean id="AuthenticationRequestStateForStorageStrategy"
+        class="net.shibboleth.sp.oidc.profile.impl.AuthenticationRequestStateForStorageStrategy"
+        p:authenticationAuthorityLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
+        p:authenticationRequestLookupStrategy-ref="shibboleth.AuthenticationRequestLookup.FromOutbound"
+        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+        p:sealStatePredicate="#{getObject('%{sp.oidc.sealAuthenticationStatePredicate:}') ?: ((%{sp.oidc.sealAuthenticationState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"
+        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}" />
+
+
+    <bean id="HandleOutboundMessage" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+        scope="prototype" c:messageHandler-ref="PreEncodeMessageHandler" c:executionDirection="OUTBOUND">
         <property name="errorEvent">
             <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
         </property>
     </bean>
-    
-    <!-- TODO Might not need all of these in the preencode step if we can add the state before...but signing and encrypting the RO still might work here -->
+
     <bean id="PreEncodeMessageHandler" class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain"
         scope="prototype">
         <property name="handlers">
@@ -266,7 +302,9 @@
                 <bean id="AddState" class="net.shibboleth.oidc.profile.messaging.handler.impl.AddStateHandler"
                     scope="prototype">
                     <property name="parameterValueLookupStrategy">
-                        <bean class="net.shibboleth.sp.oidc.profile.impl.StateLookupStrategy" scope="prototype"/>
+                        <bean class="net.shibboleth.sp.oidc.profile.impl.StateLookupStrategy" scope="prototype"
+                            p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+                            p:sealStatePredicate="#{getObject('%{sp.oidc.sealOAuthStatePredicate:}') ?: ((%{sp.oidc.sealOAuthState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
                     </property>
                 </bean>
 
@@ -274,84 +312,60 @@
                     class="net.shibboleth.oidc.profile.messaging.handler.impl.BuildPlainRequestObjectJWT"
                     scope="prototype" />
 
-                 <bean id="SignRequestObject" class="net.shibboleth.oidc.security.impl.SignJWTHandler"
+                <bean id="SignRequestObject" class="net.shibboleth.oidc.security.impl.SignJWTHandler"
                     scope="prototype" p:logName="RequestObject">
                     <property name="claimsToSignLookupStrategy">
                         <bean
                             class="net.shibboleth.sp.oidc.context.navigate.JWTClaimsSetFromRequestObjectLookupFunction" />
                     </property>
                     <property name="jwtUpdateConsumer">
-                        <bean
-                            class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+                        <bean class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
                     </property>
-                </bean> 
+                </bean>
 
-                <bean id="EncryptRequestObject"
-                    class="net.shibboleth.oidc.security.impl.EncryptJWTHandler" scope="prototype"
-                    p:logName="RequestObject">
+                <bean id="EncryptRequestObject" class="net.shibboleth.oidc.security.impl.EncryptJWTHandler"
+                    scope="prototype" p:logName="RequestObject">
                     <property name="payloadToEncryptLookupStrategy">
                         <bean
                             class="net.shibboleth.sp.oidc.context.navigate.PayloadFromRequestObjectLookupFunction" />
                     </property>
                     <property name="jwtUpdateConsumer">
-                        <bean
-                            class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+                        <bean class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
                     </property>
                 </bean>
-                 <bean id="SetAuthenticationRequestTime"
-                    class="net.shibboleth.oidc.profile.messaging.handler.impl.SetAuthenticationRequestTimeHandler" scope="prototype"/>
+                <bean id="SetAuthenticationRequestTime"
+                    class="net.shibboleth.oidc.profile.messaging.handler.impl.SetAuthenticationRequestTimeHandler"
+                    scope="prototype" />
             </list>
         </property>
     </bean>
-    
-    <!-- TODO not having a state token to issue a correlation cookie has been set to fatal  -->
-    <bean id="IssueNonceCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" scope="prototype"
-        p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-shibsp_req_}'.trim()}"
-        p:createOutputObjects="true"
-        p:errorFatal="%{sp.stateToken.errorsFatal:true}" 
-        p:stateValueLookupStrategy-ref="NonceFromStateStrategy" />
-        
-     <bean id="NonceFromStateStrategy" parent="shibboleth.Functions.Compose">
-        <constructor-arg name="g">
-            <bean class="net.shibboleth.sp.oidc.messaging.navigate.NonceFromAuthenticationRequestStateLookupFunction" />
-        </constructor-arg>
-        <constructor-arg name="f">
-            <bean parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.MessageContextLookup.Outbound">
-                <constructor-arg name="g">
-                    <bean parent="shibboleth.Functions.Expression" 
-                        c:expression="#input.getMessage()"/>
-                </constructor-arg>
-            </bean>
-        </constructor-arg>
-    </bean>
-      
-   <!-- Issue a cookie with state information that is needed to be recovered to validate the response -->
-   <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" scope="prototype"
+
+    <!-- 
+        Issue the correlation cookie that relates request to response and stores of parameters needed to recover
+        authentication state when processing the response.
+    -->
+    <bean id="IssueRequestCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie"
+        scope="prototype" 
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.authnstate.cookiePrefix:__Host-shibsp_authnstate_}'.trim()}"
-        p:createOutputObjects="true"
-        p:errorFatal="%{sp.stateToken.errorsFatal:true}" 
-        p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
-        
-        <!--  TODO TRIAL (Ensure this is sealed)-->
-    <bean id="MapAuthenticationStateToStateToken" class="net.shibboleth.sp.oidc.profile.impl.MapStateToStateToken" scope="prototype"
+        p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-shibsp_req_}'.trim()}" 
         p:createOutputObjects="true"
         p:errorFatal="%{sp.stateToken.errorsFatal:true}" 
-        p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy"/> 
-        
-    <!-- A strategy for encoding and sealing any state from the authentication request needed for validating the response -->
-    <bean id="AuthenticationRequestStateForStorageStrategy" class="net.shibboleth.sp.oidc.profile.impl.AuthenticationRequestStateForStorageStrategy"
-        p:authenticationAuthorityLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
-        p:authenticationRequestLookupStrategy-ref="shibboleth.AuthenticationRequestLookup.FromOutbound"
-        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
-        p:sealStatePredicate="#{getObject('%{sp.oidc.sealAuthenticationStatePredicate:}') ?: ((%{sp.oidc.sealAuthenticationState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"
-        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}"/>
+        p:stateValueLookupStrategy-ref="RequestCorrelationCookieFromStateStrategy"/>
     
-    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
-        p:createOutputObjects="true"
-        p:messageEncoderFactory-ref="messageEncoderFactory" />
+    <!-- 
+        Create a request correlation cookie. The cookie contains an authentication token that maps back to stored 
+        authentication state alongside a request forgery protection nonce used to prevent CSRF attacks.
+    -->
+    <bean id="RequestCorrelationCookieFromStateStrategy" 
+        class="net.shibboleth.sp.oidc.messaging.navigate.RequestCorrelationFromStateLookupFunction"
+        c:authenticationRequestDataStateLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromOutbound"
+        c:authenticationRequestLookupStrategy-ref="shibboleth.AuthenticationRequestLookup.FromOutbound"
+        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+        p:sealStatePredicate="#{getObject('%{sp.oidc.sealCorrelationCookiePredicate:}') ?: ((%{sp.oidc.sealCorrelationCookie:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
         
+    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+        p:createOutputObjects="true" p:messageEncoderFactory-ref="messageEncoderFactory" />
+
     <!-- Message Encoder factory is a prototype to allow reuse of the encoders -->
     <bean id="messageEncoderFactory"
         class="net.shibboleth.oidc.profile.impl.AuthenticationRequestMessageEncoderFactory" scope="prototype"
@@ -370,11 +384,12 @@
 
     <bean id="HTTPPostAuthnRequestEncoder"
         class="net.shibboleth.oidc.profile.encoding.impl.HTTPPostAuthnRequestEncoder" init-method="" scope="prototype"
-        p:velocityEngine-ref="shibboleth.VelocityEngine" p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
-        p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}" 
+        p:velocityEngine-ref="shibboleth.VelocityEngine"
+        p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+        p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}"
         p:cSPDigester="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
-        p:cSPNonceGenerator="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}"/>
-    
-   
+        p:cSPNonceGenerator="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+
 
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
index 6629579..aeee613 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
@@ -45,11 +45,11 @@
         <transition to="ReselectFlow" />    
     </action-state>
     
-    <action-state id="BuildOutboundMessage">    
-        <evaluate expression="HandleOutboundMessage" />
-        <!-- <evaluate expression="MapAuthenticationStateToStateToken"/> TRIAL -->
-        <evaluate expression="IssueNonceCorrelationCookie" />
-        <evaluate expression="IssueAuthnStateCookie" />
+    <action-state id="BuildOutboundMessage">
+        <!-- Must map authentication state to a state token first, so we can add the token to the state in HandleOutboundMessage -->
+        <evaluate expression="StoreAuthenticationState"/> 
+        <evaluate expression="HandleOutboundMessage" />        
+        <evaluate expression="IssueRequestCorrelationCookie" />
         <evaluate expression="EncodeMessage" />
         <evaluate expression="'proceed'" />
          
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
index 6bcbd1f..f78af5c 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
@@ -24,6 +24,7 @@
         class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype" abstract="true"
         c:executionDirection="OUTBOUND" />
    
+   <!-- TODO we do not need the Context suffix on these beans -->
     <!-- Global Functions -->
     <bean id="shibboleth.ChildLookupOrCreate.SecurityParametersContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -34,6 +35,10 @@
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext) }" />
         
+    <bean id="shibboleth.ChildLookup.OAuthStateContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.sp.oidc.context.OAuthStateContext) }" />
+        
     <bean id="shibboleth.ChildLookup.OIDCPeerEntityContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
@@ -46,6 +51,15 @@
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext) }" 
         c:createContext="true"/>
+        
+     <bean id="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.sp.oidc.context.CorrelationCookieStateContext) }" 
+        c:createContext="true"/>
+        
+    <bean id="shibboleth.ChildLookup.CorrelationCookieStateContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.sp.oidc.context.CorrelationCookieStateContext) }"/>
     
     <bean id="shibboleth.ChildLookup.OAuth2ClientAuthenticationContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -89,6 +103,33 @@
         </constructor-arg>
     </bean>
     
+    <bean id="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContextFromInboundOAuthStateContext" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.ChildLookup.OAuthStateContextFromInbound" />
+        </constructor-arg>
+    </bean>
+    
+    <bean id="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.CorrelationCookieStateContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.ChildLookup.OAuthStateContext" />
+        </constructor-arg>
+    </bean>
+        
+    <bean id="shibboleth.ChildLookup.OAuthStateContextFromInbound" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.OAuthStateContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Inbound" />
+        </constructor-arg>
+    </bean>   
+    
      <bean id="shibboleth.ChildLookup.OAuth2ClientContextFromInbound" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
             <ref bean="shibboleth.ChildLookup.OAuth2ClientContext" />
@@ -187,6 +228,16 @@
         </constructor-arg>
     </bean>
     
+    <bean id="shibboleth.ChildLookup.AuthenticationRequestStateDataFromOutbound" 
+            parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.AuthnRequestStateDataContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Outbound" />
+        </constructor-arg>
+    </bean>
+    
     <!-- Common Actions -->
     
     <bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
index 89ab7d6..4595c27 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
@@ -36,7 +36,6 @@ import org.mockito.Mockito;
 import org.opensaml.messaging.decoder.MessageDecodingException;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.saml.common.binding.SAMLBindingSupport;
 import org.springframework.context.ApplicationContext;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.test.context.ContextConfiguration;
@@ -81,7 +80,7 @@ import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
         inheritInitializers = false
         )
 @WebAppConfiguration
- at SuppressWarnings({ "unchecked", "rawtypes", "null" })
+ at SuppressWarnings({ "unchecked", "null" })
 public class OIDCSessionInitiatorFlowTest extends AbstractSPFlowTest {
     
     /** Flow ID. */
@@ -561,15 +560,13 @@ public class OIDCSessionInitiatorFlowTest extends AbstractSPFlowTest {
             authnRequest = decodeRedirect(redirectURL,
                     input != null ? input.getmember(SPConstants.STATE).string() : null);
         } else {
-            //TODO what to pull out if in the POST body
+            //TODO what to pull out if in the POST body. You can not send a POST message to the authz redirect, so is this needed?
             final byte[] body = http.getmember("response.data").unsafe_string();
             Assert.assertNotNull(body);
             // Not trivial to consider parsing the form, so just bypass that step.
             final Object oidc = prc.ensureOutboundMessageContext().ensureMessage();
             assert oidc instanceof AuthenticationRequest;
             authnRequest = (AuthenticationRequest) oidc;
-            Assert.assertEquals(SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()),
-                    input != null ? input.getmember(SPConstants.STATE).string() : null);
         }
         
         assert authnRequest != null;
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
index bef9712..8812cfb 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
@@ -174,14 +174,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
 
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
-                        TestConstants.STATE_STRING);        
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                                TestConstants.AUTHENTICATION_RFP));        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
@@ -210,19 +212,21 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
 
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
-                        TestConstants.STATE_STRING);        
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                                TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
-                TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
+                TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
         // Use application which requires private_key_jwt
-        setApplicationRequest("test-oidc-application-with-ro-private-key-jwt", input);
+        setApplicationRequest(TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT, input);
 
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
@@ -235,7 +239,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
     }
     
     /**
-     * Test successful flow with a signed id_token and a plain user info response.
+     * Test successful flow with a signed id_token and a plain user info response with max_age set.
      * 
      * @throws IOException on error
      */
@@ -247,14 +251,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
 
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
-                        TestConstants.STATE_STRING);        
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                                TestConstants.AUTHENTICATION_RFP));   
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN,
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null)));
         
@@ -282,14 +288,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN,
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null)));
         
@@ -315,14 +324,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 Instant.now(), null, null), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
@@ -350,14 +362,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null, null), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
@@ -386,14 +401,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null, Map.of("acr","loa2")), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1"))));
         
@@ -424,14 +442,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null, Map.of("acr","loa1")), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1"))));
         
@@ -460,14 +481,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null, Map.of("azp", TestConstants.CLIENT_ID)), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
@@ -494,14 +518,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 null, Map.of("azp", "bad-azp")), constructJWTUserInfoResponseSigned());
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP)); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
@@ -528,14 +555,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 Instant.now(), "bad-idtoken-nonce", null), constructJWTUserInfoResponseSigned()); 
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP)); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
@@ -550,6 +580,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 "JWT \"nonce\" claim has value bad-idtoken-nonce but should be bd1b5f211250c57e");
     }
     
+    /** Test an error response from the Token endpoint.*/
     @Test
     public void testFail_ErrorFromTokenExchange() throws Exception {
         
@@ -557,27 +588,30 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP));         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
-
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
-        assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+        final DDF output = assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+        System.out.println("test output: " + output.toString());
     }
     
     /**
-     * Test a failure, nonce mismatch in the id_token.
+     * Test a success, using a nonce in the id_token
      * 
      * @throws Exception on error
      */
@@ -588,14 +622,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 Instant.now(), TestConstants.ID_TOKEN_NONCE, null), constructJWTUserInfoResponseSigned()); 
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP)); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN,
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
@@ -612,21 +649,25 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
     }
     
     /**
-     * Test failure, a bad nonce in the correlation cookie.
+     * Test failure, the RFP value in the returned state does not match that stored in the 
+     * correlation cookie.
      * 
      * @throws IOException on error
      */
     @Test
-    public void testFail_BadCorrelationNonceState_InCookie() throws Exception {
+    public void testFail_RequestForgeryProtectionValueMisMatch() throws Exception {
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                        TestConstants.AUTHENTICATION_RFP)); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                "bad-nonce", //this is a bad nonce
+                "bad-rfp", //this is a bad rfp value in the cookie
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
@@ -635,27 +676,30 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
-        assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+        final DDF output = assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+        System.out.println("test output: " + output.toString());
     }
     
     /**
-     * Test failure, a bad nonce in the authentication response. That is, what if somehow the return URL was manipulated
-     * accidently or on purpose.
+     * Test failure: the correlation cookie state token in the OAuth state parameter does not reference the 
+     * correlation cookie. Hence the correlation cookie can not be recovered, which is a fatal error.
      * 
      * @throws IOException on error
      */
     @Test
-    public void testFail_BadCorrelationNonceState_InAuthnResponse() throws Exception {
+    public void testFail_CorrelationCookieMismatch() throws Exception {
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
-                        TestConstants.STATE_STRING_DIFFERENT_STATE);
+                        TestConstants.buildOAuthStateString(TestConstants.AUTHENTICATION_STATE_WRONG, 
+                                TestConstants.AUTHENTICATION_RFP)); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE, 
+                TestConstants.AUTHENTICATION_RFP, 
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
@@ -675,13 +719,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
      */
     @Test
     public void testFail_ErrorResponse() throws Exception {
-        final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_STRING));
+        final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(
+                TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_RFP)));
         
         // Add cookies
         input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
-                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.AUTHENTICATION_RFP,
                 TestConstants.TARGET_URL,
                 TestConstants.buildAuthenticationState(null, false, null)));
 
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
index b581746..5408668 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
@@ -18,7 +18,6 @@ import static org.testng.Assert.fail;
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.time.Instant;
@@ -35,10 +34,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.nimbusds.jose.util.StandardCharset;
 
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.oidc.profile.OIDCConstants;
 
 /** A class to hold constants used for tests.*/
 public final class TestConstants {
@@ -63,80 +67,122 @@ public final class TestConstants {
     /** Resource URL. */
     @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
     
-    /** The nonce as part of the state returned in an authentication response. Not the nonce in an id_token.*/
-    @Nonnull public static final String AUTHENTICATION_NONCE = "f26d922221226cd183729bf13247dbe2";
+    /** 
+     * The request forgery protection nonce sent in the authentication request and part of the state returned in an 
+     * authentication response. Not the nonce in an id_token.
+     */
+    @Nonnull public static final String AUTHENTICATION_RFP = "f26d922221226cd183729bf13247dbe2";
     
     /** The nonce in the ID token.*/
     @Nonnull public static final String ID_TOKEN_NONCE = "bd1b5f211250c57e";    
 
-    /** State string used in cookie names. */
-    public static final String AUTHENTICATION_STATE = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";
+    /** State token string used in cookie names. */
+    public static final String STATE_TOKEN = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";
+    
+    /** Authentication state token used to reference the stored authentication state. */
+    public static final String AUTHENTICATION_STATE_TOKEN = "1761316967710_authnstate";
+    
+    /** State string used in cookie names. A state which should not match to the a correlation cookie. */
+    public static final String AUTHENTICATION_STATE_WRONG = "1761316967710_wrong";
 
     /** Application ID used in cookie names. */
     public static final String APPLICATION_ID = "test-oidc-application-with-ro";
+    
+    /** Application ID used when private_key_jwt has been configured.*/
+    public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
 
     /** Target URL encoded in cookie. */
     public static final String TARGET_URL = "https://sp.example.org/secure";
-
-    /** 
-     * A returned state String consisting of a state and a nonce: 
-     * 
-     * <pre>
-     * {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
-     * "nonce":"f26d922221226cd183729bf13247dbe2"}
-     * </pre>
-     * */
-    public static final String STATE_STRING = """
-        eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
-        """;
-
-    /** 
-     * A returned state String consisting of a state and a nonce. The state has been changed, or is not expected: 
-     * 
-     * <pre>
-     * {"state":"1761316967710_wrong",
-     * "nonce":"f26d922221226cd183729bf13247dbe2"}
-     * </pre>
-     * */
-    public static final String STATE_STRING_DIFFERENT_STATE = """
-        eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfd3JvbmciLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0=
-        """;
     
     /**
      * Build cookie header bytes for the given parameters. These take the form of:
      * <pre>
-     * __Host-_shibsp_req_{state}={nonce};
-     * __Host-shibsp_state_{appId}_{state}={targetURL};
-     * __Host-shibsp_authnstate_{state}={authn_request_json};
+     * __Host-_shibsp_req_{stateToken}={(authnStateToken, rfp)}; (correlation cookie)
+     * __Host-shibsp_state_{appId}_{stateToken}={targetURL};  (resource URL state)
+     * __Host-shibsp_state_{appId}_{authnStateToken}={authn_request_json};   (authentication state)
      * </pre>
      * 
-     * @param state the state used for mapping response to the stored headers
+     * @param stateToken the token value used for mapping response to the stored correlation cookie and target URL
+     * @param authnStateToken the token value, held inside the 'req' correlation cookie, of the persisted authentication state.
      * @param appID the application ID
-     * @param nonce the nonce used to protect against CSRF in the request and response OAuth state parameter
-     *                  not to be confused with the OIDC ID token nonce.
+     * @param rfp the request forgery protection nonce used to protect against CSRF in the request and response OAuth 
+     *                  state parameter not to be confused with the OIDC ID token nonce. Stored inside the correlation cookie
      * @param targetURL the target URL to redirect to after processing
      * @param authnRequestStateJSON the authentication request state JSON for recovery of important parameters
      * @return the cookie header bytes
      * @throws Exception on error
      */
-    public static byte[] buildCookieHeader(final String state, final String appID, final String nonce, 
+    public static byte[] buildCookieHeader(final String stateToken,
+            final String authnStateToken, final String appID, final String rfp, 
             final String targetURL, final String authnRequestStateJSON) throws Exception {
         
 
         final String targetUrlB64 = Base64Support.encodeURLSafe(targetURL.getBytes(StandardCharsets.UTF_8));
-        final String authnJsonEnc = URLEncoder.encode(authnRequestStateJSON, StandardCharsets.UTF_8);
+        final String authnJsonEnc = Base64Support.encodeURLSafe(authnRequestStateJSON.getBytes(StandardCharsets.UTF_8));
 
         // Compose the header with semicolons and spacing as in the example
         final StringBuilder sb = new StringBuilder();
-        sb.append("__Host-shibsp_req_").append(state).append('=').append(nonce).append(";\n");
-        sb.append("__Host-shibsp_state_").append(appID).append('_').append(state)
+        sb.append("__Host-shibsp_req_").append(stateToken).append('=')
+            .append(buildCorrelationCookieString(authnStateToken, rfp)).append(";\n");
+        sb.append("__Host-shibsp_state__").append(appID).append('_').append(stateToken)
           .append('=').append(targetUrlB64).append("; \n");
-        sb.append("__Host-shibsp_authnstate_").append(state).append('=').append(authnJsonEnc).append(";\n");
+        sb.append("__Host-shibsp_state__").append(appID).append('_').append(authnStateToken).append('=').append(authnJsonEnc).append(";\n");
         System.out.println(sb.toString());
         return sb.toString().getBytes("UTF-8");
         
     }
     
+    /**
+     * Build the OAuth state string for the given state and RFP. This is used for the state parameter in the 
+     * authentication request, and contains important information about the authentication request.
+     * 
+     * @param state the state used to map to the correlation cookie bound to the browser session.
+     * @param rfp the request forgery protection nonce used to protect against CSRF in the request and response OAuth 
+     *          state parameter.
+     * @return the JSON serialized and Base64 encoded state string for the given parameters
+     */
+    public static String buildOAuthStateString(final String state, final String rfp) {
+        final JSONObject stateObject = new JSONObject();
+        stateObject.appendField(OIDCConstants.STATE_FIELD, state);
+        stateObject.appendField(OIDCConstants.RFP_FIELD,rfp);
+        final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+        try {
+            final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+            assert stateJsonAsBytes != null;
+            return Base64Support.encodeURLSafe(stateJsonAsBytes);
+        } catch (final EncodingException e) {
+            fail(e.getMessage());
+            return null;
+        }
+        
+    }
+    
+    /**
+     * Build the correlation cookie value for the given authentication state token and RFP. The authnStateToken is used
+     * to recover the authentication state stored off in the state token manager. The RFP is used to protect against 
+     * CSRF in the authentication request and response, and should be matched to that present in the OAuth state 
+     * parameter. Both are stored in the cookie value, which is JSON serialized and Base64 encoded.
+     * 
+     * @param authnStateToken the token value used for recovery of the authentication state stored off in the state 
+     *          token manager.
+     * @param rfp the request forgery protection nonce used to protect against CSRF in the request and response
+     * @return
+     */
+    public static String buildCorrelationCookieString(final String authnStateToken, final String rfp) {
+        final JSONObject stateObject = new JSONObject();
+        stateObject.appendField(OIDCConstants.AUTHN_STATE_FIELD, authnStateToken);
+        stateObject.appendField(OIDCConstants.RFP_FIELD,rfp);
+        final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+        try {
+            final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+            assert stateJsonAsBytes != null;
+            return Base64Support.encodeURLSafe(stateJsonAsBytes);
+        } catch (final EncodingException e) {
+            fail(e.getMessage());
+            return null;
+        }
+    }
+    
     /**
      * Build the authentication request state JSON for the given parameters. This is used for the state value, and 
      * contains important information about the authentication request.
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
index a51e1a2..2964fcd 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
@@ -51,6 +51,10 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
         mock.setProperty("sp.oidc.idtoken.validateAcrValue", "true");
         // Turn off the sealing of authentication request state for tests
         mock.setProperty("sp.oidc.sealAuthenticationState", "false");
+        // Turn off the sealing of the correlation cookie for tests
+        mock.setProperty("sp.oidc.sealCorrelationCookie", "false");
+        // Turn off the sealing of OAuth state for tests
+        mock.setProperty("sp.oidc.sealOAuthState", "false");
         // Create a basic default client secret
         mock.setProperty("sp.oidc.defaultClientSecret", "secret");
         mock.setProperty("idp.additionalProperties",
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
index 29c61b9..dcfa3b4 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
@@ -15,12 +15,16 @@
 package net.shibboleth.sp.oidc.profile.decoding.impl;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
 import org.slf4j.Logger;
 
 import com.nimbusds.oauth2.sdk.id.State;
@@ -31,20 +35,80 @@ import net.minidev.json.JSONValue;
 import net.minidev.json.parser.ParseException;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.sp.oidc.context.OAuthStateContext;
 
 /**
  * Decodes the OAuth 2.0 state parameter from the {@link AuthenticationResponse} message
- * into a {@link JSONObject}.
+ * into a {@link JSONObject}. The state parameter is assumed to be base64 encoded.
+ * 
+ * <p>If sealing is enabled, unwrap the state using the {@link DataSealer} provided. </p>
  *
  * <p>Returns true if the operation was successful. Returns false if the state cannot be decoded or parsed into a 
  * valid JSON object.</p>
  */
-public class DecodeStateAsJsonObjectConsumer implements Function<MessageContext, Boolean> {
+public class DecodeStateAsJsonObjectConsumer extends AbstractIdentifiableInitializableComponent 
+            implements Function<MessageContext, Boolean> {
+    
+    /** Lookup function for parent ProfileRequestContext. */
+    @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+        = new ParentProfileRequestContextLookup<>();
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DecodeStateAsJsonObjectConsumer.class);
+    
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** 
+     * If the dataSealer is provided should it be used to unwrap the state? Defaults to true, 
+     * that is, if the dataSealer is provided, always attempt to unwrap the state.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> unsealState;
+    
+    /** Constructor.*/
+    public DecodeStateAsJsonObjectConsumer() {
+        unsealState = PredicateSupport.alwaysTrue();
+    }
+    
+    /**
+     * Sets {@link DataSealer} to use.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        checkSetterPreconditions();
+        
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set the predicate to determine whether to unseal the state.
+     * 
+     * @param predicate the seal state predicate to set.
+     */
+    public void setUnsealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        
+        unsealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+    }
+    
+    /**
+     * Set the flag to determine whether to unseal the state.
+     * 
+     * @param flag the flag to set.
+     */
+    public void setUnsealState(final boolean flag) {
+        checkSetterPreconditions();
+        
+        unsealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
 
     /** {@inheritDoc} */
     @Override
@@ -63,15 +127,40 @@ public class DecodeStateAsJsonObjectConsumer implements Function<MessageContext,
     
         try {
             final String stateValue = responseState.getValue();
-            assert stateValue != null;
+            assert stateValue != null;            
+            Object stateObjectParsed = null;
+            
+            // If sealed or not sealed, we base64 URL decode the state value first. This therefore assumes the state
+            // value is always base64 encoded. In the case of sealed state, the sealed value is also base64 (non-url)
+            // encoded, so we base64 URL decode to return back to the base64 encoded value to unwrap.
             final byte[] decodedBytes = Base64Support.decodeURLSafe(stateValue);
-            final JSONObject jsonState = (JSONObject) JSONValue.parseWithException(decodedBytes);
-            if (jsonState != null) {
-                messageContext.ensureSubcontext(OAuthStateContext.class)
-                    .setState(responseState.getValue())
-                    .setStateJson(jsonState);
+            
+            final var localDataSealer = dataSealer;
+            if (localDataSealer != null && unsealState.test(PRC_LOOKUP.apply(messageContext))) {
+                final String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
+                log.trace("Attempting to unseal state value '{}'", decodedString);
+                final String unwrapped = localDataSealer.unwrap(decodedString);
+                log.trace("Unwrapped state '{}'", unwrapped);
+                stateObjectParsed = JSONValue.parseWithException(unwrapped);
+            } else {        
+                final String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
+                stateObjectParsed = JSONValue.parseWithException(decodedString);
+
+            }            
+
+            
+            if (!(stateObjectParsed instanceof JSONObject)) {
+                log.debug("OAuth state did not decode to a JSON object");
+                return false;
             }
-        } catch (DecodingException | IOException | ParseException e) {
+            final JSONObject jsonState = (JSONObject) stateObjectParsed;                
+            log.trace("Decoded JSON state '{}'", stateObjectParsed);
+
+            messageContext.ensureSubcontext(OAuthStateContext.class)
+                .setState(responseState.getValue())
+                .setStateJson(jsonState);
+            
+        } catch (DecodingException | ParseException | DataSealerException e) {
             log.debug("Unable to decode OAuth state into a JSON token", e);
             return false;
         }
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
index a516fb7..309848b 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
@@ -148,7 +148,12 @@ public class IssueStateCookie extends AbstractApplicationAction {
         stateValue = stateValueLookupStrategy.apply(profileRequestContext);
         
         if (stateValue == null) {
-            log.debug("{} No cookie contents available, skipping creation of state cookie", getLogPrefix());
+            if (errorFatal) {
+                log.warn("{} State value was missing", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            } else {
+                log.debug("{} No cookie contents available, skipping creation of state cookie", getLogPrefix());
+            }            
             return false;
         }
         
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java
new file mode 100644
index 0000000..2e09a18
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java
@@ -0,0 +1,167 @@
+/*
+ * 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.oidc.profile.impl;
+
+import java.io.IOException;
+import java.util.function.BiFunction;
+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 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.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+
+
+/**
+ * Profile action that recovers a state value from a state token and uses a customisable strategy to set it onto the
+ * {@link ProfileRequestContext}.
+ *
+ * <p>Failures encountered during processing may be treated as fatal according to the {@link #errorFatal} flag. If
+ * fatal and an error occurs, an {@link EventIds#INVALID_MESSAGE} event is raised.</p>
+ *
+ * <p>This action does not interpret the state value itself. Instead, the token value consumer is expected to 
+ * understand the structure and semantics of the recovered state value and set it on the {@link ProfileRequestContext} 
+ * appropriately.</p>
+ */
+public class MapStateTokenToStateValue extends AbstractApplicationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateTokenToStateValue.class);
+    
+    /** Lookup strategy for state token. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
+    
+    /** 
+     * A consumer {@link BiFunction} that process the state value and adds it to the profile request context. The 
+     * returned boolean indicates if the consumer function was successful or not. An unsuccessful outcome may trigger 
+     * an error depending on the {@link #errorFatal} flag.
+     */
+    @NonnullAfterInit private BiFunction<ProfileRequestContext, byte[], Boolean> stateValueConsumer;
+    
+    /** Whether an error mapping the state token to a state value is fatal. */
+    private boolean errorFatal;
+    
+    /**
+     * 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");
+    }
+    
+    /**
+     * Set the state value consumer to add the recovered state value to the profile request context. The return value 
+     * indicates if the consumer function was successful or not. An unsuccessful outcome may trigger an error 
+     * depending on the {@link #errorFatal} flag.
+     * 
+     * @param consumer The consumer to set.
+     */
+    public void setStateValueConsumer(final BiFunction<ProfileRequestContext, byte[], Boolean> consumer) {
+        checkSetterPreconditions();
+        stateValueConsumer = Constraint.isNotNull(consumer, "StateValueConsumer can not be null");
+    }
+    
+    /**
+     * Sets whether an error looking up a state value from a state token should result in a fatal event.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setErrorFatal(final boolean flag) {
+        checkSetterPreconditions();
+        
+        errorFatal = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (stateTokenLookupStrategy == null) {
+            throw new ComponentInitializationException("State token lookup strategy cannot be null");
+        }
+        if (stateValueConsumer == null) {
+            throw new ComponentInitializationException("State value consumer cannot be null");
+        }
+    }
+
+    /** {@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", getLogPrefix());
+                if (errorFatal) {
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                }
+                return;
+            }
+            
+            final byte[] stateValue = ensureApplication().getStateTokenManager().recoverFromStateToken(
+                    ensureAgent(), ensureApplication(), token);     
+
+            if (stateValue == null && !errorFatal) {
+                log.debug("{} No state value found for state token '{}'", getLogPrefix(), token);
+                return;
+            } else if (stateValue == null && errorFatal){
+                log.debug("{} No state value found for state token '{}'", getLogPrefix(), token);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            
+            final Boolean success = stateValueConsumer.apply(profileRequestContext, stateValue);
+            // Treat null as failure
+            if ((success == null || Boolean.FALSE.equals(success)) && errorFatal) {
+                log.warn("{} Could not set state value", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
+            }
+            
+        } catch (final IOException e) {
+            log.warn("{} Exception recovering state value from state token", getLogPrefix(), e);
+            if (errorFatal) {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            }
+        } finally {
+            RemotedHttpServletRequestResponseContext.clearCurrent();
+        }
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
similarity index 64%
rename from sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
index b0a13ab..167d50e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
@@ -16,6 +16,7 @@ package net.shibboleth.sp.oidc.profile.impl;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -26,35 +27,53 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+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;
+import net.shibboleth.sp.profile.StateTokenManager;
 
 /**
- *
+ * 
+ * An action that extracts a state value from the {@link ProfileRequestContext} using a configured lookup strategy 
+ * and maps that value into an application-managed state token. The resulting token is then stored via a configured
+ * consumer for later use: such as placement into an OAuth 2.0 "state" parameter or other correlation mechanism.
+ * 
+ * <p>The action delegates the actual token creation and storage to the application's 
+ * {@link StateTokenManager}.</p>
+ * 
+ * 
+ * @event {@link EventIds#IO_ERROR}
  */
-public class MapStateToStateToken extends AbstractApplicationAction {
+public class MapStateValueToStateToken extends AbstractApplicationAction {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateToStateToken.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateValueToStateToken.class);
 
     /** Whether an error constructing a state token is fatal. */
     private boolean errorFatal;
     
-    /** Agent input. */
-    @NonnullBeforeExec private DDF input;
-
-    /** Target resource to operate on. */
-    @NonnullBeforeExec private byte[] target;
-    
-    /** Lookup strategy for the contents of the state cookie. */
+    /** Lookup strategy for the contents of the state token. */
     @NonnullAfterInit private Function<ProfileRequestContext,String> stateValueLookupStrategy;
     
+    /** A consumer that adds the state token to the profile request context.*/
+    @NonnullAfterInit private BiConsumer<ProfileRequestContext, String> stateTokenConsumer;
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (stateValueLookupStrategy == null) {
+            throw new ComponentInitializationException("StateValueLookupStrategy cannot be null");
+        }
+        if (stateTokenConsumer == null) {
+            throw new ComponentInitializationException("StateTokenConsumer cannot be null");
+        }
+    }
+    
     
     /**
      * Sets whether an error computing a state token should result in a fatal event.
@@ -70,7 +89,7 @@ public class MapStateToStateToken extends AbstractApplicationAction {
     }
     
     /**
-     * Sets the lookup strategy for obtaining the contents of the state cookie.
+     * Sets the lookup strategy for obtaining the contents of the state token.
      * 
      * @param strategy lookup strategy
      */
@@ -80,34 +99,15 @@ public class MapStateToStateToken extends AbstractApplicationAction {
         stateValueLookupStrategy = Constraint.isNotNull(strategy, "State value lookup strategy cannot be null");
     }
     
-    /** {@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) {
-//            log.warn("{} Input message did not contain {} member", getLogPrefix(), SPConstants.TARGET);
-//            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-//            return false;
-//        }
-//        
-//        ensureAgentRequestContext().setTargetURL(target);
+    /**
+     * Set the consumer used to store off the state token onto the profile request context.
+     * 
+     * @param consumer The stateTokenConsumer to set.
+     */
+    public void setStateTokenConsumer(@Nonnull final BiConsumer<ProfileRequestContext, String> consumer) {
+        checkSetterPreconditions();
         
-        return true;
+        stateTokenConsumer = Constraint.isNotNull(consumer, "StateTokenConsumer can not be null");
     }
 
     /** {@inheritDoc} */
@@ -117,12 +117,6 @@ public class MapStateToStateToken extends AbstractApplicationAction {
         ensureOutputObjects();
         
         final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-        
-        String state = null;
-        if (input.getmember(SPConstants.STATE).isstring()) {      
-            log.debug("{} Input message already contains {} parameter", getLogPrefix(), SPConstants.STATE);
-            state = input.getmember(SPConstants.STATE).string();
-        }
 
         // We do the crazy stuff to accomodate cookie-backed state management.
         try {
@@ -134,16 +128,18 @@ public class MapStateToStateToken extends AbstractApplicationAction {
                 if (stateValue != null) {
                     //TODO UTF-8?
                     final byte[] stateValueBytes = stateValue.getBytes(StandardCharsets.UTF_8);
+                    // b64 encode is handled by the state manager
                     assert stateValueBytes != null;
                     final String token = ensureApplication().getStateTokenManager().preserveToStateToken(
                             ensureAgent(), ensureApplication(), stateValueBytes);
-                    if (state == null) {
-                        input.addmember(SPConstants.STATE).string(token);
-                    }
+                    
+                    stateTokenConsumer.accept(profileRequestContext, token);
                     
                     log.debug("{} State preserved to state token: {}", getLogPrefix(), token);
+                } else{
+                    log.debug("{} State value was null, state not preserved", getLogPrefix());
                 }
-            } catch (final IOException e) {
+            } catch (final IOException | RuntimeException e) {
                 log.warn("{} Exception preserving state to token", getLogPrefix(), e);
                 if (errorFatal) {
                     ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
index 7a0c655..0a1b586 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
@@ -19,7 +19,6 @@ import java.security.SecureRandom;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.apache.commons.codec.binary.Hex;
 import org.opensaml.messaging.context.MessageContext;
 
 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
@@ -35,7 +34,7 @@ public final class OIDCSupport {
     }
     
     /**
-     * Generates a random identifier, encoded in Hex..
+     * Generates a secure random identifier, encoded in Hex..
      *  
      * @param length the length of the parameter.
      * 
@@ -51,29 +50,6 @@ public final class OIDCSupport {
         assert nonce != null;
         return nonce;
     }
-        
-    
-    /**
-     * <p>Generate a state parameter from a nonce component and an execution key component.</p>
-     * 
-     * <p>The nonce is separated from the key by a dot e.g. {@literal <nonce>.<keyHex>}.</p>
-     * 
-     *  <p>The nonce is assumed to be already encoded in its transmission format e.g. Hex. The key is
-     *  hex encoded before it is combined with the nonce. The result is assumed URL encoded e.g. inside
-     *  the allowed set of URI characters or, no character in the state is from the URI reserved set.</p>
-     * 
-     * @param nonce the nonce component. 
-     * @param key the key component. The key is hex encoded before it is added to the generated state.
-     * 
-     * @return the combined state component.
-     */
-    @Nonnull public static String generateState(@Nonnull final String nonce, @Nonnull final String key) {
-        Constraint.isNotNull(nonce, "NonceHex key can not be null");
-        Constraint.isNotNull(key, "Webflow execution key can not be null");
-        
-        final String keyHex = Hex.encodeHexString(key.getBytes());
-        return nonce+"."+keyHex;
-    }
     
     /**
      * Extracts the state parameter value from an {@link AuthenticationResponse}.
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
similarity index 90%
rename from sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
index 12a14bd..5dafa83 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
@@ -41,8 +41,8 @@ import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
 import net.shibboleth.sp.profile.AbstractApplicationAction;
 
 /**
- * Action that processes a previously issued state cookie submitted with the request and
- * extracts the value for use by subsequent validation steps.
+ * Action that processes a previously issued state cookie, referenced by a state token, submitted with the request and
+ * extracts the value for use by subsequent validation steps. The cookie is immediately unset once retrieved.
  * 
  * <p>The value is set onto the context tree using a custom consumer strategy.</p>
  * 
@@ -52,13 +52,13 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_MESSAGE}
  */
-public class ProcessStateCookie extends AbstractApplicationAction {
+public class ResolveStateCookie extends AbstractApplicationAction {
     
     /** Default cookie prefix. */
     @Nonnull @NotEmpty public static final String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessStateCookie.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveStateCookie.class);
     
     /** Cookie manager. */
     @NonnullAfterInit private CookieManager cookieManager;
@@ -83,7 +83,7 @@ public class ProcessStateCookie extends AbstractApplicationAction {
     @NonnullBeforeExec private String stateToken;
     
     /** Constructor. */
-    public ProcessStateCookie() {
+    public ResolveStateCookie() {
         cookiePrefix = DEFAULT_COOKIE_PREFIX;
     }
     
@@ -114,7 +114,7 @@ public class ProcessStateCookie extends AbstractApplicationAction {
     /**
      * Sets the cookie prefix.
      * 
-     * <p>Defaults to {@link ProcessStateCookie#DEFAULT_COOKIE_PREFIX}.</p>
+     * <p>Defaults to {@link ResolveStateCookie#DEFAULT_COOKIE_PREFIX}.</p>
      * 
      * @param prefix cookie prefix
      */
@@ -199,17 +199,17 @@ public class ProcessStateCookie extends AbstractApplicationAction {
             final String cookieName = cookiePrefix + escaper.escape(stateToken);
 
             final String value = cookieManager.getCookieValue(cookieName, null);
+            cookieManager.unsetCookie(cookieName);
+            
             if (value == null && !errorFatal) {
-                log.debug("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+                log.debug("{} No cookie found for state token '{}'", getLogPrefix(), stateToken);
                 return;
             } else if (value == null){
-                log.warn("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+                log.debug("{} No cookie found for state token '{}'", getLogPrefix(), stateToken);
                 ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
                 return;
             }
 
-            cookieManager.unsetCookie(cookieName);
-
             if (cookieValueConsumerStrategy != null) {
                 final Boolean success = cookieValueConsumerStrategy.apply(profileRequestContext, value);
                 // Treat null as failure
@@ -218,9 +218,9 @@ public class ProcessStateCookie extends AbstractApplicationAction {
                     ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
                     return;
                 }
-                log.debug("{} Processed correlation cookie for state token '{}'", getLogPrefix(), stateToken);
+                log.debug("{} Resolved cookie for state token '{}'", getLogPrefix(), stateToken);
             } else {
-                log.warn("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
+                log.debug("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
             }
         } finally {
             RemotedHttpServletRequestResponseContext.clearCurrent();
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
index f0c609e..4a3cce5 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.sp.oidc.profile.impl;
 
+import java.nio.charset.StandardCharsets;
 import java.util.function.BiFunction;
 import java.util.function.Predicate;
 
@@ -33,21 +34,26 @@ import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponen
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.PredicateSupport;
-import net.shibboleth.shared.net.URISupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
 import net.shibboleth.shared.security.DataSealer;
 import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
 import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
 
 /**
- * A consumer function that converts the decoded authentication request state data JSON string into an object and sets it
- * onto an {@link AuthnRequestStateDataContext} within the inbound message context, along with setting the issuer onto
- * the {@link OIDCPeerEntityContext}.
+ * A consumer function that converts the authentication request state data JSON into an 
+ * {@link AuthenticationRequestStateData} object and sets it onto an {@link AuthnRequestStateDataContext} within the 
+ * inbound message context, along with setting the issuer onto the {@link OIDCPeerEntityContext}.
+ * 
+ * <p>The supplied byte array is assumed to represent UTF‑8 JSON, already Base64‑decoded upstream.</p>
+ * 
+ * <p>In production use, you should always attempt to unseal the authentication request state, such that it will fail if
+ * it was not sealed. Allowing unsealed authentication request state should only be used for testing.</p>
  */
 //TODO this class is complex and more fitting a decoder type mechanism
 public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdentifiableInitializableComponent 
-    implements BiFunction<ProfileRequestContext, String, Boolean> {
+    implements BiFunction<ProfileRequestContext, byte[], Boolean> {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationRequestToPeerContextConsumer.class);
@@ -125,34 +131,32 @@ public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdent
 
     /** {@inheritDoc} */
     @Override
-    public Boolean apply(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final String value) {
+    public Boolean apply(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final byte[] value) {
         checkComponentActive();
         if (profileRequestContext == null || value == null) {
             log.trace("Authentication request value could not be decoded, the input value was null");
             return false;
         }
-        String decoded = null;
-        try {
-            decoded = URISupport.doURLDecode(value);
-        } catch (final Exception e) {
-            log.trace("Authentication request value could not be decoded", e);
-        }
-        if (decoded == null) {
-            log.trace("Authentication request value was not decoded");
-            return false;
-        }
+        // The value is already base64 decoded by the token manager
+        String valueString = new String(value, StandardCharsets.UTF_8);
         // Possibly unseal and convert to class
         try {
             if (unsealState.test(profileRequestContext) && dataSealer != null) {
-                decoded = dataSealer.unwrap(decoded);
+                valueString = dataSealer.unwrap(valueString);
             }
-            final AuthenticationRequestStateData authnState = objectMapper.readValue(decoded, AuthenticationRequestStateData.class);
+            final AuthenticationRequestStateData authnState = 
+                    objectMapper.readValue(valueString, AuthenticationRequestStateData.class);
             log.debug("Recovered authentication request state '{}'", authnState);
             final MessageContext inboundCtx = profileRequestContext.getInboundMessageContext();
             if (inboundCtx == null) {
                 log.trace("There is no Inbound Context, cannot set authentication request data");
                 return false;
             }
+            if (StringSupport.trimOrNull(authnState.getAuthenticationAuthority()) == null) {
+                // Fail early as this is usually terminal
+                log.debug("Authenticating authority could not be recovered");
+                return false;
+            }
             // Add identifier to the peer context now we know it
             inboundCtx.ensureSubcontext(OIDCPeerEntityContext.class)
                     .setIdentifier(authnState.getAuthenticationAuthority());
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.java
new file mode 100644
index 0000000..80d0357
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.oidc.profile.impl;
+
+import java.util.function.BiConsumer;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+
+/**
+ * A consumer that sets the authentication state token on the {@link AuthnRequestStateDataContext} in the outbound 
+ * message context of the {@link ProfileRequestContext}.
+ */
+public class SetAuthenticationStateTokenConsumer implements BiConsumer<ProfileRequestContext, String>{
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationStateTokenConsumer.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final ProfileRequestContext prc, final String token) {        
+        final MessageContext outbound = prc.getOutboundMessageContext();
+        if (outbound == null) {
+            return;
+        }
+        log.trace("Setting authentication state token '{}' onto token context", token);
+        final AuthnRequestStateDataContext stateCtx = outbound.ensureSubcontext(AuthnRequestStateDataContext.class);
+        stateCtx.setToken(token);
+        
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
new file mode 100644
index 0000000..bf546f1
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
@@ -0,0 +1,184 @@
+/*
+ * 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.oidc.profile.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.parser.ParseException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
+
+
+
+/**
+ * Consumer that decodes a correlation cookie value and stores it on a {@link CorrelationCookieStateContext}. The
+ * cookie value is expected to be the raw value of the cookie, decoding happens here.
+ *
+ * <p>The supplied value is expected to represent a JSON object encoded either as:
+ * <ul>
+ *   <li>a URL-safe Base64 representation of UTF-8 JSON, or</li>
+ *   <li>a URL-safe Base64 representation of UTF-8 JSON sealed using a {@link DataSealer}.</li>
+ * </ul>
+ *
+ * <p>On failure, this consumer returns {@code false}.</p>
+ */
+ at ThreadSafeAfterInit
+public class SetCorrelationCookieValueToContextConsumer extends AbstractIdentifiableInitializableComponent
+            implements BiFunction<ProfileRequestContext, String, Boolean> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SetCorrelationCookieValueToContextConsumer.class);
+    
+    /** The strategy to locate the {@link CorrelationCookieStateContext} to operate on. */
+    @NonnullAfterInit 
+    private Function<ProfileRequestContext, CorrelationCookieStateContext> correlationCookieStateContextLookupStrategy;
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** 
+     * If the dataSealer is provided should it be used to unwrap the correlation cookie state? Defaults to true, 
+     * that is, if the dataSealer is provided, always attempt to unwrap the state.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> unsealState;
+    
+    /** Constructor.*/
+    public SetCorrelationCookieValueToContextConsumer() {
+        unsealState = PredicateSupport.alwaysTrue();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (correlationCookieStateContextLookupStrategy == null) {
+            throw new ComponentInitializationException("CorrelationCookieStateContextLookupStrategy "
+                    + "lookup strategy can not be null");
+        }
+    }
+    
+    /**
+     * Set the strategy to locate the {@link CorrelationCookieStateContext} to operate on.
+     * 
+     * @param strategy The strategy to set.
+     */
+    public void setCorrelationCookieStateContextLookupStrategy(
+            final Function<ProfileRequestContext, CorrelationCookieStateContext> strategy) {
+        checkSetterPreconditions();
+        correlationCookieStateContextLookupStrategy = Constraint.isNotNull(strategy,
+                "CorrelationCookieStateContextLookupStrategy can not be null");
+    }
+    
+    /**
+     * Sets {@link DataSealer} to use.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        checkSetterPreconditions();
+        
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set the predicate to determine whether to unseal the state.
+     * 
+     * @param predicate the seal state predicate to set.
+     */
+    public void setUnsealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        
+        unsealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+    }
+    
+    /**
+     * Set the flag to determine whether to unseal the state.
+     * 
+     * @param flag the flag to set.
+     */
+    public void setUnsealState(final boolean flag) {
+        checkSetterPreconditions();
+        
+        unsealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
+        // Value should be b64 encoded
+        if (value == null ||  profileRequestContext == null) {
+            return false;
+        }
+        final CorrelationCookieStateContext context = 
+                correlationCookieStateContextLookupStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.debug("Correlation cookie context not found, can not set correlation cookie onto the context");
+            return false;
+        }
+        log.trace("Attempting to set correlation cookie onto the context '{}'", value);
+        Object correlationCookieAsObject = null;        
+        try {
+            final byte[] decoded = Base64Support.decodeURLSafe(value);
+            final String decodedString = new String(decoded, StandardCharsets.UTF_8);
+            log.trace("Attempting to set base64 URL decoded correlation cookie '{}'", decodedString);
+            final var localDataSealer = dataSealer;
+            if (localDataSealer != null && unsealState.test(profileRequestContext)) {
+                log.trace("Attempting to unseal correlation cookie");
+                final String unsealed = localDataSealer.unwrap(decodedString);
+                correlationCookieAsObject = JSONValue.parseWithException(unsealed);                
+            } else {                
+                correlationCookieAsObject = JSONValue.parseWithException(decodedString);                
+            }
+            
+        } catch (final DataSealerException | DecodingException | ParseException e) {
+            log.warn("Unable to process correlation cookie, was it sealed and you are not unsealing it?"
+                    , e);
+            return false;
+        }
+        log.trace("Decoded correlation cookie as '{}'", correlationCookieAsObject);
+        if (correlationCookieAsObject instanceof final JSONObject json) {
+            context.setValue(json);   
+        } else {
+            log.error("Decoded correlation cookie was not a JSON object");
+            return false;
+        }
+             
+        return true;
+        
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
deleted file mode 100644
index 2d7b7f1..0000000
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
+++ /dev/null
@@ -1,53 +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.oidc.profile.impl;
-
-import java.util.function.BiFunction;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.net.URISupport;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.context.TokenConsumerContext;
-
-/**
- * A consumer that sets the decoded nonce value onto the token context's message correlation ID.
- * 
- * TODO, check this is a sensible place to put the nonce, if so, document why a nonce here not a correlation ID.
- */
-public class SetNonceValueToTokenContextConsumer implements BiFunction<ProfileRequestContext, String, Boolean> {
-    
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(SetNonceValueToTokenContextConsumer.class);
-
-    /** {@inheritDoc} */
-    @Override
-    public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
-        
-        final String decoded = URISupport.doURLDecode(value);
-        
-        log.debug("Nonce '{}' set onto token context", decoded);
-        
-        profileRequestContext.ensureSubcontext(AgentRequestContext.class)
-            .ensureSubcontext(TokenConsumerContext.class).setMessageCorrelationID(decoded);
-        return true;
-        
-    }
-
-}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
index 979bfcb..053b523 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
@@ -14,40 +14,92 @@
 
 package net.shibboleth.sp.oidc.profile.impl;
 
+import java.nio.charset.StandardCharsets;
+import java.util.function.Predicate;
+
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
 import org.slf4j.Logger;
 
-import com.nimbusds.jose.util.StandardCharset;
-
 import net.minidev.json.JSONObject;
 import net.minidev.json.JSONValue;
 import net.shibboleth.oidc.profile.core.StateToken;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.oidc.profile.OIDCConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
 /**
  * A strategy function that constructs a Base64URL encoded JSON object containing a state value from the input 
- * {@link DDF} and a cryptographically secure nonce for CSRF protection.
+ * {@link DDF} and a cryptographically secure request forgery protection value for CSRF protection.
  *
  * <p>The JSON Object includes the following:</p>
  * <ul>
  *   <li><b>state</b>: The value of {@code SPConstants.STATE} from the input DDF.</li>
- *   <li><b>nonce</b>: A securely generated random string (32 characters).</li>
+ *   <li><b>rfp</b>: A securely generated random string (32 characters).</li>
  * </ul>
- * 
- * 
 */
 public class StateLookupStrategy extends AbstractAgentAndRelyingPartyContextLookupFunction<StateToken> {
     
+    /** Lookup function for parent ProfileRequestContext. */
+    @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+        = new ParentProfileRequestContextLookup<>();
+    
     /** Class logger. */
     @Nonnull
-    private final Logger log = LoggerFactory.getLogger(StateLookupStrategy.class);
+    private final Logger log = LoggerFactory.getLogger(StateLookupStrategy.class); 
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** 
+     * If the dataSealer is provided should it be used to seal the authentication request state? Defaults to true, 
+     * that is, if the dataSealer is provided, always seal state.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> sealState;
+    
+    /** Constructor.*/
+    public StateLookupStrategy() {
+        sealState = PredicateSupport.alwaysTrue();
+    }
+    
+    /**
+     * Sets {@link DataSealer} to use.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {        
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set the predicate to determine whether to seal the state.
+     * 
+     * @param predicate the seal state predicate to set.
+     */
+    public void setSealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        sealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+    }
+    
+    /**
+     * Set the flag to determine whether to seal the state.
+     * 
+     * @param flag the flag to set.
+     */
+    public void setSealState(final boolean flag) {
+        sealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+     
 
     /** {@inheritDoc} */
     @Override
@@ -56,19 +108,36 @@ public class StateLookupStrategy extends AbstractAgentAndRelyingPartyContextLook
         if (input == null) {
             return null;
         }
+        
         final JSONObject stateObject = new JSONObject();
         stateObject.appendField(OIDCConstants.STATE_FIELD, input.getmember(SPConstants.STATE).string());
-        stateObject.appendField(OIDCConstants.NONCE_FIELD, OIDCSupport.generateRandom(32));
+        stateObject.appendField(OIDCConstants.RFP_FIELD, OIDCSupport.generateRandom(32));
         final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
         if (stateJsonString == null || stateJsonString.isEmpty()) {
             return null;
         }
         try {
-            final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+            final DataSealer localDataSealer = dataSealer;
+            if (localDataSealer != null && sealState.test(PRC_LOOKUP.apply(messageCtx))) {
+                log.trace("Sealing OAuth state");
+                final String wrapped = localDataSealer.wrap(stateJsonString);
+                final byte[] wrappedAsBytes = wrapped.getBytes(StandardCharsets.UTF_8);
+                if (wrappedAsBytes == null) {
+                    log.error("Unable to seal OAuth state, wrapped bytes are empty");
+                    return null;
+                }
+                // The data sealer base64 encodes the sealed output, we base64 URL encode the base64 encoded output
+                // for transport in the URL.
+                return new StateToken(Base64Support.encodeURLSafe(wrappedAsBytes), stateObject);
+            }
+            log.warn("OAuth state was NOT sealed, either DataSealer is not configured or "
+                    + "sealing predicate returned false. Sealing should be enabled in production");
+           
+            final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharsets.UTF_8);
             assert stateJsonAsBytes != null;
             final String serializedValue = Base64Support.encodeURLSafe(stateJsonAsBytes);
             return new StateToken(serializedValue, stateObject);
-        } catch (final EncodingException e) {
+        } catch (final EncodingException | DataSealerException e) {
             log.error("Unable to generate OAuth state", e);
             return null;
         }
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
index 422e4a5..a3a5aef 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
@@ -28,67 +28,81 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 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.profile.AbstractApplicationAction;
 
 /**
- * An action that validates that the nonce in the OAuth 2.0 state parameter in the response matches that recovered from 
- * the stored nonce. 
+ * An action that validates that the request forgery protection (RFP) nonce in the OAuth 2.0 state parameter in the 
+ * response matches that recovered from the stored RFP nonce in the correlation cookie. 
  */
 public class ValidateResponseState extends AbstractApplicationAction {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateResponseState.class);
     
-    /** Lookup strategy for nonce value stored in the OAuth state parameter. */
-    @NonnullAfterInit private Function<ProfileRequestContext,String> nonceTokenLookupStrategy;
+    /** Lookup strategy for the RFP value stored in the OAuth state parameter. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> rfpTokenFromOAuthStateLookupStrategy;
+    
+    /** 
+     * Lookup strategy to obtain the RFP value stored against the users session in a cookie. This represents
+     * the RFP value that was sent in the initial request.
+     */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> rfpFromCookieLookupStrategy;
     
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
         
-        if (nonceTokenLookupStrategy == null) {
-            throw new ComponentInitializationException("Nonce token lookup strategy cannot be null");
+        if (rfpFromCookieLookupStrategy == null) {
+            throw new ComponentInitializationException("Request forgery token lookup strategy from cookie "
+                    + "cannot be null");
+        }
+        if (rfpTokenFromOAuthStateLookupStrategy == null) {
+            throw new ComponentInitializationException("Request forgery token lookup strategy from OAuth state"
+                    + " cannot be null");
         }
     } 
     
     /**
-     * Sets the lookup strategy to obtain the nonce value stored in the OAuth state
+     * Sets the lookup strategy to obtain the RFP value stored in the OAuth state
      * parameter.
      * 
      * @param strategy
      *            lookup strategy
      */
-    public void setNonceTokenLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
+    public void setRfpTokenFromOAuthStateLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
         checkSetterPreconditions();
-        nonceTokenLookupStrategy = Constraint.isNotNull(strategy,
-                "NonceTokenLookupStrategy can not be null");
+        rfpTokenFromOAuthStateLookupStrategy = Constraint.isNotNull(strategy,
+                "RFP token from oAuth state lookup strategy can not be null");
+    }
+    /**
+     * Set the lookup strategy to obtain the RFP value stored against the users session in a cookie. 
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRfpFromCookieLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        rfpFromCookieLookupStrategy = Constraint.isNotNull(strategy,
+                "RFP from cookie lookup strategy can not be null");
     }
     
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-        // Using this to retrieve the nonce value stored earlier
-        final String nonceFromCorrelationCookie = 
-                agentRequestContext.ensureSubcontext(TokenConsumerContext.class).getMessageCorrelationID();
-        
-        final String nonceFromStateParam = nonceTokenLookupStrategy.apply(profileRequestContext);
-        
-        // We always require state, otherwise we can not recover the values we need
-        if (StringSupport.trimOrNull(nonceFromCorrelationCookie) == null || 
-                StringSupport.trimOrNull(nonceFromStateParam)  == null) {
-            log.error("{} The state parameter was not present in both the request and response, "
+        final String rfpFromCorrelationCookie = rfpFromCookieLookupStrategy.apply(profileRequestContext);        
+        final String rfpFromStateParam = rfpTokenFromOAuthStateLookupStrategy.apply(profileRequestContext);
+
+        if (StringSupport.trimOrNull(rfpFromCorrelationCookie) == null || 
+                StringSupport.trimOrNull(rfpFromStateParam)  == null) {
+            log.error("{} Required OAuth state data was missing from the request or response, "
                     + "state is required",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
             return;
         }
-        assert nonceFromCorrelationCookie != null;
-        if (!nonceFromCorrelationCookie.equals(nonceFromStateParam)) {
-            log.error("{} Request state '{}' did not match response state '{}', has it been tampered with!",
-                    getLogPrefix(), nonceFromCorrelationCookie, nonceFromStateParam);
+        assert rfpFromCorrelationCookie != null;
+        if (!rfpFromCorrelationCookie.equals(rfpFromStateParam)) {
+            log.error("{} Request state did not match response state, has it been tampered with!",
+                    getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
             return;
         } 
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
index 4232079..e99bd2a 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
@@ -92,8 +92,7 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
             }
         });
         
-        action.setErrorFatal(true);
-        action.initialize();
+        action.setErrorFatal(true);        
 
         input = new DDF(null).structure();
         arc.setInput(input);
@@ -111,17 +110,24 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
     @DataProvider
     Object[][] correlationData() {
         return new Object[][] {
-            new Object[] { null, TEST_VALUE},
-            new Object[] { TEST_STATE, null},
+            new Object[] { null, TEST_VALUE},            
             new Object[] { TEST_STATE, TEST_VALUE},
         };
     }
         
     @Test(dataProvider="correlationData")
-    public void testAction(final String state, final String id) {
+    public void testAction(final String state, final String id) throws ComponentInitializationException {
+        action.initialize();
         evaluateAction(state, id);
     }
     
+    @Test
+    public void testAction_NullValue() throws ComponentInitializationException {
+        action.setErrorFatal(false);
+        action.initialize();
+        evaluateAction(TEST_STATE, null);
+    }
+    
     private void evaluateAction(final String state, final String id) {
         stateValue = id;
         if (state != null) {
@@ -151,7 +157,7 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
     
     @Test
     public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
-        
+        action.initialize();
         final List<Cookie> cookies = new ArrayList<>(12);
         for (int i = 0; i < 12; ++i) {
             cookies.add(new Cookie(IssueStateCookie.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
similarity index 96%
rename from sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java
rename to sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
index 0d10a62..756ccee 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
@@ -31,11 +31,11 @@ import net.shibboleth.shared.net.CookieManager;
 import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
 
 /**
- * Tests for {@link ProcessStateCookie}.
+ * Tests for {@link ResolveStateCookie}.
  */
-public class ProcessStateCookieTest extends BaseAgplicationActionTest {
+public class ResolveStateCookieTest extends BaseAgplicationActionTest {
 
-    private ProcessStateCookie action;
+    private ResolveStateCookie action;
     private CookieManager cookieManager;
 
 
@@ -43,7 +43,7 @@ public class ProcessStateCookieTest extends BaseAgplicationActionTest {
     @BeforeMethod
     public void BeforeMethod() throws ComponentInitializationException  {
         super.beforeMethod();
-        action = new ProcessStateCookie();
+        action = new ResolveStateCookie();
         cookieManager = Mockito.mock(CookieManager.class);
         action.setCookieManager(cookieManager);        
     }
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
index 65a3eaa..9a17fcc 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
@@ -18,6 +18,8 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 
+import java.nio.charset.StandardCharsets;
+
 import org.mockito.Mockito;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -46,6 +48,9 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
     /** The serialized form of the authnState.*/
     private String authnStateSerialized;
     
+    /** The serialized form of the authnState as UTF-8 encoded bytes.*/
+    private byte[] authnStateSerializedAsBytes;
+    
     @Override
     @BeforeMethod
     public void beforeMethod() throws ComponentInitializationException {
@@ -63,6 +68,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
         function.setObjectMapper(mapper);
         try {
             authnStateSerialized = mapper.writeValueAsString(authnState);
+            authnStateSerializedAsBytes = authnStateSerialized.getBytes(StandardCharsets.UTF_8);
         } catch (final JsonProcessingException e) {
            throw new ComponentInitializationException(e);
         }       
@@ -73,7 +79,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
     public void testSuccess() throws Exception {
         function.initialize();
         
-        function.apply(prc, authnStateSerialized);
+        function.apply(prc, authnStateSerializedAsBytes);
         
         final var inboundMsgCtx = prc.getInboundMessageContext();
         assertNotNull(inboundMsgCtx);
@@ -101,7 +107,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
         function.setDataSealer(sealer);
         function.initialize();
         
-        function.apply(prc, authnStateSerialized);
+        function.apply(prc, authnStateSerializedAsBytes);
         
         final var inboundMsgCtx = prc.getInboundMessageContext();
         assertNotNull(inboundMsgCtx);
@@ -125,7 +131,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
     public void testFail_BadJSONAuthnState() throws Exception {
         function.initialize();
         
-        function.apply(prc, "bad");
+        function.apply(prc, "bad".getBytes(StandardCharsets.UTF_8));
         
         final var inboundMsgCtx = prc.getInboundMessageContext();
         assertNotNull(inboundMsgCtx);

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


More information about the commits mailing list