[java-plugin-shibd-oidc] branch main updated: JSHIBDOIDC-8 - Add back missing token validation logic

Codeberg noreply at shibboleth.net
Fri Jan 16 14:55:39 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/6dc197b91fef92898d12c128430454cd5cd340d4

The following commit(s) were added to refs/heads/main by this push:
     new 6dc197b  JSHIBDOIDC-8 - Add back missing token validation logic
6dc197b is described below

commit 6dc197b91fef92898d12c128430454cd5cd340d4
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jan 16 14:55:29 2026 +0000

    JSHIBDOIDC-8 - Add back missing token validation logic
    
     - Add AudienceClaimsValidator
     - Add NonceClaimValidator
     - Add AuthenticationTimeClaimValidator
     - Change to new automatic flow detection
     - Add more state to be stored between authentication request and
    authentication response.
     - Improve tests
    
    https://shibboleth.atlassian.net/browse/JSHIBDOIDC-8
---
 pom.xml                                            |   8 +
 sp-oidc-api/pom.xml                                |   6 +
 .../oidc/context/AuthnRequestStateDataContext.java |  50 +++
 ...nticationRequestFromOutboundLookupStrategy.java |  43 +++
 .../navigate/ClientIdBiFunctionLookupStrategy.java |  55 ++++
 .../profile/AuthenticationRequestStateData.java    | 303 ++++++++++++++++++
 .../AuthenticationRequestStateDataTest.java        | 134 ++++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   2 -
 .../sp/consumer/oidc/code/post/post-beans.xml      |  22 --
 .../sp/consumer/oidc/code/query/query-beans.xml    |  22 --
 .../idp/flows/sp/consumer/oidc/oidc-beans.xml      |  95 +++---
 .../idp/flows/sp/consumer/oidc/oidc-flow.xml       |   3 +-
 .../idp/flows/sp/initiator/oidc/oidc-beans.xml     |  28 +-
 .../idp/flows/sp/initiator/oidc/oidc-flow.xml      |   3 +-
 .../shibboleth/idp/flows/sp/oidc-common-beans.xml  |  28 +-
 .../net/shibboleth/sp/service/agent/postconfig.xml |  14 +-
 .../shibboleth/idp/module/conf/sp/oidc.properties  |   9 +
 .../oidc/flows/OIDCSessionInitiatorFlowTest.java   |   1 +
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   | 350 ++++++++++++++++++---
 .../shibboleth/sp/oidc/flows/TestConstants.java    | 114 +++++--
 ...DCEnvironmentApplicationContextInitializer.java |   2 +
 .../shibboleth/idp/module/conf/sp/sp.properties    | 116 +++++--
 sp-oidc-impl/pom.xml                               |  21 ++
 .../impl/AuthTimeRequestedActivationCondition.java |  81 +++++
 ...thenticationRequestStateForStorageStrategy.java | 209 ++++++++++++
 .../AuthenticationRequestTimeLookupFunction.java   |  97 ++++++
 ...AgeFromAuthenticationRequestLookupFunction.java | 117 +++++++
 ...omAuthenticationRequestStateLookupStrategy.java |  87 +++++
 .../impl/PrepareOIDCInboundMessageContext.java     |  23 +-
 .../profile/impl/ProcessCorrelationCookie.java     |  55 +++-
 ...AuthenticationRequestToPeerContextConsumer.java | 170 ++++++++++
 .../impl/SetNonceValueToTokenContextConsumer.java  |   9 +-
 .../AuthTimeRequestedActivationConditionTest.java  | 102 ++++++
 ...ticationRequestStateForStorageStrategyTest.java | 180 +++++++++++
 ...uthenticationRequestTimeLookupFunctionTest.java | 111 +++++++
 ...romAuthenticationRequestLookupFunctionTest.java | 121 +++++++
 ...enticationRequestToPeerContextConsumerTest.java | 139 ++++++++
 37 files changed, 2708 insertions(+), 222 deletions(-)

diff --git a/pom.xml b/pom.xml
index f44c83f..0a20d17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -192,6 +192,14 @@ copyright/license information. -->
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <!-- Jackson BOM -->
+            <dependency>
+                <groupId>com.fasterxml.jackson</groupId>
+                <artifactId>jackson-bom</artifactId>
+                <version>${jackson.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
             <!-- Shibboleth IdP BOM for importing IdP dependencies -->
             <dependency>
                 <groupId>${idp.groupId}</groupId>
diff --git a/sp-oidc-api/pom.xml b/sp-oidc-api/pom.xml
index af71cd6..26e3b02 100644
--- a/sp-oidc-api/pom.xml
+++ b/sp-oidc-api/pom.xml
@@ -79,6 +79,12 @@
         </dependency>
         
         <!-- Test Dependencies -->
+        
+         <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
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
new file mode 100644
index 0000000..9ed6356
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.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.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.
+ */
+public class AuthnRequestStateDataContext extends BaseContext {
+    
+    /** The authentication state data. */
+    @Nullable private AuthenticationRequestStateData authnState;
+    
+    /**
+     * Sets the authentication state recovered from the authentication request.
+     * 
+     * @param authnState The authnState to set.
+     */
+    @Nonnull public AuthnRequestStateDataContext setAuthnState(@Nullable final AuthenticationRequestStateData state) {      
+        authnState = state;
+        return this;
+    }
+    
+    /**
+     * Get the authentication state recovered from the authentication request.
+     * 
+     * @return the authentication state.
+     */
+    @Nullable public AuthenticationRequestStateData getAuthnState() {
+        return authnState;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java
new file mode 100644
index 0000000..4b29a8b
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * 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.navigate;
+
+import java.util.function.Function;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * Lookup strategy that returns the {@link OIDCAuthenticationRequest} from the outbound message context.
+ */
+public class AuthenticationRequestFromOutboundLookupStrategy 
+    implements Function<ProfileRequestContext, OIDCAuthenticationRequest> {
+
+    /** {@inheritDoc} */
+    @Override
+    public OIDCAuthenticationRequest apply(final ProfileRequestContext prc) {
+        final MessageContext outbound = prc.getOutboundMessageContext();
+        if (outbound == null) {
+            return null;
+        }
+        if (outbound.getMessage() instanceof final OIDCAuthenticationRequest request) {
+            return request;
+        }
+        return null;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java
new file mode 100644
index 0000000..2c8138d
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * 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.navigate;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A strategy that adapts a {@link Function} into a {@link BiFunction} for clientId lookup. 
+ */
+ at ThreadSafe
+public class ClientIdBiFunctionLookupStrategy implements BiFunction<ProfileRequestContext, JWTClaimsSet, String>{
+    
+    /** The delegated to clientId lookup strategy.*/
+    @Nonnull private final Function<ProfileRequestContext, String> clientIdLookupStrategy;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the delegated clientIdLookupStrategy
+     */
+    public ClientIdBiFunctionLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        clientIdLookupStrategy = Constraint.isNotNull(strategy, "ClientID Lookup Strategy should not be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String apply(@Nullable final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
+        return clientIdLookupStrategy.apply(prc);
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java
new file mode 100644
index 0000000..a0b3e6e
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * A DTO class that carries authentication request information that needs to be recovered to validate the authentication
+ * response. This class is designed for JSON serialization and deserialization for storage e.g. in a cookie.
+ */
+ at JsonInclude(JsonInclude.Include.NON_NULL)
+ at NotThreadSafe
+public class AuthenticationRequestStateData {
+    
+    /** The identifier of the client that is making the authentication request. */
+    @Nullable private String clientId;
+
+    /** The id_token nonce value. */
+    @Nullable private String nonce;
+
+    /** The expected issuer of the authentication response. */
+    @Nullable private String authenticatingAuthority;
+
+    /** The optional PKCE code verifier. */
+    @Nullable private String pkceCodeVerifier;
+    
+    /** The optional max authentication age. */
+    @Nullable private Duration maxAge;
+    
+    /**
+     * Has auth_time been requested and is required inside the id_token? either because the max_age parameter was used, 
+     * or the claims parameter requested auth_time. 
+     */
+    private boolean authTimeRequired;
+    
+    /** The time at which the RP made the authentication request to the OP.*/
+    @Nullable private Instant authnRequestTime;
+    
+    /**
+     * Set the identifier of the client that is making the authentication request. Can be used to ensure the audience
+     * of the response matches the client that made the request.
+     * 
+     * @param id The client ID to set.
+     * 
+     * @return the updated object
+     */
+    @Nonnull public AuthenticationRequestStateData setClientId(@Nullable final String id) {
+        clientId = id;
+        return this;
+    }
+    
+    /**
+     * Get the identifier of the client that is making the authentication request. Can be used to ensure the audience
+     * of the response matches the client that made the request.
+     * 
+     * @return the client ID.
+     */
+    @Nullable public String getClientId() {
+        return clientId;
+    }
+
+    /**
+     * Get the nonce expected in the id_token response.
+     * 
+     * @return the nonce
+     */
+    @JsonProperty("nonce")
+    @Nullable public String getNonce() {
+        return nonce;
+    }
+
+    /**
+     * Set the nonce expected in the id_token response.
+     * 
+     * @param nonceIn the nonce to set
+     * 
+     * @return the updated object
+     */
+    @Nonnull public AuthenticationRequestStateData setNonce(@Nullable final String nonceIn) {
+        nonce = nonceIn;
+        return this;
+    }
+
+    /**
+     * Get the expected issuer of the authentication response.
+     * 
+     * @return the authentication authority
+     */
+    @JsonProperty("authority")
+    @Nullable public String getAuthenticationAuthority() {
+        return authenticatingAuthority;
+    }
+
+    /**
+     * Set the expected issuer of the authentication response.
+     * 
+     * @param issuer the authentication authority to set
+     * 
+     * @return the updated object
+     */
+    @Nonnull public AuthenticationRequestStateData setAuthenticationAuthority(@Nullable final String authority) {
+        authenticatingAuthority = authority;
+        return this;
+    }
+
+    /**
+     * Get the PKCE code verifier used to generate the code challenge in the authentication request.
+     * 
+     * @return the PKCE code verifier
+     */
+    @JsonProperty("pkce_code_verifier")
+    @Nullable public String getPkceCodeVerifier() {
+        return pkceCodeVerifier;
+    }
+    
+    /**
+     * Set the PKCE code verifier used to generate the code challenge in the authentication request.
+     * 
+     * @param pkceCodeVerifier the PKCE code verifier
+     * 
+     * @return the updated object
+     */
+    @Nonnull public AuthenticationRequestStateData setPkceCodeVerifier(@Nullable final String pkceCodeVerifierIn) {
+        pkceCodeVerifier = pkceCodeVerifierIn;
+        return this;
+    }
+    
+    /**
+     * Set the maxAge, a null value indicates no maxAge was specified in the authentication request.
+     * 
+     * @return the maxAge. 
+     */
+    @JsonProperty("max_age")
+    public Duration getMaxAge() {
+        return maxAge;
+    }
+    
+    /**
+     * set the maxAge, a null value indicates no maxAge was specified in the authentication request.
+     * 
+     * @param age The max_age to set.
+     */
+    @Nonnull public AuthenticationRequestStateData setMaxAge(@Nullable final Duration age) {
+        maxAge = age;
+        return this;
+    }
+    
+    /**
+     * Set if auth_time is required inside the id_token either because the max_age parameter was used, or the claims 
+     * parameter requested it.
+     * 
+     * @param requested is the auth_time requested and hence required inside the id_token.
+     */
+    @Nonnull public AuthenticationRequestStateData setAuthTimeRequired(final boolean requested) {
+        authTimeRequired = requested;
+        return this;
+    }
+    
+    /**
+     * Is auth_time required inside the id_token? either because the max_age parameter was used, or the claims parameter 
+     * requested it.
+     * 
+     * @return true if auth_time has been requested and is required inside the id_token, false otherwise.
+     */
+    @JsonProperty("is_auth_time_required")
+    public boolean isAuthTimeRequired() {
+        return authTimeRequired;
+    }
+    
+    /**
+     * Set the time at which this RP sent this authentication request to the OP.
+     * 
+     * @param time the time the request was made
+     */
+    public AuthenticationRequestStateData setAuthnRequestTime(@Nullable final Instant time) {
+        authnRequestTime = time;
+        return this;
+    }
+    
+    /**
+     * Get the time at which this RP sent this authentication request to the OP.
+     * 
+     * @return the time the request was made
+     */
+    @JsonProperty("authn_request_time")
+    @Nullable public Instant getAuthnRequestTime() {
+        return authnRequestTime;
+    }
+    
+    /**
+     * Create an instance of this class from the given authentication request and authentication authority.
+     * 
+     * @param request the authentication request
+     * @param authenticationAuthority the expected issuer of the authentication response
+     * 
+     * @return a populated instance of this class
+     */
+    public static AuthenticationRequestStateData from(
+            @Nonnull final OIDCAuthenticationRequest request, @Nonnull final String authenticationAuthority) {
+        
+        @SuppressWarnings("null")
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setNonce(request.getNonce() != null ? request.getNonce().getValue() : null)
+                .setAuthenticationAuthority(authenticationAuthority)
+                .setPkceCodeVerifier(request.getCodeVerifier())
+                .setMaxAge(request.getMaxAge())
+                .setAuthnRequestTime(request.getAuthnRequestTime())
+                .setClientId(request.getClientID().getValue());
+
+        state.setAuthTimeRequired(isAuthTimeRequired(request));
+        return state;
+    }
+    
+    /**
+     * Determine if auth_time is required inside the id_token either because the max_age parameter was used, or the 
+     * claims parameter requested it as an ESSENTIAL claim.
+     * 
+     * @param request the authentication request
+     * 
+     * @return true if auth_time is required inside the id_token, false otherwise.
+     */
+    static boolean isAuthTimeRequired(@Nonnull final OIDCAuthenticationRequest request) {
+
+        // If max_age is specified, auth_time MUST be included in the ID Token.
+        if (request.getMaxAge() != null) {
+            return true;
+        }
+
+        final OIDCClaimsRequest requestedClaims = request.getRequestedClaims();
+        if (requestedClaims == null) {
+            return false;
+        }
+
+        final ClaimsSetRequest idTokenClaims = requestedClaims.getIDTokenClaimsRequest();
+        if (idTokenClaims == null) {
+            return false;
+        }
+        // If auth_time is requested as an ESSENTIAL claim, auth_time MUST be included in the ID Token.
+        final Entry authTime = idTokenClaims.get("auth_time");
+        return authTime != null && authTime.getClaimRequirement() == ClaimRequirement.ESSENTIAL;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+
+        final String nonceSuffix = mask(nonce);        
+        String pkceCodeVerifierSuffix = mask(pkceCodeVerifier);
+        pkceCodeVerifierSuffix = pkceCodeVerifierSuffix == null ? "" : pkceCodeVerifierSuffix;
+
+        return "AuthenticationRequestStateData [nonce="+nonceSuffix+", authenticatingAuthority=" + authenticatingAuthority
+                + ", pkceCodeVerifier="+pkceCodeVerifierSuffix+", maxAge=" + maxAge + ", authTimeRequired="
+                + authTimeRequired + ", authnRequestTime=" + authnRequestTime + "]";
+    }
+    
+    /**
+     * A simple method to mask all but the last 2 characters of a string value for logging purposes.
+     * 
+     * @param value the string value to mask
+     * @return the masked string
+     */
+    private static String mask(final String value) {
+        if (value == null) {
+            return null;
+        }
+        if (value.length() <= 4) {
+            return "****";
+        }
+        return "****" + value.substring(value.length() - 2);
+    }
+
+
+
+    
+}
+
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java
new file mode 100644
index 0000000..0a1cd5d
--- /dev/null
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Duration;
+
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * Tests for {@link AuthenticationRequestStateData}.
+ */
+public class AuthenticationRequestStateDataTest {
+    
+    @Test
+    void testAuthTimeNotRequired_whenNoMaxAgeAndNoClaims() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        Mockito.when(request.getMaxAge()).thenReturn(null);
+        Mockito.when(request.getRequestedClaims()).thenReturn(null);
+
+        assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testAuthTimeNotRequired_whenIdTokenClaimsWithoutAuthTime() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        final OIDCClaimsRequest claims = Mockito.mock(OIDCClaimsRequest.class);
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+
+        Mockito.when(request.getMaxAge()).thenReturn(null);
+        Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+        Mockito.when(claims.getIDTokenClaimsRequest()).thenReturn(idTokenClaims);
+
+        assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+
+    
+    @Test
+    void testAuthTimeNotRequired_whenAuthTimeNotEssential() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();        
+        final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+                (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.VOLUNTARY)));
+
+        Mockito.when(request.getMaxAge()).thenReturn(null);
+        Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+        assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testAuthTimeRequired_whenAuthTimeEssential() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);        
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();        
+        final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+                (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.ESSENTIAL)));
+        
+        
+        Mockito.when(request.getMaxAge()).thenReturn(null);
+        Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+        assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testAuthTimeRequired_whenMaxAgePresent() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+
+        Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+        Mockito.when(request.getRequestedClaims()).thenReturn(null);
+
+        assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testAuthTimeRequired_whenMaxAgePresentEvenIfAuthTimeNotEssential() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();        
+        final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+                (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.VOLUNTARY)));
+
+        Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+        Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+        assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testAuthTimeRequired_whenMaxAgeAndAuthTimeEssential() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();        
+        final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+                (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.ESSENTIAL)));
+
+        Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+        Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+        assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+    }
+    
+    @Test
+    void testFromCopiesAuthTimeRequiredCorrectly() {
+        final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+        Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+
+        final AuthenticationRequestStateData state =
+                AuthenticationRequestStateData.from(request, "https://op.example.org");
+
+        assertTrue(state.isAuthTimeRequired());
+    }
+
+}
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index ef54bda..6a74492 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -131,7 +131,5 @@
         </constructor-arg>
     </bean>   
     
-    <!-- TODO Does this need to load in the global context? -->
-    <bean id="shibboleth.sp.oidc.DefaultJSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
 
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
index 2010c5b..e5743d9 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
@@ -6,28 +6,6 @@
                            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">
-
- <!--    <alias alias="shibboleth.HttpServletRequestValidator" name="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Artifact" /> -->
-
-<!--     <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
-            p:messageType="org.opensaml.saml.saml2.core.Response">
-        <constructor-arg>
-            <bean class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPArtifactDecoder" scope="prototype"
-                p:checkDuringInit="false"
-                p:parserPool-ref="shibboleth.ParserPool"
-                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
-                p:bindingDescriptor-ref="shibboleth.Binding.SAML2Artifact"
-                p:peerEntityRole="#{T(org.opensaml.saml.saml2.metadata.IDPSSODescriptor).DEFAULT_ELEMENT_NAME}"
-                p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver"
-                p:identifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy"
-                p:selfEntityIDResolver-ref="ArtifactSupport.SelfEntityIDResolver"
-                p:artifactEndpointResolver-ref="shibboleth.EndpointResolver"
-                p:SOAPClient-ref="shibboleth.SOAPClient.SAML"
-                p:SOAPPipelineName="SP.SAML2.ArtifactDecoder"
-                p:SOAPClientSecurityConfigurationProfileId="#{T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}"
-                />
-        </constructor-arg>
-    </bean> -->
     
     <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
             p:messageType="org.opensaml.saml.saml2.core.Response">
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 3a15b80..3a1ba1d 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
@@ -6,28 +6,6 @@
                            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">
-
- <!--    <alias alias="shibboleth.HttpServletRequestValidator" name="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Artifact" /> -->
-
-<!--     <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
-            p:messageType="org.opensaml.saml.saml2.core.Response">
-        <constructor-arg>
-            <bean class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPArtifactDecoder" scope="prototype"
-                p:checkDuringInit="false"
-                p:parserPool-ref="shibboleth.ParserPool"
-                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
-                p:bindingDescriptor-ref="shibboleth.Binding.SAML2Artifact"
-                p:peerEntityRole="#{T(org.opensaml.saml.saml2.metadata.IDPSSODescriptor).DEFAULT_ELEMENT_NAME}"
-                p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver"
-                p:identifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy"
-                p:selfEntityIDResolver-ref="ArtifactSupport.SelfEntityIDResolver"
-                p:artifactEndpointResolver-ref="shibboleth.EndpointResolver"
-                p:SOAPClient-ref="shibboleth.SOAPClient.SAML"
-                p:SOAPPipelineName="SP.SAML2.ArtifactDecoder"
-                p:SOAPClientSecurityConfigurationProfileId="#{T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}"
-                />
-        </constructor-arg>
-    </bean> -->
     
     <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
             p:messageType="com.nimbusds.openid.connect.sdk.AuthenticationResponse">
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 f1de866..7cdcd9f 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
@@ -44,21 +44,28 @@
         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>
-        
-    <bean id="ProcessIssuerCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessCorrelationCookie" scope="prototype"
+    
+    <!-- TODO should this be a cookie or go through the state manager-->
+    <bean id="ProcessAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessCorrelationCookie" scope="prototype"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_iss_}"
+        p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_authnstate_}"
         p:createOutputObjects="true"
+        p:errorFatal="true"
         p:stateTokenLookupStrategy-ref="StateFromStateLookup">
         <property name="cookieValueConsumerStrategy">
-			<bean class="net.shibboleth.sp.oidc.profile.impl.SetIssuerToPeerContextConsumer"/>
-		</property>
-	</bean>
+            <bean class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationRequestToPeerContextConsumer"
+                p:objectMapper-ref="shibboleth.JSONObjectMapper"
+                p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+                p:unsealStatePredicate="#{getObject('%{sp.oidc.sealAuthenticationStatePredicate:}') ?: ((%{sp.oidc.sealAuthenticationState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"
+                />
+        </property>
+    </bean>
     
     <bean id="ValidateAuthenticationResponseResult" scope="prototype"
         class="net.shibboleth.sp.oidc.profile.impl.ValidateAuthenticationResponseResult" />
@@ -67,12 +74,6 @@
         class="net.shibboleth.sp.oidc.profile.impl.ValidateResponseState"
         p:nonceTokenLookupStrategy-ref="NonceFromStateLookup" />
         
-    <!-- TODO Copied over -->
-    <bean id="PrepareInboundMessageContext" 
-        class="net.shibboleth.sp.oidc.profile.impl.PrepareOIDCInboundMessageContext" scope="prototype"
-        p:identifierLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
-    </bean>
-        
      <bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
         <constructor-arg name="messageHandler"> <!-- TODO Copied over -->
             <bean class="net.shibboleth.sp.oidc.metadata.impl.OIDCProviderMetadataLookupHandler"
@@ -92,8 +93,7 @@
         
     <bean id="SelectProfileConfiguration"
         class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
-        p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
-        
+        p:profileId-ref="shibboleth.sp.oidc.ProfileId" />        
 
     <bean id="NonceFromStateLookup" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
@@ -118,7 +118,9 @@
                 <property name="handlers">
                     <list>
                         <bean id="InitializeOAuth2ClientAuthenticationContext" scope="prototype"
-                            class="net.shibboleth.oidc.profile.impl.InitializeOAuth2ClientAuthenticationContext"/>
+                            class="net.shibboleth.oidc.profile.impl.InitializeOAuth2ClientAuthenticationContext"
+                            p:OAuth2ClientAuthenticationContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer"/>
+                            
                         <bean id="PopulateJWTClientAuthenticationSignatureSigningParameters" scope="prototype"
                             class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParametersHandler"
                             p:noResultIsError="true"
@@ -144,6 +146,7 @@
                                    </bean>
                             </property> 
                         </bean>
+                        
                          <bean id="InitializeOAuth2ClientAuthenticationMethodHandler" scope="prototype"
                             class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientAuthenticationMethodHandler"
                             p:securityParametersContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext"
@@ -166,7 +169,7 @@
      
     <bean id="DefaultTokenResponseDecoder" scope="prototype"
         class="net.shibboleth.oidc.profile.decoding.impl.AccessTokenResponseDecoder"
-        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}" />
+        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}" />
 
     <bean id="DefaultAuthCodeTokenResponseEncoder" scope="prototype"
         class="net.shibboleth.oidc.profile.encoding.impl.AuthCodeTokenRequestEncoder" 
@@ -261,16 +264,16 @@
     <util:list id="IDTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
         <ref bean="IDTokenRequiredClaimsValidator" />
         <ref bean="IssuerClaimsValidator" />
-        <!-- <ref bean="AudienceClaimsValidator" /> -->
+        <ref bean="AudienceClaimsValidator" />
         <ref bean="AzpClaimRequiredValidator" />
-        <!-- ref bean="AzpClaimsValidator" /> -->
+        <ref bean="AzpClaimsValidator" />
         <ref bean="ExpiryClaimsValidator" />
         <ref bean="IssuedAtClaimsValidator" />
         <ref bean="NotBeforeClaimsValidator" />
-<!--         <ref bean="NonceClaimValidator" /> -->
+        <ref bean="NonceClaimValidator" />
         <ref bean="AtHashValidator"/>
-<!--         <ref bean="AuthenticationTimeClaimValidator"/> -->
-<!--         <ref bean="ACRClaimValidator"/> -->
+        <ref bean="AuthenticationTimeClaimValidator"/>
+        <!-- <ref bean="ACRClaimValidator"/> should we just pass these back to the agent-->
     </util:list>
 
     <bean id="IDTokenRequiredClaimsValidator"
@@ -318,20 +321,20 @@
     <bean id="ManyValuesPredicate"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ManyValuesIntegerComparisonPredicate" />
 
-<!--     <bean id="AzpClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
-        p:claimName="azp" p:valueToMatchLookupStrategy-ref="ClientIDFromOAuth2ClientContextFunction">
+    <bean id="AzpClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="azp" p:valueToMatchLookupStrategy-ref="shibboleth.ClientIdLookup.BiFunction">
         <property name="activationCondition">
             <bean id="AzpClaimExistsCondition"
                 class="net.shibboleth.oidc.security.jwt.claims.impl.ClaimExistsActivationCondition" c:claimToCheck="azp" />
         </property>
-    </bean> -->
+    </bean> 
 
     <bean id="IssuerIDFromOIDCProviderMetadataContextLookupFunction" scope="prototype"
         class="net.shibboleth.oidc.profile.logic.IssuerIDFromOIDCProviderMetadataContextLookupFunction"
         p:oIDCMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromInboundMessageContext" />
 
     <bean id="AudienceClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
-        p:audienceLookupStrategy-ref="ClientIDFromOAuth2ClientContextFunction"
+        p:audienceLookupStrategy-ref="shibboleth.ClientIdLookup.BiFunction"
         p:extraAudienceValidation="true">
         <property name="additionalAudiencesLookupStrategy">
             <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExtraAudiencesLookupStrategy" 
@@ -343,18 +346,18 @@
         class="net.shibboleth.oidc.profile.context.navigate.ClientIDFromOAuth2ClientContextFunction"
         c:oauth2ClientContextLookupStrategy-ref="shibboleth.ChildLookup.OAuth2ClientContextFromInbound" />
 
-<!--     <bean id="NonceClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+     <bean id="NonceClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
         p:claimName="nonce"
         p:valueToMatchLookupStrategy="#{getObject('sp.oidc.jwt.NonceLookupStrategy') ?: 
                                 getObject('DefaultNonceLookupStrategy')}"
         p:activationCondition="#{getObject('sp.oidc.jwt.NonceActivationCondition') ?: 
-                                getObject('DefaultNonceActivationCondition')}" /> -->
+                                getObject('DefaultNonceActivationCondition')}" />
 
-<!--     <bean id="DefaultNonceActivationCondition"
-        class="net.shibboleth.idp.plugin.authn.oidc.rp.security.impl.NonceValidationActivationCondition" /> -->
+     <bean id="DefaultNonceActivationCondition"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.NonceValidationActivationCondition" />
 
-<!--     <bean id="DefaultNonceLookupStrategy"
-        class="net.shibboleth.idp.plugin.authn.oidc.rp.security.impl.AuthenticationRequestNonceClaimLookupStrategy" /> -->
+     <bean id="DefaultNonceLookupStrategy"
+        class="net.shibboleth.sp.oidc.profile.impl.NonceFromAuthenticationRequestStateLookupStrategy" />
 
     <bean id="OIDCMetadataContextChildLookup" class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext) }" />
@@ -375,34 +378,36 @@
     <bean id="DefaultIDTokenJOSEHeaderLookupStrategy" 
         class="net.shibboleth.oidc.profile.context.navigate.IDTokenJOSEHeaderLookupStrategy"/>
     
-<!--     <bean id="AuthenticationTimeClaimValidator" 
+    <bean id="AuthenticationTimeClaimValidator" 
                 class="net.shibboleth.oidc.security.jwt.claims.impl.AuthenticationTimeClaimsValidator"
                 p:authnLifetimeLookupStrategy-ref="MaxAgeLookupFunction"
                 p:authnRequestTimeLookupStrategy-ref="AuthenticationRequestTimeLookupFunction"
                 p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.clockSkew:PT60S}"
                 p:activationCondition="#{getObject('sp.oidc.jwt.AuthTimeActivationCondition') ?: 
-                                getObject('DefaultAuthTimeActivationCondition')}"/> -->
+                                getObject('DefaultAuthTimeActivationCondition')}"/>
                                 
-    <bean id="MaxAgeLookupFunction" scope="prototype"
-        class="net.shibboleth.oidc.security.jwt.claims.impl.MaxAgeLookupFunction"
+   <bean id="MaxAgeLookupFunction" scope="prototype"
+        class="net.shibboleth.sp.oidc.profile.impl.MaxAgeFromAuthenticationRequestLookupFunction"
         c:maxAgeDefault="%{sp.oidc.idtoken.jwt.verifier.authnLifetime:PT60S}"/>
         
-<!--     <bean id="AuthenticationRequestTimeLookupFunction" 
-        class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.AuthenticationRequestTimeLookupFunction"
-        p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S}"/>   -->
+   <bean id="AuthenticationRequestTimeLookupFunction" 
+        class="net.shibboleth.sp.oidc.profile.impl.AuthenticationRequestTimeLookupFunction"
+        p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S}"
+        p:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound"/> 
 
-<!--     <bean id="DefaultAuthTimeActivationCondition" 
-            class="net.shibboleth.oidc.security.jwt.claims.impl.AuthTimeRequestedActivationCondition"
-            c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
+   <bean id="DefaultAuthTimeActivationCondition" 
+        class="net.shibboleth.sp.oidc.profile.impl.AuthTimeRequestedActivationCondition"
+        c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound"/>
 
-<!--     <bean id="ACRClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ACRClaimsValidator"
+ 
+    <!-- <bean id="ACRClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ACRClaimsValidator"
         p:requestedEssentialAcrsClaimLookupStrategy="#{getObject('sp.oidc.jwt.RequestedEssentialAcrsClaimLookupStrategy') ?: 
                                 getObject('DefaultRequestedEssentialAcrsClaimLookupStrategy')}"/>
     
     <bean id="DefaultRequestedEssentialAcrsClaimLookupStrategy" 
                 class="net.shibboleth.oidc.security.jwt.claims.impl.RequestedEssentialACRClaimsLookupStrategy"
-                c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
-    
+                c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> 
+     -->
     <!-- - End ID Token Claims Validation -->
     
     <bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
@@ -417,7 +422,7 @@
 
     <bean id="DefaultUserInfoResponseDecoder" scope="prototype"
         class="net.shibboleth.oidc.profile.decoding.impl.UserInfoResponseDecoder"
-        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}"/>
+        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}"/>
 
     <bean id="DefaultUserInfoRequestEncoder" scope="prototype"
         class="net.shibboleth.oidc.profile.encoding.impl.UserInfoRequestEncoder" 
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 7cc51ec..24eb746 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
@@ -8,7 +8,7 @@
        <evaluate expression="DecodeMessage" />
        <evaluate expression="MapStateTokenToResource" />
        <evaluate expression="ProcessNonceCorrelationCookie" />
-       <evaluate expression="ProcessIssuerCorrelationCookie" />
+       <evaluate expression="ProcessAuthnStateCookie" />
        <evaluate expression="ValidateResponseStateMatchesRequest" />
        <evaluate expression="ValidateAuthenticationResponseResult" />
        
@@ -22,7 +22,6 @@
         <transition on="proceed" to="AuthorizationCodeFlow" />
     </action-state>
     
-    <!-- TODO Single grant type supported for now, but we could switch before this like the RP -->
      <action-state id="AuthorizationCodeFlow">        
         <evaluate expression="InitializeOAuth2ClientAuthenticationContextHandler" />
         <evaluate expression="ExchangeCodeForAccessToken" />
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 0046940..326ac48 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
@@ -22,10 +22,10 @@
         p:requireDiscoveryURL="false"
         p:requireRelyingPartyId="true" />
         
-        <!-- TODO Copied over -->
+    <!-- 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:identifierLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
+        p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
     </bean>
     
     <bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
@@ -59,7 +59,8 @@
         scope="prototype" />
 
     <bean id="InitializeOAuth2ClientContext" scope="prototype"
-        class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"/>
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"
+        p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple"/>
         
     <bean id="InitializeAuthorizationRequest"
         class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype"/>
@@ -335,13 +336,26 @@
             </bean>
         </constructor-arg>
     </bean>
-    
-    <bean id="IssueIssuerCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
+      
+   <!-- Issue a cookie with state information that is needs to be recovered to validate the response -->
+   <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
         p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
-        p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-_shibsp__iss_}'.trim()}"
+        p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-_shibsp__authnstate_}'.trim()}"
         p:createOutputObjects="true"
         p:errorFatal="%{sp.stateToken.errorsFatal:true}" 
-        p:requestIDLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+        p:requestIDLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
+        
+ <!--  <bean id="MapAuthenticationStateToStateToken" class="net.shibboleth.sp.oidc.profile.impl.MapStateToStateToken" scope="prototype"
+        p:createOutputObjects="true"
+        p:errorFatal="%{sp.stateToken.errorsFatal:false}" /> -->
+        
+    <!-- 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="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
         p:createOutputObjects="true"
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 6571ccd..7e75a35 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
@@ -56,7 +56,8 @@
     <action-state id="BuildOutboundMessage">    
         <evaluate expression="HandleOutboundMessage" />
         <evaluate expression="IssueNonceCorrelationCookie" />
-        <evaluate expression="IssueIssuerCorrelationCookie" />
+        <evaluate expression="IssueAuthnStateCookie" />
+        <!-- <evaluate expression="MapAuthenticationStateToStateToken"/> -->
         <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 a3572ff..369b7fc 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
@@ -61,10 +61,24 @@
         c:outputType="#{T(com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata)}"
         c:expression="#input.getProviderInformation()" /> 
         
-    <bean id="shibboleth.ChildLookup.OAuth2ClientContext"
+    <bean id="shibboleth.ChildLookup.AuthnRequestStateDataContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext) }" />
+        
+   <bean id="shibboleth.ChildLookup.OAuth2ClientContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.oidc.profile.context.OAuth2ClientContext) }" />
+        
+   <bean id="shibboleth.AuthenticationRequestLookup.FromOutbound"
+        class="net.shibboleth.sp.oidc.context.navigate.AuthenticationRequestFromOutboundLookupStrategy" />
     
+    <!-- Add BiFunction for use with validators that finds the clientId from the sp.issuer  -->
+    <bean id="shibboleth.ClientIdLookup.BiFunction" class="net.shibboleth.sp.oidc.context.navigate.ClientIdBiFunctionLookupStrategy">
+        <constructor-arg ref="shibboleth.ClientIdLookup.Simple"/>
+    </bean>
+    
+    <!-- An alias to make it clear the clientId is coming from the issuer of the request, lookup -->
+    <alias alias="shibboleth.ClientIdLookup.Simple" name="shibboleth.IssuerLookup.Simple"/>
         
     <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromPeerContext" parent="shibboleth.Functions.Compose">
         <constructor-arg name="g">
@@ -102,7 +116,7 @@
     
     <bean id="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer" parent="shibboleth.Functions.Compose">
        <constructor-arg name="g">
-            <ref bean="shibboleth.ChildLookup.OAuth2ClientAuthenticationContext" />
+            <ref bean="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContext" />
         </constructor-arg>
         <constructor-arg name="f">
             <ref bean="shibboleth.ChildLookup.OIDCPeerEntityContext" />
@@ -153,5 +167,15 @@
             <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
         </constructor-arg>
     </bean>
+    
+    <bean id="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound" 
+            parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.AuthnRequestStateDataContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.MessageContextLookup.Inbound" />
+        </constructor-arg>
+    </bean>
 
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index 82b002b..10868ba 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -17,13 +17,13 @@
     <!-- Import parent beans for metadata-driven configuration approach. -->
 <!--     <import resource="relying-party-mddriven.xml" /> -->
 
-    <!--
-    Default Profile Configurations, also usable as parent beans in overrides.
-    
-    Each profile shows up twice, once as a basic bean and again as a child bean that adds
-    a suite of metadata-driven lookup strategies pre-injected.
-    -->
-    
+    <!-- Auto-wired protocol service support bean for use by parent plugin. -->
+    <bean class="net.shibboleth.sp.BasicProtocolSupportService"
+        p:id="OIDC"
+        p:order="%{sp.oidc.relativeOrder:1}"
+        p:sessionInitiators="oidc"
+        p:tokenConsumers="#{{'oidc/code/query', 'oidc/code/post'}}" />
+
     <!-- Credential bean factories -->
     <bean id="shibboleth.oidc.ClientSecretCredential" abstract="true"
         class="net.shibboleth.oidc.security.credential.BasicClientSecretCredentialFactoryBean" />    
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
index ec57c50..72e7f5d 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
@@ -22,4 +22,13 @@ sp.oidc.signing.rsa.enc.key = %{idp.home}/credentials/sp/sp-encryption-rsa.jwk
 ## TEST ENC FOR NOW from profile config
 #idp.oidc.requestobject.encrypted = true
 
+## TODO check these are sensibly named
 
+#sp.oidc.jwt.verifier.clockSkew = 
+#sp.oidc.idtoken.jwt.verifier.clockSkew:PT60S
+#sp.oidc.idtoken.jwt.verifier.authnLifetime:PT60S
+#sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S
+
+# Each protocol plugin will contain an order priority controlling
+# relative order of use when handling requests, lower is "earlier".
+#sp.oidc.relativeOrder = 1
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 91f4fc0..3ba7456 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
@@ -1,3 +1,4 @@
+
 /*
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
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 85eb332..c20c156 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
@@ -23,9 +23,11 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
@@ -142,138 +144,360 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
         if (httpClient == null) {
             fail("Mocked Http Client could not be found");
         }
+        
     }
     
 
     /**
-     * Test successful flow.
+     * Test successful flow with a signed id_token and a plain user info response.
      * 
-     * @throws IOException 
+     * @throws IOException on error
      */
     @Test
     public void testSuccess_SignedIDToken_PlainUserInfo() throws Exception {
         
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now()), 
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, null), 
                 constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.STATE_STRING);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie")
-            .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
         
-        setApplicationRequest("test-oidc-application-with-ro", input);
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
         final DDF output = assertOutputMessageSuccess(result);
         assert output != null;
-        System.out.println("testSuccess output: " + output.toString());
+        System.out.println("test output: " + output.toString());
         validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
                 TestConstants.RESOURCE_URL);
     }
     
     /**
-     * Test successful flow.
+     * Test successful flow with a signed id_token and a plain user info response.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testSuccess_SignedIDToken_PlainUserInfo_WithMaxAge() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, 
+                Map.of("auth_time",Date.from(Instant.now()))), constructJSONUserInfoResponse());
+
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.STATE_STRING);        
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true)));
+        
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
+                TestConstants.RESOURCE_URL);
+    }
+    
+    /**
+     * Test a failure when max_age was sent in the authentication request but auth_time is missing in the id_token.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testFail_SignedIDToken_PlainUserInfo_WithMaxAge_ButNoAuthTime() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, 
+                null), constructJSONUserInfoResponse());
+
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true)));
+        
+        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);
+    }
+    
+    /**
+     * Test successful flow with a signed and encrypted id_token and a plain user info response.
      * 
-     * @throws IOException 
+     * @throws IOException on error
      */
     @Test
     public void testSuccess_SignedEncryptedIDToken_PlainUserInfo() throws Exception {
         
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSignedAndEncrypted(Instant.now().plusSeconds(3600), Instant.now()), 
-                constructJSONUserInfoResponse());
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSignedAndEncrypted(Instant.now().plusSeconds(3600), 
+                Instant.now(), null, null), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie")
-            .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
         
-        setApplicationRequest("test-oidc-application-with-ro", input);
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
         final DDF output = assertOutputMessageSuccess(result);
         assert output != null;
-        System.out.println("testSuccess output: " + output.toString());
+        System.out.println("test output: " + output.toString());
         validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
                 TestConstants.RESOURCE_URL);
     }
     
     /**
-     * Test successful flow.
+     * Test successful flow with a signed id_token and a JWT user info response.
      * 
-     * @throws IOException 
+     * @throws IOException on error
      */
     @Test
     public void testSuccess_SignedIDToken_JWTUserInfo() throws Exception {
         
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now()), 
-                constructJWTUserInfoResponseSigned());     
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), 
+                null, null), constructJWTUserInfoResponseSigned());     
+
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
+        
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
+                TestConstants.RESOURCE_URL);
+    }
+    
+    /**
+     * Test successful flow, where id_token contains an AZP claim.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testSuccess_SignedIDTokenWithAZP_JWTUserInfo() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), 
+                null, Map.of("azp", TestConstants.CLIENT_ID)), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
         
-        setApplicationRequest("test-oidc-application-with-ro", input);
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
         final DDF output = assertOutputMessageSuccess(result);
         assert output != null;
-        System.out.println("testSuccess output: " + output.toString());
+        System.out.println("test output: " + output.toString());
         validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
                 TestConstants.RESOURCE_URL);
     }
     
     /**
-     * Test bad state.
+     * Test a failure if the id_token contains the wrong AZP value (not the client_id).
      * 
-     * @throws IOException 
+     * @throws IOException on error
      */
     @Test
-    public void testFail_BadState() throws IOException {
+    public void testFail_IDToken_AZPMistmatch() throws Exception {
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), 
+                null, Map.of("azp", "bad-azp")), constructJWTUserInfoResponseSigned());
+        
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
+
+        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);
+    }
+    
+    /**
+     * Test a failure, nonce mismatch in the id_token.
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public void testFail_IDToken_NonceMismatch() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), 
+                Instant.now(), "bad-idtoken-nonce", null), constructJWTUserInfoResponseSigned()); 
+        
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
+
+        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);
+    }
+    
+    /**
+     * Test a failure, nonce mismatch in the id_token.
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    public void testSuccess_IDToken_WithNonce() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), 
+                Instant.now(), TestConstants.ID_TOKEN_NONCE, null), constructJWTUserInfoResponseSigned()); 
+        
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+        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.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
+
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"), 
+                TestConstants.RESOURCE_URL);
+    }
+    
+    /**
+     * Test failure, a bad nonce in the response state.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testFail_BadCorrelationNonceState() throws Exception {
         final AuthenticationSuccessResponse response = 
                 buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie")
-            .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.APPLICATION_ID,
+                "bad-nonce", //this is a bad nonce
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
 
-        setApplicationRequest("test-oidc-application-with-ro", input);
+        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);
     }
+   
     
     /**
      * Test successful flow.
      * 
-     * @throws IOException 
+     * @throws IOException on error
      */
     @Test
-    public void testFail_ErrorResponse() throws IOException {
+    public void testFail_ErrorResponse() throws Exception {
         final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_STRING));
         
         // Add cookies
-        input.addmember("http.headers.Cookie")
-            .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.AUTHENTICATION_STATE, 
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_NONCE,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false)));
 
-        setApplicationRequest("test-oidc-application-with-ro", input);
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, TestConstants.FLOW_ID);
@@ -287,20 +511,35 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
      * 
      * @param expiry expiry time
      * @param issuedAt issue time
+     * @param nonce the nonce
      * @return the tokens
      * @throws ParseException 
      * @throws JOSEException 
      */
     private OIDCTokenResponse constructSuccessfulTokenResponseSigned(
-            @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+            @Nonnull final Instant expiry, @Nonnull final Instant issuedAt, @Nullable final String nonce, 
+            @Nullable final Map<String, Object> additionalClaims) 
+                    throws JOSEException, ParseException {
 
-         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
-                 .subject("jdoe")
-                 .issuer("https://op.example.org")
-                 .audience("mock-client-id")
-                 .expirationTime(Date.from(expiry))
-                     .issueTime(Date.from(issuedAt))
-                     .build();
+         final JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
+                .subject("jdoe")
+                .issuer(TestConstants.ISSUER)
+                .audience(TestConstants.CLIENT_ID)
+                .expirationTime(Date.from(expiry))
+                    .issueTime(Date.from(issuedAt));
+        
+         if (nonce != null) {
+             claimsSetBuilder.claim("nonce", nonce);
+         }
+         if (additionalClaims != null) {
+             additionalClaims.entrySet()
+                 .stream()
+                 .forEach(entry -> claimsSetBuilder.claim(entry.getKey(), entry.getValue()));
+         }
+         
+         final JWTClaimsSet claimsSet = claimsSetBuilder.build();
+
+         
          final JWT signedIdToken = 
                  TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
          
@@ -314,20 +553,33 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
      * 
      * @param expiry expiry time
      * @param issuedAt issue time
+     * @param nonce the nonce
      * @return the tokens
      * @throws ParseException 
      * @throws JOSEException 
      */
     private OIDCTokenResponse constructSuccessfulTokenResponseSignedAndEncrypted(
-            @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+            @Nonnull final Instant expiry, @Nonnull final Instant issuedAt, @Nullable final String nonce,
+            @Nullable final Map<String, Object> additionalClaims) 
+                    throws JOSEException, ParseException {
 
-         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+         final JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
                  .subject("jdoe")
-                 .issuer("https://op.example.org")
-                 .audience("mock-client-id")
+                 .issuer(TestConstants.ISSUER)
+                 .audience(TestConstants.CLIENT_ID)
                  .expirationTime(Date.from(expiry))
-                     .issueTime(Date.from(issuedAt))
-                     .build();
+                     .issueTime(Date.from(issuedAt));
+         
+         if (nonce != null) {
+             claimsSetBuilder.claim("nonce", nonce);
+         }
+         if (additionalClaims != null) {
+             additionalClaims.entrySet()
+                 .stream()
+                 .forEach(entry -> claimsSetBuilder.claim(entry.getKey(), entry.getValue()));
+         }
+         final JWTClaimsSet claimsSet = claimsSetBuilder.build();
+         
          final JWT encryptedIdToken = 
                  TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, JWEAlgorithm.RSA_OAEP_256, 
                          EncryptionMethod.A128CBC_HS256, opSigningCredential, rpEncryptionCredential);
@@ -359,7 +611,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
     private UserInfoSuccessResponse constructJWTUserInfoResponseSigned() throws JOSEException, ParseException {
         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                 .subject("jdoe")
-                .issuer("https://op.example.org")
+                .issuer(TestConstants.ISSUER)
                 .audience("mock-client-id")
                 .claim("name", "John Doe").build();
        
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 3dd5dd7..aaa9845 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
@@ -14,8 +14,28 @@
 
 package net.shibboleth.sp.oidc.flows;
 
+import static org.testng.Assert.fail;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+
 import javax.annotation.Nonnull;
 
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+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 net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
 /** A class to hold constants used for tests.*/
 public final class TestConstants {
 
@@ -23,6 +43,9 @@ public final class TestConstants {
     private TestConstants() {
         
     }
+    
+    /** The client_id to use, must match the issuer in oidc-test-agents.xml.*/
+    @Nonnull @NotEmpty public static final String CLIENT_ID = "testsp.example.org";
 
     /** Flow ID. */
     @Nonnull public static final String FLOW_ID = "sp/token-consumer";
@@ -38,40 +61,91 @@ 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 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";
+
+    /** Application ID used in cookie names. */
+    public static final String APPLICATION_ID = "test-oidc-application-with-ro";
+
+    /** 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: 
      * 
-     * <code>
+     * <pre>
      * {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
      * "nonce":"f26d922221226cd183729bf13247dbe2"}
-     * </code>
+     * </pre>
      * */
     public static final String STATE_STRING = """
         eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
         """;
 
     /**
-     * Cookie headers representing the state string {@literal STATE_STRING}:
-     * 
+     * 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_iss_{state}={issuer};
+     * __Host-shibsp_state__{appId}_{state}={targetURL};
+     * __Host-_shibsp_authnstate_{state}={authn_request_json};
+     * </pre>
      * 
+     * @param state the state used for mapping response to the stored headers
+     * @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 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 final String COOKIE_HEADER = """
-        __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; 
-        _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; 
-        __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
-        """;
+    public static byte[] buildCookieHeader(final String state, final String appID, final String nonce, 
+            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);
+
+        // 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)
+          .append('=').append(targetUrlB64).append("; \n");
+        sb.append("__Host-_shibsp_authnstate_").append(state).append('=').append(authnJsonEnc).append(";\n");
+        System.out.println(sb.toString());
+        return sb.toString().getBytes("UTF-8");
+        
+    }
     
-    /**
-     * Cookie headers representing a bad state string where the nonce is incorrect, as recovered from 
-     * the __Host-_shibsp_req cookie.
-     */
-    public static final String COOKIE_HEADER_BAD_STATE = """
-       __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=222cbf13247dbe2;
-       _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; 
-       __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
-        """;
+    public static String buildAuthenticationState(final Duration maxAge, final boolean authTimeRequired) {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setClientId(APPLICATION_ID)
+                .setNonce(ID_TOKEN_NONCE)
+                .setAuthenticationAuthority(ISSUER)
+                .setMaxAge(maxAge)
+                .setAuthTimeRequired(authTimeRequired)
+                .setAuthnRequestTime(Instant.now());
+        
+        final ObjectMapper jsonMapper = JsonMapper.builder()
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                .serializationInclusion(Include.NON_ABSENT)
+                .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+                .addModule(new Jdk8Module())
+                .addModule(new JavaTimeModule())
+                .build();
+        
+        try {
+            return jsonMapper.writeValueAsString(state);
+        } catch (final JsonProcessingException e) {
+            fail(e.getMessage());
+        }
+        return "";        
+    }
 }
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 b595e57..c7b6512 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
@@ -47,6 +47,8 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
         mock.setProperty("sp.agent.authn.method", "basic");
         // Resolve attributes, to test attribute resolution
         mock.setProperty("sp.oidc.resolveAttributes", "true");
+        // Turn off the sealing of authentication request state for tests
+        mock.setProperty("sp.oidc.sealAuthenticationState", "false");
         mock.setProperty("idp.additionalProperties",
                 "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/oidc.properties,  /conf/sp/oidc-test.properties");
         applicationContext.getEnvironment().getPropertySources().addFirst(mock);
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 83ae124..0859911 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -10,23 +10,6 @@ sp.defaultAuthority = https://op.example.org
 #sp.service.agents.failFast = false
 sp.service.agents.checkInterval = PT5M
 
-# Default agent IP address filter
-#sp.agent.authn.allowedAddressRanges = 127.0.0.1/32, ::1/128
-# Set to basic for shared secret authentication
-#sp.agent.authn.method =
-# Set false to globally disable cookie-based authentication by agents
-#sp.agent.authn.cached = true
-#sp.agent.authn.cacheDuration = PT1H
-
-# Set to StorageService to use for remoted storage data if in use.
-#sp.storageService = shibboleth.StorageService
-# Set to DataSealer to use for remoted data encryption.
-#sp.dataSealer = shibboleth.DataSealer
-
-# Default precedence/set of Session Initiator and Token Consumer flows to attempt
-sp.application.sessionInitiators = oidc
-sp.application.tokenConsumers = oidc/code/query, oidc/code/post
-
 # General SP cookie properties
 #sp.cookie.secure = true
 #sp.cookie.httpOnly = true
@@ -39,6 +22,7 @@ sp.application.tokenConsumers = oidc/code/query, oidc/code/post
 #sp.cookie.limit = 10
 
 # Default state token management (SAML RelayState, etc.)
+#sp.stateToken.errorsFatal = false
 # Set to shibboleth.sp.CookieStateTokenManager to switch to cookie-based mechanism
 #sp.stateToken.Manager = shibboleth.sp.StorageStateTokenManager
 # Controls storage back-end for storage-based state tokens
@@ -47,8 +31,102 @@ sp.application.tokenConsumers = oidc/code/query, oidc/code/post
 #sp.stateToken.cookiePrefix = __Host-shibsp_state_
 
 # Request/response correlation control
-#sp.correlation.cookiePrefix = __Host-_shibsp_req_
+#sp.correlation.cookiePrefix = __Host-shibsp_req_
+
+# POST data preservation controls
+#sp.postData.preservation = false
+#sp.postData.limit = 1048576
+#sp.postData.lifetime = PT5M
+#sp.postData.cookiePrefix = __Host-shibsp_post_
+#sp.postData.StorageService = shibboleth.StorageService
+#sp.postData.errorsFatal = false
 
 # Uncomment/set to define a default IdP discovery service URL or Function
-#sp.discoveryURL = https://test.shibboleth.net/shibboleth-ds/index.html
+#sp.discoveryURL =
 #sp.discoveryURLFunction =
+
+# Settings for Hub-mediated session storage
+#sp.session.storageService =
+#sp.session.maxStorageTimeout = P1D
+
+###############################
+# Agent Authentication Settings
+###############################
+
+# Default agent IP address filter
+#sp.agent.authn.allowedAddressRanges = 127.0.0.1/32, ::1/128
+# Set to basic for shared secret authentication
+#sp.agent.authn.method =
+# Set false to globally disable cookie-based authentication by agents
+#sp.agent.authn.cached = true
+#sp.agent.authn.cacheDuration = PT1H
+
+
+# Properties exist for optional "enterprise" agent secret validation. Most of them
+# are for LDAP, replicating the large range of properties available in the IdP "proper"
+# but kept separate for flexibility. They are "chained" to the IdP properties in most
+# cases for convenience but can be overridden if necessary. 
+
+#sp.agent.authn.JAAS.loginConfigNames                = ShibSPAgentAuth
+#sp.agent.authn.JAAS.loginConfig                     = %{idp.home}/conf/sp/jaas.config
+
+#sp.agent.authn.Krb5.servicePrincipal                =
+#sp.agent.authn.Krb5.keytab                          =
+
+## Authenticator strategy,
+#  either anonSearchAuthenticator, bindSearchAuthenticator, directAuthenticator, adAuthenticator
+#sp.agent.authn.LDAP.authenticator                   = %{idp.authn.LDAP.authenticator:anonSearchAuthenticator}
+
+## Connection properties ##
+#sp.agent.authn.LDAP.ldapURL                         = %{idp.authn.LDAP.ldapURL:ldap://localhost:10389}
+#sp.agent.authn.LDAP.useStartTLS                     = %{idp.authn.LDAP.useStartTLS:true}
+# Time to wait for startTLS responses
+#sp.agent.authn.LDAP.startTLSTimeout                 = %{idp.authn.LDAP.startTLSTimeout:PT3S}
+# Time to wait for connections to open
+#sp.agent.authn.LDAP.connectTimeout                  = %{idp.authn.LDAP.connectTimeout:PT3S}
+# Time to wait for operation responses (e.g. search, bind)
+#sp.agent.authn.LDAP.responseTimeout                 = %{idp.authn.LDAP.responseTimeout:PT3S}
+# Connection strategy to use when multiple URLs are supplied, either ACTIVE_PASSIVE, ROUND_ROBIN, RANDOM
+#sp.agent.authn.LDAP.connectionStrategy              = %{idp.authn.LDAP.connectionStrategy:ACTIVE_PASSIVE}
+
+## SSL configuration, either jvmTrust, certificateTrust, or keyStoreTrust
+#sp.agent.authn.LDAP.sslConfig                       = %{idp.authn.LDAP.sslConfig:certificateTrust}
+## If using certificateTrust above, set to the trusted certificate's path
+#sp.agent.authn.LDAP.trustCertificates               = %{idp.authn.LDAP.trustCertificates:%{idp.home}/credentials/ldap-server.crt}
+## If using keyStoreTrust above, set to the truststore path
+#sp.agent.authn.LDAP.trustStore                      = %{idp.authn.LDAP.trustStore:%{idp.home}/credentials/ldap-server.truststore}
+
+## Return attributes during authentication
+# Setting this property will cause entry resolution to occur as part of authentication
+# Note that this property is not compatible with the adAuthenticator
+#sp.agent.authn.LDAP.returnAttributes                = %{idp.authn.LDAP.returnAttributes:1.1}
+
+## DN resolution properties ##
+
+# Search DN resolution, used by anonSearchAuthenticator, bindSearchAuthenticator
+# for AD: CN=Users,DC=example,DC=org
+#sp.agent.authn.LDAP.baseDN                          = %{idp.authn.LDAP.baseDN:ou=people,dc=example,dc=org}
+#sp.agent.authn.LDAP.subtreeSearch                   = %{idp.authn.LDAP.subtreeSearch:false}
+#sp.agent.authn.LDAP.userFilter                      = %{idp.authn.LDAP.userFilter:undefined}
+# bind search configuration
+# for AD: sp.agent.authn.LDAP.bindDN=adminuser at domain.com
+#sp.agent.authn.LDAP.bindDN                          = %{idp.authn.LDAP.bindDN:uid=myservice,ou=system}
+
+# Format DN resolution, used by directAuthenticator, adAuthenticator
+# for AD use sp.agent.authn.LDAP.dnFormat=%s at domain.com
+#sp.agent.authn.LDAP.dnFormat                        = %{idp.authn.LDAP.dnFormat:uid=%s,ou=people,dc=example,dc=org}
+
+# pool passivator, either none, bind or anonymousBind
+#sp.agent.authn.LDAP.bindPoolPassivator              = %{idp.authn.LDAP.bindPoolPassivator:none}
+
+# LDAP pool configuration, used for both authn and DN resolution
+#sp.agent.authn.pool.LDAP.minSize                    = %{idp.authn.pool.LDAP.minSize:3}
+#sp.agent.authn.pool.LDAP.maxSize                    = %{idp.authn.pool.LDAP.maxSize:10}
+#sp.agent.authn.pool.LDAP.validateOnCheckout         = %{idp.authn.pool.LDAP.validateOnCheckout:false}
+#sp.agent.authn.pool.LDAP.validatePeriodically       = %{idp.authn.pool.LDAP.validatePeriodically:true}
+#sp.agent.authn.pool.LDAP.validatePeriod             = %{idp.authn.pool.LDAP.validatePeriod:PT5M}
+#sp.agent.authn.pool.LDAP.validateDN                 = %{idp.authn.pool.LDAP.validateDN:}
+#sp.agent.authn.pool.LDAP.validateFilter             = %{idp.authn.pool.LDAP.validateFilter:(objectClass=*)}
+#sp.agent.authn.pool.LDAP.prunePeriod                = %{idp.authn.pool.LDAP.prunePeriod:PT5M}
+#sp.agent.authn.pool.LDAP.idleTime                   = %{idp.authn.pool.LDAP.idleTime:PT10M}
+#sp.agent.authn.pool.LDAP.blockWaitTime              = %{idp.authn.pool.LDAP.blockWaitTime:PT3S}
diff --git a/sp-oidc-impl/pom.xml b/sp-oidc-impl/pom.xml
index 99a0102..bf65051 100644
--- a/sp-oidc-impl/pom.xml
+++ b/sp-oidc-impl/pom.xml
@@ -146,6 +146,27 @@
             <groupId>jakarta.servlet</groupId>
             <artifactId>jakarta.servlet-api</artifactId>
             <scope>provided</scope>
+        </dependency>        
+        
+         <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+         <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jdk8</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <scope>provided</scope>
         </dependency>
         
         <!-- Test Dependencies -->
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java
new file mode 100644
index 0000000..88a6b6e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java
@@ -0,0 +1,81 @@
+/*
+ * 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.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+
+/**
+ * Has the auth_time claim been requested? This is taken from the authentication request state. A failure is treated
+ * as a request for auth_time to be checked (for a better security posture).
+ */
+public final class AuthTimeRequestedActivationCondition implements BiPredicate<ProfileRequestContext, JWTClaimsSet> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AuthTimeRequestedActivationCondition.class);
+    
+    /** Strategy to find the authentication request.*/
+    @Nonnull private final 
+    Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+
+    /**
+     * Constructor.
+     *
+     * @param strategy strategy to find the authentication request
+     */
+    public AuthTimeRequestedActivationCondition(@ParameterName(name = "authenticationRequestLookupStrategy")
+            @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+        authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+                "authenticationRequestLookupStrategy can not be null");
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * <p>The claims parameter is ignored.</p>
+     */
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext prc, @Nullable final JWTClaimsSet claims) {
+        
+        final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(prc);
+        final var localAuthnRequest = authnRequestCtx != null ? authnRequestCtx.getAuthnState() : null;
+        if (authnRequestCtx == null || localAuthnRequest == null) {
+            // Return true, because if we are not sure, we should default to testing the auth_time as a better security 
+            // posture
+            log.trace("Authentication request state not found in profile context, is the "
+                    + "authenticationRequestLookupStrategy configured correctly? 'auth_time' will now be checked"); 
+            return true;
+        }
+        final boolean authTimeWasRequested = localAuthnRequest.isAuthTimeRequired();
+        log.trace("Authentication time (auth_time) {} request during authentication", 
+                authTimeWasRequested ? "was" : "was not");
+        return authTimeWasRequested;
+       
+    }
+    
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java
new file mode 100644
index 0000000..826d115
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java
@@ -0,0 +1,209 @@
+/*
+ * 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.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.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.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.profile.AuthenticationRequestStateData;
+
+/**
+ *  A {@link Function} that produces a serialized JSON string representing authentication request state that is required
+ *  for validating the authentication response. This can be stored in a way that can be recovered later when the 
+ *  response is received.
+ *  
+ *  <p>Importantly, if an authentication authority (relying party) is not available, or the authentication request 
+ *  is not available, null will be returned. This will ultimately prevent the authentication response from being
+ *  validated correctly, and the calling action may wish to terminate the authentication request before a response
+ *  is made.</p>
+ */
+public class AuthenticationRequestStateForStorageStrategy extends AbstractIdentifiableInitializableComponent 
+    implements Function<ProfileRequestContext,String> {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(AuthenticationRequestStateForStorageStrategy.class);
+    
+    /** Strategy to locate authentication authority, that is the issuer of the eventual authentication response. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> authenticationAuthorityLookupStrategy;
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Strategy to locate the authentication request. */
+    @NonnullAfterInit private 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.*/
+    public AuthenticationRequestStateForStorageStrategy() {
+        sealState = PredicateSupport.alwaysTrue();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (authenticationAuthorityLookupStrategy == null) {
+            throw new ComponentInitializationException("Authentication authority lookup strategy cannot be null");
+        }
+        if (authenticationRequestLookupStrategy == null) {
+            throw new ComponentInitializationException("Authentication request lookup strategy can not be null");
+        }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot 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 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();
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper} to use for serialization.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+    
+    /**
+     * Set the authentication request lookup strategy to use.
+     * 
+     * @param strategy the strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationRequest> strategy) {
+        checkSetterPreconditions();
+        authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+                "AuthenticationRequestLookupStrategy can not be null");
+    }
+    
+    /**
+     * Set the authentication authority lookup strategy to use.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setAuthenticationAuthorityLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        authenticationAuthorityLookupStrategy = Constraint.isNotNull(strategy, 
+                "Authentication Authority Lookup Strategy can not be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext prc) {
+        checkComponentActive();
+        
+
+        if (prc == null) {
+            log.debug("{}: ProfileRequestContext is null, response correlation will fail", getId());
+            return null;
+        }
+        
+        // Find the relying party or authenticating authority this request has been sent to.
+        final String authenticationAuthority = authenticationAuthorityLookupStrategy.apply(prc);
+        // Locate the authentication request to pull out information for saving off.
+        final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(prc);
+        
+        if (authenticationAuthority == null) {
+            log.debug("{}: Authentication authority is null, response correlation will fail", getId());
+            return null;
+        }        
+        if (authnRequest == null) {
+            log.debug("{}: Authentication request is null, response correlation will fail", getId());
+            return null;
+        }
+        // Construct a DTO for saving off the authentication request state
+        final AuthenticationRequestStateData state = 
+                AuthenticationRequestStateData.from(authnRequest, authenticationAuthority);
+        log.debug("{}: Created authentication request state data: {}", getId(), state);
+        
+        try {
+            // Serialize the authentication request state
+            final String serializedState = objectMapper.writeValueAsString(state);
+            if (serializedState == null) {
+                return null;
+            }
+            final DataSealer localDataSealer = dataSealer;
+            if (localDataSealer != null && sealState.test(prc)) {
+                return localDataSealer.wrap(serializedState);
+            } else {
+                return serializedState;
+            }
+        } catch (final JsonProcessingException | DataSealerException e) {
+            log.error("{}: Issue generating authentication request state",getId(),e);
+        }
+        return null;
+    }
+
+}
+
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java
new file mode 100644
index 0000000..15c9fe4
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java
@@ -0,0 +1,97 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/** 
+ * Lookup function that returns the time at which the RP sent the authentication request, as taken from the 
+ * authentication request state object recovered when the response was obtained from the agent. 
+ * 
+ * <p>If the time can not be found, <code>null</code> is returned.</p>
+ */
+ at NotThreadSafe
+public class AuthenticationRequestTimeLookupFunction implements Function<ProfileRequestContext, Instant> {
+    
+    /** 
+     * Strategy used to locate the {@link AuthnRequestStateDataContext}.
+     */
+    @Nonnull private Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+    
+    /** 
+     * A clock skew to apply to the authentication request time. This accounts for differences between system clocks by
+     * treating the authentication request as if it occurred slightly earlier by the specified skew duration. By default
+     * there is no skew.
+     */
+    @Nonnull private Duration clockSkew;
+    
+    /** Constructor.*/
+    public AuthenticationRequestTimeLookupFunction() {
+        authenticationRequestLookupStrategy = prc -> {
+            final var inboundMsgCtx = prc.getInboundMessageContext();
+            if (inboundMsgCtx == null) {
+                return null;
+            }
+            return inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+        };   
+        final Duration ofSeconds = Duration.ofSeconds(0);
+        assert ofSeconds != null;
+        clockSkew = ofSeconds;
+    }
+    
+    /**
+     * Set the clock skew.
+     * 
+     * @param skew clock skew to set
+     */
+    public void setClockSkew(@Nonnull final Duration skew) {          
+        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link AuthenticationRequestStateData} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+        authenticationRequestLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+    }
+
+    @Override
+    @Nullable public Instant apply(@Nullable final ProfileRequestContext input) {
+        if (input == null) {
+            return null;
+        }
+        final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(input);
+        final AuthenticationRequestStateData authnRequest = authnRequestCtx != null  ? authnRequestCtx.getAuthnState() : null;
+        final Instant authTime = authnRequest != null ? authnRequest.getAuthnRequestTime() : null;
+        return authTime == null ? null : authTime.minus(clockSkew);
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java
new file mode 100644
index 0000000..cf675d1
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java
@@ -0,0 +1,117 @@
+/*
+ * 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.time.Duration;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/** 
+ * Locate the maximum authentication age from the authentication request. If not present in the request, it is taken
+ * from the profile configuration associated with the relying party context. Returning a default value if neither are 
+ * found.
+ */
+ at NotThreadSafe
+public class MaxAgeFromAuthenticationRequestLookupFunction extends AbstractRelyingPartyLookupFunction<Duration> {
+    
+    /** Default max authentication age if none can be found on the profile context.*/
+    @Nonnull private final Duration maxAgeDefault;
+    
+    /** 
+     * Strategy used to locate the {@link AuthnRequestStateDataContext}. Defaults to under the inbound message context.
+     */
+    @Nonnull private Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+    
+    /**
+     * Constructor.
+     *
+     * @param defaultAge the default value to use for maximum authentication age
+     */
+    public MaxAgeFromAuthenticationRequestLookupFunction(
+            @ParameterName(name = "maxAgeDefault") @Nonnull final Duration defaultAge) {
+        maxAgeDefault = Constraint.isNotNull(defaultAge, "Max Age default can not be null");
+        
+        authenticationRequestLookupStrategy = prc -> {
+            final var inboundMsgCtx = prc.getInboundMessageContext();
+            if (inboundMsgCtx == null) {
+                return null;
+            }
+            return inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+        };  
+    }
+    
+    /**
+     * Set the authentication request lookup strategy to use.
+     * 
+     * @param strategy the strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+        authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+                "AuthenticationRequestLookupStrategy can not be null");
+    }
+
+    @Override
+    @Nonnull
+    public Duration apply(@Nullable final ProfileRequestContext input) {
+        
+        if (input == null) {
+            return maxAgeDefault;
+        }
+        
+        // Max_age from authentication request is authoritative over that from the profile config.
+        
+        final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(input);
+        final AuthenticationRequestStateData authnRequest = 
+                authnRequestCtx != null  ? authnRequestCtx.getAuthnState() : null;
+        if (authnRequest != null) {
+            final Duration authnRequestMaxAge = authnRequest.getMaxAge();
+            if (authnRequestMaxAge != null) {
+                return authnRequestMaxAge;
+            }
+        }
+        
+        // Check one was not specified in the relying party context
+        
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof final OIDCAuthenticationProfileConfiguration config){
+                   final Duration maxAge = config.getMaxAuthenticationAge(input);
+                   if (maxAge == null) {
+                       return maxAgeDefault;
+                   } else {
+                       return maxAge;
+                   }
+            }
+        }
+        return maxAgeDefault;
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java
new file mode 100644
index 0000000..a55639e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java
@@ -0,0 +1,87 @@
+/*
+ * 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 java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/** 
+ * Lookup strategy that finds the id_token nonce from the {@link AuthenticationRequestStateData}. 
+ * Returns {@code null} if not found.
+ */
+ at ThreadSafe
+public final class NonceFromAuthenticationRequestStateLookupStrategy 
+                        implements BiFunction<ProfileRequestContext,JWTClaimsSet, String> {
+    
+    /** The strategy used to lookup the {@link AuthenticationRequestStateData}.*/
+    @Nonnull 
+    private final Function<ProfileRequestContext, AuthenticationRequestStateData> authenticationStateDataLookupStrategy; 
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the lookup strategy to use
+     */
+    public NonceFromAuthenticationRequestStateLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AuthenticationRequestStateData> strategy) {
+        authenticationStateDataLookupStrategy = 
+                Constraint.isNotNull(strategy, "Authentication state data lookup strategy can not be null");
+    }
+    
+    /** Constructor.*/
+    public NonceFromAuthenticationRequestStateLookupStrategy() {
+        authenticationStateDataLookupStrategy = prc -> {
+            if (prc == null) {
+                return null;
+            }
+            final MessageContext messageContext = prc.getInboundMessageContext();
+            if (messageContext == null) {
+                return null;
+            }
+            final AuthnRequestStateDataContext context = messageContext.getSubcontext(AuthnRequestStateDataContext.class);
+            if (context != null) {
+                return context.getAuthnState();
+            }
+            return null;
+        };
+    }
+
+    @Override @Nullable public String apply(@Nullable final ProfileRequestContext context,
+            @Nullable final JWTClaimsSet claims) {
+        
+        final AuthenticationRequestStateData state = authenticationStateDataLookupStrategy.apply(context);
+        final String nonce = state != null ? state.getNonce() : null;
+        if (StringSupport.trimOrNull(nonce) == null) {
+            return null;
+        }
+        return nonce;
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
index 3f92aa5..bfcff9c 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
@@ -45,16 +45,17 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link EventIds#INVALID_PROFILE_CTX}
  * @post Add an {@link OIDCPeerEntityContext} to the inbound message context.
  */
+// TODO modified fromn the RP version and more appropriate use of relying party id lookup over 'identifier'
 public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(PrepareOIDCInboundMessageContext.class);
     
     /** Strategy to lookup the identifier of the OP/RP to base the inbound context on. */
-    @NonnullAfterInit private Function<ProfileRequestContext,String> identifierLookupStrategy;
+    @NonnullAfterInit private Function<ProfileRequestContext,String> relyingPartyLookupStrategy;
     
     /** The identifier of the OP/RP to base the inbound context on. */
-    @Nullable private String identifier;
+    @Nullable private String relyingPartyId;
     
     /** 
      * Should the peer entity context be added to an existing inbound message context or not.
@@ -72,8 +73,8 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
         
-        if (identifierLookupStrategy == null) {
-            throw new ComponentInitializationException("IdentifierLookupStrategy cannot be null");
+        if (relyingPartyLookupStrategy == null) {
+            throw new ComponentInitializationException("Relying Party Lookup Strategy cannot be null");
         }
     }
     
@@ -107,10 +108,10 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
      * 
      * @param strategy lookup strategy
      */
-    public void setIdentifierLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+    public void setRelyingPartyLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
     	checkSetterPreconditions();
         
-        identifierLookupStrategy = Constraint.isNotNull(strategy, "Identifier lookup strategy can not be null");
+    	relyingPartyLookupStrategy = Constraint.isNotNull(strategy, "Relying Party Lookup Strategy cannot be null");
     }
 
     @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -119,9 +120,9 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
             return false;
         }        
         
-        identifier = identifierLookupStrategy.apply(profileRequestContext);
-        if (identifier == null) {       
-            log.warn("{} No identifier returned from lookup function", getLogPrefix());
+        relyingPartyId = relyingPartyLookupStrategy.apply(profileRequestContext);
+        if (relyingPartyId == null) {       
+            log.warn("{} No relying party ID returned from lookup function", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
@@ -148,9 +149,9 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
         }        
 
         final OIDCPeerEntityContext peerContext = msgCtx.ensureSubcontext(OIDCPeerEntityContext.class);
-        peerContext.setIdentifier(identifier);
+        peerContext.setIdentifier(relyingPartyId);
 
-        log.debug("{} Initialized inbound context for OIDC authentication message to {}", getLogPrefix(), identifier);
+        log.debug("{} Initialized inbound context for '{}'", getLogPrefix(), relyingPartyId);
     }
 
 }
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
index 591e61f..87ec9df 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
@@ -14,12 +14,13 @@
 
 package net.shibboleth.sp.oidc.profile.impl;
 
-import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
@@ -47,6 +48,7 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
  * <p>The value is set onto the context tree using a custom consumer strategy.</p>
  * 
  * TODO this is almost the same as that in sp-server
+ * TODO if used for more than correlation cookies, rename class to just processing cookies?
  * 
  * <p>The information captured is stored in a {@link TokenConsumerContext} child of the
  * {@link AgentRequestContext}.</p>
@@ -60,7 +62,7 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
 public class ProcessCorrelationCookie extends AbstractApplicationAction {
     
     /** Default cookie prefix. */
-    @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
+    @Nonnull @NotEmpty static public final String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessCorrelationCookie.class);
@@ -74,8 +76,15 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
     /** Lookup strategy for state token value. */
     @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
     
-    /** A consumer to set the cookie value into the appropriate context in the tree.*/
-    @Nullable BiConsumer<ProfileRequestContext, String> cookieValueConsumerStrategy;
+    /** 
+     * A consumer to set the cookie value into the appropriate context in the tree. The return value indicates
+     * if the consumer function was successful or not. An unsuccessful outcome may trigger an error depending on the
+     * errorFatal flag.
+     */
+    @Nullable BiFunction<ProfileRequestContext, String, Boolean> cookieValueConsumerStrategy;
+    
+    /** Whether an error recovering a correlation cookie is fatal. */
+    private boolean errorFatal;
     
     /** State token accompanying request. */
     @NonnullBeforeExec private String stateToken;
@@ -97,11 +106,13 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
     }
             
     /**
-     * Set the consumer strategy to use to set the cookie value into the context tree.
+     * Set the consumer function strategy to use to set the cookie value into the context tree. The return value 
+     * indicates if the consumer function was successful or not. An unsuccessful outcome may trigger an error 
+     * depending on the errorFatal flag.     * 
      * 
      * @param strategy The strategy to set.
      */
-    public void setCookieValueConsumerStrategy(final BiConsumer<ProfileRequestContext, String> strategy) {
+    public void setCookieValueConsumerStrategy(final BiFunction<ProfileRequestContext, String, Boolean> strategy) {
         checkSetterPreconditions();
         cookieValueConsumerStrategy = Constraint.isNotNull(strategy,
                 "cookieValueConsumerStrategy can not be null");
@@ -120,6 +131,19 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
         cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
     }
     
+    /**
+     * Sets whether an error recovering a state token should result in a fatal event.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setErrorFatal(final boolean flag) {
+        checkSetterPreconditions();
+        
+        errorFatal = flag;
+    }
+    
     /**
      * Sets the lookup strategy for obtaining the request message's ID.
      * 
@@ -151,11 +175,11 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
         }
 
         stateToken = stateTokenLookupStrategy.apply(profileRequestContext);
-        if (stateToken == null) {
+        if (stateToken == null && !errorFatal) {
             log.debug("{} No state token found in request, skipping correlation cookie processing", getLogPrefix());
             return false;
         }
-        
+        // If error is fatal, we do not skip processing here.
         return true;
     }
 
@@ -175,15 +199,26 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
             final String cookieName = cookiePrefix + escaper.escape(stateToken);
 
             final String value = cookieManager.getCookieValue(cookieName, null);
-            if (value == null) {
+            if (value == null && !errorFatal) {
                 log.debug("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
                 return;
+            } else if (value == null){
+                log.warn("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                return;
             }
 
             cookieManager.unsetCookie(cookieName);
 
             if (cookieValueConsumerStrategy != null) {
-                cookieValueConsumerStrategy.accept(profileRequestContext, value);
+                final Boolean success = cookieValueConsumerStrategy.apply(profileRequestContext, value);
+                // Treat null as failure
+                if ((success == null || Boolean.FALSE.equals(success)) && errorFatal) {
+                    log.warn("{} Could not set correlation cookie", getLogPrefix());
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+                    return;
+                }
+                log.debug("{} Processed correlation cookie for state token '{}'", getLogPrefix(), stateToken);
             } else {
                 log.warn("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
             }
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
new file mode 100644
index 0000000..f0c609e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
@@ -0,0 +1,170 @@
+/*
+ * 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 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.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.net.URISupport;
+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.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}.
+ */
+//TODO this class is complex and more fitting a decoder type mechanism
+public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdentifiableInitializableComponent 
+    implements BiFunction<ProfileRequestContext, String, Boolean> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationRequestToPeerContextConsumer.class);
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** 
+     * If the dataSealer is provided should it be used to unwrap the authentication request state? Defaults to true, 
+     * that is, if the dataSealer is provided, always attempt to unwrap the state.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> unsealState;
+    
+    /** Constructor.*/
+    public SetAuthenticationRequestToPeerContextConsumer() {
+        unsealState = PredicateSupport.alwaysTrue();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper} to use for serialization.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+    
+    /**
+     * 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(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final String 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;
+        }
+        // Possibly unseal and convert to class
+        try {
+            if (unsealState.test(profileRequestContext) && dataSealer != null) {
+                decoded = dataSealer.unwrap(decoded);
+            }
+            final AuthenticationRequestStateData authnState = objectMapper.readValue(decoded, 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;
+            }
+            // Add identifier to the peer context now we know it
+            inboundCtx.ensureSubcontext(OIDCPeerEntityContext.class)
+                    .setIdentifier(authnState.getAuthenticationAuthority());
+            // Attach the recovered authentication request state information to the inbound context for later use
+            inboundCtx.ensureSubcontext(AuthnRequestStateDataContext.class).setAuthnState(authnState);
+            return true;
+            
+        } catch (final JsonProcessingException | DataSealerException e) {
+            log.error("Unable to process authentication request state data, was it sealed and you are not unsealing it?"
+                    , e);
+            return false;
+        }     
+    }
+
+}
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
index b20016d..2d7b7f1 100644
--- 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
@@ -14,7 +14,7 @@
 
 package net.shibboleth.sp.oidc.profile.impl;
 
-import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 
 import javax.annotation.Nonnull;
 
@@ -28,15 +28,17 @@ 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 BiConsumer<ProfileRequestContext, String> {
+public class SetNonceValueToTokenContextConsumer implements BiFunction<ProfileRequestContext, String, Boolean> {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(SetNonceValueToTokenContextConsumer.class);
 
     /** {@inheritDoc} */
     @Override
-    public void accept(final ProfileRequestContext profileRequestContext, final String value) {
+    public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
         
         final String decoded = URISupport.doURLDecode(value);
         
@@ -44,6 +46,7 @@ public class SetNonceValueToTokenContextConsumer implements BiConsumer<ProfileRe
         
         profileRequestContext.ensureSubcontext(AgentRequestContext.class)
             .ensureSubcontext(TokenConsumerContext.class).setMessageCorrelationID(decoded);
+        return true;
         
     }
 
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java
new file mode 100644
index 0000000..6d669e0
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link AuthTimeRequestedActivationCondition}
+ */
+public class AuthTimeRequestedActivationConditionTest extends BaseAgplicationActionTest {
+    
+    /** The condition to test.*/
+    private AuthTimeRequestedActivationCondition condition;
+    
+
+    @Test
+    public void testAuthTimeRequested() {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setAuthTimeRequired(true);
+        condition = new AuthTimeRequestedActivationCondition(prc -> 
+             new AuthnRequestStateDataContext().setAuthnState(state));        
+        
+        final boolean requested = condition.test(prc, null);
+        assertTrue(requested);
+        
+    }
+
+
+    @Test
+    public void testAuthTimeNotRequested() {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setAuthTimeRequired(false);
+
+        condition = new AuthTimeRequestedActivationCondition(
+            prcArg -> new AuthnRequestStateDataContext().setAuthnState(state)
+        );
+
+        final boolean requested = condition.test(prc, null);
+        assertFalse(requested);
+    }
+
+    @Test
+    public void testFailSecureWhenContextLookupReturnsNull() {
+        // Lookup strategy returns null -> should default to true (fail-secure)
+        condition = new AuthTimeRequestedActivationCondition(
+            prcArg -> null
+        );
+
+        final boolean requested = condition.test(prc, null);
+        assertTrue(requested, "Should default to true when AuthnRequestStateDataContext is missing");
+    }
+
+    @Test
+    public void testFailSecureWhenAuthnStateIsNull() {
+        // Context present but authnState is null -> should default to true (fail-secure)
+        condition = new AuthTimeRequestedActivationCondition(
+            prcArg -> new AuthnRequestStateDataContext().setAuthnState(null)
+        );
+
+        final boolean requested = condition.test(prc, null);
+        assertTrue(requested, "Should default to true when authnState is null");
+    }
+
+    @Test
+    public void testClaimsAreIgnored() {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setAuthTimeRequired(true);
+
+        condition = new AuthTimeRequestedActivationCondition(
+            prcArg -> new AuthnRequestStateDataContext().setAuthnState(state)
+        );
+
+        final JWTClaimsSet claims1 = new JWTClaimsSet.Builder().claim("foo", "bar").build();
+        final JWTClaimsSet claims2 = new JWTClaimsSet.Builder().claim("baz", 123).build();
+
+        assertTrue(condition.test(prc, claims1));
+        assertTrue(condition.test(prc, claims2));
+    }
+
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java
new file mode 100644
index 0000000..a9f1a38
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+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.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.Nonce;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link AuthenticationRequestStateForStorageStrategy}.
+ */
+public class AuthenticationRequestStateForStorageStrategyTest extends BaseAgplicationActionTest{
+    
+    /** The strategy to test.*/
+    private AuthenticationRequestStateForStorageStrategy strategy;
+    
+    /** The authentication request.*/
+    private OIDCAuthenticationRequest authnRequest;
+    
+    /** The ObjectMapper to use.*/
+    private ObjectMapper mapper;
+    
+    /** The time when the request was constructed in the before method.*/
+    private Instant now;
+    
+    private Duration maxAge;
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        
+        mapper = JsonMapper.builder()
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+                .serializationInclusion(Include.NON_ABSENT)
+                .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+                .addModule(new Jdk8Module())
+                .addModule(new JavaTimeModule())
+                .build();
+        
+        strategy = new AuthenticationRequestStateForStorageStrategy();
+        strategy.setId("TestComponent");
+        strategy.setAuthenticationAuthorityLookupStrategy(prc -> "https://op.example.com");
+        authnRequest = new OIDCAuthenticationRequest(new ClientID("client-id"));
+        authnRequest.setNonce(new Nonce("nonce"));
+        authnRequest.setCodeVerifier("PKCE");
+        now = Instant.now();
+        authnRequest.setAuthnRequestTime(now);
+        maxAge = Duration.ofMinutes(1);
+        authnRequest.setMaxAge(maxAge);
+        
+        strategy.setAuthenticationRequestLookupStrategy(prc -> authnRequest);
+        strategy.setObjectMapper(mapper);        
+    }
+    
+    @Test
+    public void testSuccessfulConstruction() throws Exception {
+        strategy.initialize();
+        final String requestState = strategy.apply(prc);        
+
+        final JsonNode root = mapper.readTree(requestState);
+        assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+        assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+        assertTrue(root.hasNonNull("pkce_code_verifier"), "Pkce_code_verifier should be present and non-null");
+        assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+        assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+        assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+        assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+    }
+    
+    @Test
+    public void testFailedConstruction_NoAuthenticatingAuthority() throws Exception {
+        strategy.setAuthenticationAuthorityLookupStrategy(prc -> null);
+        strategy.initialize();
+        final String requestState = strategy.apply(prc);        
+
+        assertNull(requestState, "no authenticating authority should result in a null construction");
+    }
+    
+    @Test
+    public void testFailedConstruction_NoAuthenticationRequest() throws Exception {
+        strategy.setAuthenticationRequestLookupStrategy(prc -> null);
+        strategy.initialize();
+        final String requestState = strategy.apply(prc);        
+
+        assertNull(requestState, "no authentication request should result in a null construction");
+    }
+    
+    @Test
+    public void testSuccessfulConstruction_NoPKCE() throws Exception {
+        authnRequest.setCodeVerifier(null);
+        strategy.initialize();
+        final String requestState = strategy.apply(prc);        
+
+        final JsonNode root = mapper.readTree(requestState);
+        assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+        assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+        assertNull(root.get("pkce_code_verifier"), "Pkce_code_verifier should be null");
+        assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+        assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+        assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+        assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+
+    }
+    
+    @Test
+    public void testSuccessfulConstruction_WithDataSealer() throws Exception {
+        final DataSealer sealer = Mockito.mock(DataSealer.class);
+        Mockito.when(sealer.wrap(Mockito.anyString())).thenReturn("sealed");
+        strategy.setDataSealer(sealer);
+        strategy.initialize();
+        final String requestState = strategy.apply(prc);        
+        assertNotNull(requestState, "request state should not be null");
+        assert requestState != null;
+        assertTrue(requestState.contains("sealed"), "request state should be sealed");
+
+    }
+    
+    @Test
+    public void testSuccessfulConstruction_WithDataSealerButSealedPredicateIsFalse() throws Exception {
+        final DataSealer sealer = Mockito.mock(DataSealer.class);
+        Mockito.when(sealer.wrap(Mockito.anyString())).thenReturn("sealed");
+        strategy.setDataSealer(sealer);
+        strategy.setSealStatePredicate(prc -> false);
+        strategy.initialize();
+        
+        final String requestState = strategy.apply(prc);        
+        
+        assertNotNull(requestState, "request state should not be null");
+        assert requestState != null;
+        assertFalse(requestState.contains("sealed"), "request state should be sealed");
+        
+        final JsonNode root = mapper.readTree(requestState);
+        assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+        assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+        assertTrue(root.hasNonNull("pkce_code_verifier"), "Pkce_code_verifier should be present and non-null");
+        assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+        assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+        assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+        assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java
new file mode 100644
index 0000000..d697468
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Tests for {@link AuthenticationRequestTimeLookupFunction}.
+ */
+public class AuthenticationRequestTimeLookupFunctionTest {
+
+    /**
+     * Tests the authentication request time comes from the state.
+     */
+    @Test
+    public void testAuthTimeReturned() {
+        final Instant now = Instant.now();
+
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setAuthnRequestTime(now);
+
+        final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+        function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+        final Instant result = function.apply(new ProfileRequestContext());
+        assertEquals(result, now);
+    }
+
+    /**
+     * Tests the authentication request time comes from the state minus the clock skew
+     */
+    @Test
+    public void testAuthTimeMinusClockSkew() {
+        final Instant now = Instant.now();
+        final Duration skew = Duration.ofSeconds(10);
+
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setAuthnRequestTime(now);
+
+        final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+        assert skew != null;
+        function.setClockSkew(skew);
+
+        function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+        final Instant result = function.apply(new ProfileRequestContext());
+        assertEquals(result, now.minus(skew));
+    }
+
+    /**
+     * Tests no authentication request time in the state.
+     */
+    @Test
+    public void testNoAuthTimeReturnsNull() {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData(); // no authn time set
+
+        final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+        function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+        final Instant result = function.apply(new ProfileRequestContext());
+        assertNull(result);
+    }
+
+    /**
+     * Tests the the lookup strategy returns null.
+     */
+    @Test
+    public void testNoAuthnContextReturnsNull() {
+        final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+        function.setAuthenticationRequestLookupStrategy(prc -> null);
+
+        final Instant result = function.apply(new ProfileRequestContext());
+        assertNull(result);
+    }
+
+    /**
+     * Tests a null profile request context input produces a null result.
+     */
+    @Test
+    public void testNullProfileRequestContextReturnsNull() {
+        final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+        final Instant result = function.apply(null);
+        assertNull(result);
+    }
+
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java
new file mode 100644
index 0000000..8dbb0a2
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+
+import java.time.Duration;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Tests for {@link MaxAgeFromAuthenticationRequestLookupFunction}.
+ */
+public class MaxAgeFromAuthenticationRequestLookupFunctionTest {
+
+    /**
+     * Tests max_age present in authentication request.
+     */
+    @Test
+    public void testMaxAgeFromAuthenticationRequest() {
+        final Duration expected = Duration.ofSeconds(600);
+
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setMaxAge(expected);
+
+        final MaxAgeFromAuthenticationRequestLookupFunction function = 
+                new MaxAgeFromAuthenticationRequestLookupFunction(Duration.ofSeconds(120));
+
+        function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+        final Duration result = function.apply(new ProfileRequestContext());
+        assertEquals(result, expected);
+    }
+
+    /**
+     * Test no max_age present in authentication request, so fallback to profile configuration (not the default value).
+     */
+    @Test
+    public void testFallbackToProfileConfiguration() {
+        final Duration expected = Duration.ofSeconds(300);
+
+        final MaxAgeFromAuthenticationRequestLookupFunction func = new MaxAgeFromAuthenticationRequestLookupFunction(
+                Duration.ofSeconds(999));
+
+        func.setAuthenticationRequestLookupStrategy(prc -> null); // no authn request → fallback path
+
+        final ProfileRequestContext prc = new ProfileRequestContext();
+
+        final RelyingPartyContext rpc = prc.ensureSubcontext(RelyingPartyContext.class);
+        final DefaultOIDCAuthorizationConfiguration config = new DefaultOIDCAuthorizationConfiguration();
+        config.setMaxAuthenticationAge(expected);
+        rpc.setProfileConfig(config);
+
+        final Duration result = func.apply(prc);
+        assertEquals(result, expected);
+    }
+
+    @Test
+    public void testFallbackToDefault_WhenNoAuthnRequestOrRPC() {
+        final Duration defaultValue = Duration.ofSeconds(180);
+
+        final MaxAgeFromAuthenticationRequestLookupFunction function = 
+                new MaxAgeFromAuthenticationRequestLookupFunction(
+                defaultValue);
+
+        function.setAuthenticationRequestLookupStrategy(prc -> null);
+
+        final Duration result = function.apply(new ProfileRequestContext());
+        assertEquals(result, defaultValue);
+    }
+
+    /**
+     * Tests no max_age in authn request, no max_age in RPC, so default used.
+     */
+    @Test
+    public void testAuthnRequestPresent_ButNoMaxAgeThenRPCDefinesNoneThenDefault() {
+        final Duration defaultValue = Duration.ofSeconds(75);
+
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData();
+
+        final MaxAgeFromAuthenticationRequestLookupFunction function = 
+                new MaxAgeFromAuthenticationRequestLookupFunction(
+                defaultValue);
+
+        function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+        final ProfileRequestContext prc = new ProfileRequestContext();
+
+        final RelyingPartyContext rpc = prc.ensureSubcontext(RelyingPartyContext.class);
+
+        final DefaultOIDCAuthorizationConfiguration config = new DefaultOIDCAuthorizationConfiguration() {
+            @Override
+            public Duration getMaxAuthenticationAge(final ProfileRequestContext ctx) {
+                return null;
+            }
+        };
+
+        rpc.setProfileConfig(config);
+
+        final Duration result = function.apply(prc);
+        assertEquals(result, defaultValue);
+    }
+
+}
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
new file mode 100644
index 0000000..1ee91f1
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link SetAuthenticationRequestToPeerContextConsumer}.
+ */
+public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgplicationActionTest{
+    
+    /** The Consumer, which is a function, to test.*/
+    private SetAuthenticationRequestToPeerContextConsumer function;
+    
+    /** The authentication state data used to construct and suitable JSON input string for deserialising.*/
+    private AuthenticationRequestStateData authnState;
+    
+    /** The serialized form of the authnState.*/
+    private String authnStateSerialized;
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        
+        function = new SetAuthenticationRequestToPeerContextConsumer();
+        function.setId("TestComponent");
+        
+        authnState = new AuthenticationRequestStateData();
+        authnState.setAuthenticationAuthority("https://op.example.org");
+        authnState.setPkceCodeVerifier("PKCECodeVerifier");
+        authnState.setNonce("nonce");
+        
+        final ObjectMapper mapper = new ObjectMapper();
+        function.setObjectMapper(mapper);
+        try {
+            authnStateSerialized = mapper.writeValueAsString(authnState);
+        } catch (final JsonProcessingException e) {
+           throw new ComponentInitializationException(e);
+        }       
+        
+    }
+    
+    @Test
+    public void testSuccess() throws Exception {
+        function.initialize();
+        
+        function.apply(prc, authnStateSerialized);
+        
+        final var inboundMsgCtx = prc.getInboundMessageContext();
+        assertNotNull(inboundMsgCtx);
+        assert inboundMsgCtx != null;
+        final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+        assertNotNull(peerCtx);
+        assert peerCtx != null;
+        assertEquals(peerCtx.getIdentifier(), "https://op.example.org");
+        final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+        assertNotNull(authnStateCtx);
+        assert authnStateCtx != null;
+        assertNotNull(authnStateCtx.getAuthnState());
+        final var authnStateFromCtx = authnStateCtx.getAuthnState();
+        assertNotNull(authnStateFromCtx);
+        assert authnStateFromCtx != null;
+        assertEquals(authnStateFromCtx.getNonce(), "nonce");
+        assertEquals(authnStateFromCtx.getPkceCodeVerifier(), "PKCECodeVerifier");        
+    }   
+    
+    @Test
+    public void testSuccess_SealedWithDataSealer() throws Exception {
+        final DataSealer sealer = Mockito.mock(DataSealer.class);
+        Mockito.when(sealer.unwrap(Mockito.anyString())).thenReturn(authnStateSerialized);
+        function.setDataSealer(sealer);
+        function.initialize();
+        
+        function.apply(prc, authnStateSerialized);
+        
+        final var inboundMsgCtx = prc.getInboundMessageContext();
+        assertNotNull(inboundMsgCtx);
+        assert inboundMsgCtx != null;
+        final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+        assertNotNull(peerCtx);
+        assert peerCtx != null;
+        assertEquals(peerCtx.getIdentifier(), "https://op.example.org");
+        final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+        assertNotNull(authnStateCtx);
+        assert authnStateCtx != null;
+        assertNotNull(authnStateCtx.getAuthnState());
+        final var authnStateFromCtx = authnStateCtx.getAuthnState();
+        assertNotNull(authnStateFromCtx);
+        assert authnStateFromCtx != null;
+        assertEquals(authnStateFromCtx.getNonce(), "nonce");
+        assertEquals(authnStateFromCtx.getPkceCodeVerifier(), "PKCECodeVerifier");        
+    }   
+    
+    @Test
+    public void testFail_BadJSONAuthnState() throws Exception {
+        function.initialize();
+        
+        function.apply(prc, "bad");
+        
+        final var inboundMsgCtx = prc.getInboundMessageContext();
+        assertNotNull(inboundMsgCtx);
+        assert inboundMsgCtx != null;
+        final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+        assertNull(peerCtx);
+        final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+        assertNull(authnStateCtx);
+           
+    } 
+
+}

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


More information about the commits mailing list