[java-plugin-shibd-oidc] branch main updated: Add more tests, cleanup existing tests, move tests to correct packages

Codeberg noreply at shibboleth.net
Tue May 12 08:07:12 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/11f1a1e147a634f5d79111a4ee4f23c6c36d3a32

The following commit(s) were added to refs/heads/main by this push:
     new 11f1a1e  Add more tests, cleanup existing tests, move tests to correct packages
11f1a1e is described below

commit 11f1a1e147a634f5d79111a4ee4f23c6c36d3a32
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue May 12 09:06:57 2026 +0100

    Add more tests, cleanup existing tests, move tests to correct packages
    
     - Some of these are taken from the RP-Proxy, and may end up in
    oidc-commons, but at least for now we are increasing test coverage on
    the shibd-oidc-sp
     - Some Javadoc improvments
---
 pom.xml                                            |   1 -
 sp-oidc-api/pom.xml                                |  60 ++-
 .../AbstractAuthenticationLookupStrategyTest.java  |   9 -
 ...nticationContextClassRefLookupStrategyTest.java |   7 +-
 .../DisplayParameterLookupStrategyTest.java        |   7 +-
 .../ForceAuthnParameterLookupStrategyTest.java     |   7 +-
 .../config/navigate/PromptLookupStrategyTest.java  |   7 +-
 .../RequestedClaimsLookupStrategyTest.java         |   7 +-
 .../navigate/ResourceLookupStrategyTest.java       |   7 +-
 .../config/navigate/ScopeLookupStrategyTest.java   |   7 +-
 .../navigate/UiLocalesLookupStrategyTest.java      |   7 +-
 .../shibboleth/sp/oidc/testing/TestConstants.java  | 123 +++++
 .../net/shibboleth/sp/oidc/testing/TestHelper.java | 180 ++++++++
 sp-oidc-bom/pom.xml                                |   7 +-
 sp-oidc-conf-impl/pom.xml                          |   5 +-
 .../flows/AbstractOIDCTokenConsumerFlowTest.java   |  66 +--
 .../oidc/flows/OIDCSessionInitiatorFlowTest.java   |   1 +
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   | 119 ++---
 ...DCTokenConsumerFlowUsingStorageServiceTest.java |  14 +-
 .../shibboleth/sp/oidc/flows/TestConstants.java    | 186 --------
 sp-oidc-impl/pom.xml                               |  13 +-
 .../profile/impl/ExchangeCodeForAccessToken.java   |   3 +-
 .../sp/oidc/profile/impl/ExtractOIDCClaims.java    |  13 +
 ...SetCorrelationCookieValueToContextConsumer.java | 184 --------
 .../profile/impl/BaseOIDCAuthenticationTest.java   | 154 +++++++
 .../oidc/profile/impl/BuildRequestObjectTest.java  | 512 +++++++++++++++++++++
 .../impl/CreateAuthenticationStateDataTest.java    | 132 +++++-
 .../impl/DefaultClaimMergingStrategyTest.java      | 115 +++++
 .../impl/ExchangeCodeForAccessTokenTest.java       | 221 +++++++++
 .../profile/impl/ProcessEndUserClaimsTest.java     | 274 +++++++++++
 .../profile/impl/UserInfoEndpointLookupTest.java   | 229 +++++++++
 .../impl/ValidateUserInfoJSONObjectClaimsTest.java | 193 ++++++++
 .../resources/metadata/openid-configuration.json   |  60 +++
 sp-oidc-testing/.checkstyle                        |  10 -
 sp-oidc-testing/pom.xml                            |  78 ----
 35 files changed, 2369 insertions(+), 649 deletions(-)

diff --git a/pom.xml b/pom.xml
index 6d455cc..b376b21 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,7 +52,6 @@ copyright/license information. -->
         <module>sp-oidc-api</module>
         <module>sp-oidc-impl</module>
         <module>sp-oidc-dist</module>
-        <module>sp-oidc-testing</module>
     </modules>
 
     <dependencies>
diff --git a/sp-oidc-api/pom.xml b/sp-oidc-api/pom.xml
index 2057fd7..7600871 100644
--- a/sp-oidc-api/pom.xml
+++ b/sp-oidc-api/pom.xml
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- See LICENSE.txt file in the root directory of this repository for the copyright/license information. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<!-- See LICENSE.txt file in the root directory of this repository for the
+copyright/license information. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
@@ -8,17 +10,18 @@
         <artifactId>sp-oidc-parent</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </parent>
-    
+
     <artifactId>sp-oidc-api</artifactId>
     <description>SP OIDC API.</description>
     <name>Shibboleth SP :: OIDC Protocol Plugin :: API</name>
     <packaging>jar</packaging>
-    
+
     <properties>
-        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <checkstyle.configLocation>
+            ${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
         <automatic.module.name>net.shibboleth.sp.oidc</automatic.module.name>
     </properties>
-        
+
     <dependencies>
         <!-- Compile dependencies -->
 
@@ -27,20 +30,20 @@
             <groupId>${shibd.groupId}</groupId>
             <artifactId>sp-server-api</artifactId>
             <scope>provided</scope>
-        </dependency>        
-                
+        </dependency>
+
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-authn-api</artifactId>
             <scope>provided</scope>
         </dependency>
-        
+
         <dependency>
             <groupId>${shib-profile.groupId}</groupId>
             <artifactId>shib-profile-api</artifactId>
             <scope>provided</scope>
         </dependency>
-        
+
         <dependency>
             <groupId>${shib-attribute.groupId}</groupId>
             <artifactId>shib-attribute-api</artifactId>
@@ -61,7 +64,7 @@
             <artifactId>shib-metadata-impl</artifactId>
             <scope>provided</scope>
         </dependency>
-        
+
         <dependency>
             <groupId>${oidc-common.groupId}</groupId>
             <artifactId>oidc-common-profile-api</artifactId>
@@ -77,10 +80,10 @@
             <artifactId>oidc-common-metadata-api</artifactId>
             <scope>provided</scope>
         </dependency>
-        
+
         <!-- Test Dependencies -->
-        
-         <dependency>
+
+        <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <scope>test</scope>
@@ -100,7 +103,34 @@
             <artifactId>jakarta.servlet-api</artifactId>
             <scope>test</scope>
         </dependency>
-        
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jdk8</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>test-jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+        </plugins>
+    </build>
+
 </project>
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AbstractAuthenticationLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AbstractAuthenticationLookupStrategyTest.java
index c7cba55..0768887 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AbstractAuthenticationLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AbstractAuthenticationLookupStrategyTest.java
@@ -14,8 +14,6 @@
 
 package net.shibboleth.sp.oidc.profile.config.navigate;
 
-import java.nio.charset.StandardCharsets;
-
 import javax.annotation.Nonnull;
 
 import org.opensaml.messaging.context.MessageContext;
@@ -37,13 +35,6 @@ import net.shibboleth.sp.ddf.DDF;
  * An abstract test class for lookup functions that work to build authentication requests.
  */
 public abstract class AbstractAuthenticationLookupStrategyTest {    
-
-    /** Resource URL. */    
-    public static final byte[] RESOURCE_URL = "https://sp.example.org/secure".getBytes(StandardCharsets.UTF_8);
-
-    /** Redirect URI. */
-    @Nonnull
-    public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
     
     protected RequestContext rc;
     protected MessageContext mc;
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AuthenticationContextClassRefLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AuthenticationContextClassRefLookupStrategyTest.java
index 1856fc2..0142480 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AuthenticationContextClassRefLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/AuthenticationContextClassRefLookupStrategyTest.java
@@ -17,6 +17,7 @@ package net.shibboleth.sp.oidc.profile.config.navigate;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import org.testng.annotations.BeforeMethod;
@@ -31,7 +32,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
-import net.shibboleth.sp.oidc.profile.config.navigate.AuthenticationContextClassRefLookupStrategy;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -54,8 +55,8 @@ public class AuthenticationContextClassRefLookupStrategyTest extends AbstractAut
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
 
     }
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/DisplayParameterLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/DisplayParameterLookupStrategyTest.java
index 23c382e..5c37549 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/DisplayParameterLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/DisplayParameterLookupStrategyTest.java
@@ -18,6 +18,8 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 
+import java.nio.charset.StandardCharsets;
+
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -25,6 +27,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -47,8 +50,8 @@ public class DisplayParameterLookupStrategyTest extends AbstractAuthenticationLo
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
     
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ForceAuthnParameterLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ForceAuthnParameterLookupStrategyTest.java
index 669a96b..b607d33 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ForceAuthnParameterLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ForceAuthnParameterLookupStrategyTest.java
@@ -18,6 +18,8 @@ import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+import java.nio.charset.StandardCharsets;
+
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -26,6 +28,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -48,8 +51,8 @@ public class ForceAuthnParameterLookupStrategyTest extends AbstractAuthenticatio
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
 
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/PromptLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/PromptLookupStrategyTest.java
index dbf243f..9ea3fa4 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/PromptLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/PromptLookupStrategyTest.java
@@ -19,6 +19,8 @@ import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
+import java.nio.charset.StandardCharsets;
+
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -28,6 +30,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -49,8 +52,8 @@ public class PromptLookupStrategyTest extends AbstractAuthenticationLookupStrate
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
     
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/RequestedClaimsLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/RequestedClaimsLookupStrategyTest.java
index bbf8d6e..8f80894 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/RequestedClaimsLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/RequestedClaimsLookupStrategyTest.java
@@ -18,6 +18,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
 import org.testng.annotations.BeforeMethod;
@@ -30,7 +31,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
-import net.shibboleth.sp.oidc.profile.config.navigate.RequestedClaimsLookupStrategy;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -53,8 +54,8 @@ public class RequestedClaimsLookupStrategyTest extends AbstractAuthenticationLoo
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
     
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ResourceLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ResourceLookupStrategyTest.java
index beed47a..08f7e97 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ResourceLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ResourceLookupStrategyTest.java
@@ -19,6 +19,7 @@ import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import org.testng.annotations.BeforeMethod;
@@ -29,7 +30,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
-import net.shibboleth.sp.oidc.profile.config.navigate.ResourceLookupStrategy;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -52,8 +53,8 @@ public class ResourceLookupStrategyTest extends AbstractAuthenticationLookupStra
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
     
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ScopeLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ScopeLookupStrategyTest.java
index 8d7603c..2263c52 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ScopeLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/ScopeLookupStrategyTest.java
@@ -18,6 +18,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Set;
 
 import org.testng.annotations.BeforeMethod;
@@ -29,7 +30,7 @@ import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
-import net.shibboleth.sp.oidc.profile.config.navigate.ScopeLookupStrategy;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -52,8 +53,8 @@ public class ScopeLookupStrategyTest extends AbstractAuthenticationLookupStrateg
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
     
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/UiLocalesLookupStrategyTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/UiLocalesLookupStrategyTest.java
index 7b14d94..ca8a595 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/UiLocalesLookupStrategyTest.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/config/navigate/UiLocalesLookupStrategyTest.java
@@ -18,6 +18,7 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import org.testng.annotations.BeforeMethod;
@@ -27,7 +28,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
-import net.shibboleth.sp.oidc.profile.config.navigate.UiLocalesLookupStrategy;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
@@ -50,8 +51,8 @@ public class UiLocalesLookupStrategyTest extends AbstractAuthenticationLookupStr
 
         ddf = new DDF(null).structure();
         ddf.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
-        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
-        ddf.addmember(SPConstants.TARGET).unsafe_string(RESOURCE_URL);
+        ddf.addmember(InitiatorConstants.RESPONSE_URL).string(TestConstants.RESPONSE_URL);
+        ddf.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         addDDFToAgentRequestContext(ddf);
     }
 
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
new file mode 100644
index 0000000..3c88442
--- /dev/null
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
@@ -0,0 +1,123 @@
+/*
+ * 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.testing;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/** A class to hold constants used for tests.*/
+public final class TestConstants {
+
+    
+    /** The client_id to use, must match the issuer in oidc-test-agents.xml.*/
+    @Nonnull @NotEmpty public static final String CLIENT_ID = "testspclientid";
+
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/token-consumer";
+
+    /** Issuer. */
+    @Nonnull public static final String ISSUER = "https://op.example.org";
+
+    /** REDIRECT URI. */
+    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
+
+    /** Resource URL. */
+    @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+    
+    /** The nonce in the ID token.*/
+    @Nonnull public static final String ID_TOKEN_NONCE = "bd1b5f211250c57e";    
+    
+    /** The key to the storage record that gets held in the request state cookie set by the Storage Service.*/
+    @Nonnull public static final String STATE_COOKIE_STORAGE_KEY = "c516767a37f59fe32e049ac1cb1a8921";
+
+    /** State token string used in cookie names. */
+    public static final String STATE_TOKEN = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";    
+
+    /** Application ID used in cookie names. */
+    public static final String APPLICATION_ID = "test-oidc-application-with-ro";
+    
+    /** Application ID used when private_key_jwt has been configured.*/
+    public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
+    
+    /** Application ID used when the form_post response mode has been configured.*/
+    public static final String APPLICATION_ID_POST_RESPONSE_MODE = "test-oidc-application-with-post-responsemode";
+
+    /** Target URL encoded in cookie. */
+    public static final String TARGET_URL = "https://sp.example.org/secure";
+    
+    /** The defaulted prefix for state tokens.*/
+    public static final String STATE_TOKEN_PREFIX = "__Host-shibsp_state_";
+    
+    /** Real Token response from OpenID connect tests.*/
+    @Nonnull
+    @NotEmpty
+    public static final String ACCESS_TOKEN_RESPONSE = """
+                {
+                  "access_token": "W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji",
+                  "refresh_token": "W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji",
+                  "token_type": "Bearer",
+                  "expires_in": 3600,
+                  "id_token": "eyJraWQiOiJiNjliYzcyOS05NDJjLTQzNjItYmM2YS03OWU3MjAwOWY0Yzg\
+                iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiM0pBZmFibUx4eWVnNjJJM2JiT0RrdyIsInN1YiI\
+                6InVzZXItc3ViamVjdC0xMjM0NTMxIiwiYXVkIjoibXl0ZXN0Y2xpZW50IiwiaXNzIjoiaHR0cHM6\
+                XC9cL3d3dy5jZXJ0aWZpY2F0aW9uLm9wZW5pZC5uZXRcL3Rlc3RcL2FcL3Rlc3RfcnBfcHJveHlcLy\
+                IsImV4cCI6MTY0MzI3NTQyNCwiaWF0IjoxNjQzMjc1MTI0fQ.igU798iSZYUR7Z6_MQQendFCqm9Pf\
+                6Xpd4KtnHcPGG5FxtXPiDhlyhP_7ewSIQtzWxQ5wEatixDJWl2oRasivNCJyl6ZNeUG4YuT8S7xlgp\
+                UumoycBxrmomS7wgZSola8oZo8BuiRKlWzqLLtFkAChGi5iJEtHfVBe3k3PyQAsWRQdtixkvdxvyLq\
+                FLznIaPmMH-OZ82w7sJf4bpalZSAaR5NLpyzLFw-r81IOtOVrpQtnycCw0w_JxAP16GyZXCJc56vCE\
+                o8rji7juk3szTOclbsYFrrd9iokWHl0qJa0G1ev5XQLsiUxYxgO1Nw-CAnHtESZ9w7JIfLLX1YlY73g",
+                  "scope": "openid"
+                }
+                """;
+
+    /** Mock response from the token endpoint when an error occurs.*/
+    @Nonnull
+    @NotEmpty
+    public static final String TOKEN_RESPONSE_ERROR = """
+        {
+          "error": "invalid_request",
+          "error_description": "this request was bad"
+        }
+        """;
+    
+    /** Mock response from the UserInfo endpoint when an error occurs.*/
+    @Nonnull @NotEmpty
+    public static final String USERINFO_RESPONSE_ERROR = """
+        Bearer realm="example.com",
+               error="invalid_token",
+               error_description="The access token expired"
+        """;
+
+    
+    /** Mock Plain response from the UserInfo endpoint.*/
+    @Nonnull @NotEmpty
+    public static final String USERINFO_RESPONSE = """
+        {
+          "sub": "248289761001",
+          "name": "Jane Doe",
+          "given_name": "Jane",
+          "family_name": "Doe",
+          "preferred_username": "j.doe",
+          "email": "janedoe at example.com",
+          "picture": "http://example.com/janedoe/me.jpg"
+        }
+        """;
+
+    /** Private constructor.*/
+    private TestConstants() {
+        
+    }
+}
diff --git a/sp-oidc-testing/src/main/java/net/shibboleth/sp/oidc/testing/TestHelper.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
similarity index 69%
rename from sp-oidc-testing/src/main/java/net/shibboleth/sp/oidc/testing/TestHelper.java
rename to sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
index 9c77fbb..123e635 100644
--- a/sp-oidc-testing/src/main/java/net/shibboleth/sp/oidc/testing/TestHelper.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
@@ -13,11 +13,17 @@
  */
 package net.shibboleth.sp.oidc.testing;
 
+import static org.testng.Assert.fail;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
 import java.security.PublicKey;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
 import java.security.interfaces.RSAPublicKey;
 import java.text.ParseException;
+import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
 import java.util.List;
@@ -29,8 +35,14 @@ import javax.annotation.Nullable;
 import org.opensaml.security.credential.Credential;
 import org.testng.Assert;
 
+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 com.nimbusds.jose.EncryptionMethod;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JOSEObjectType;
@@ -54,10 +66,18 @@ import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
 import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
+import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 
 import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
 import net.shibboleth.sp.profile.ConsumerConstants;
 
 /** A helper class for testing.*/
@@ -368,4 +388,164 @@ public final class TestHelper {
                 + "}";
     }
 
+
+    /**
+     * Build cookie header bytes for the given parameters. These take the form of:
+     * <pre>
+     * __Host-shibsp_state_{appId}_{stateToken}={contents};
+     * </pre>
+     * 
+     * @param stateToken the token value used for mapping the OAuth 2.0 state to the cookie
+     * @param appID the application ID
+     * @param stateCookieContents the contents of the state cookie, either the authentication request state JSON 
+     *                          or a key to the authentication request state in the storage manager
+     * @param urlEncode do the contents need URL encoding?
+     * @return the cookie header bytes
+     * @throws Exception on error
+     */
+    public static byte[] buildCookieHeader(final String stateToken, final String appID, 
+            final String stateCookieContents, final boolean urlEncode) throws Exception {
+        
+        String contentsEncoded = null;
+        if (urlEncode) {
+            contentsEncoded = 
+                    Base64Support.encodeURLSafe(stateCookieContents.getBytes(StandardCharsets.UTF_8));
+        } else {
+            contentsEncoded = stateCookieContents;
+        }
+    
+        // Compose the header with semicolons and spacing as in the example
+        final StringBuilder sb = new StringBuilder();
+    
+        sb.append(TestConstants.STATE_TOKEN_PREFIX).append(appID).append('_').append(stateToken)
+          .append('=').append(contentsEncoded).append("; \n");
+    
+        System.out.println(sb.toString());
+        return sb.toString().getBytes("UTF-8");
+        
+    }
+
+
+    /**
+     * Build the authentication request state data for the given parameters. 
+     * 
+     * @param maxAge the maximum age of the authentication request, used for validation of the authentication response
+     * @param authTimeRequired a flag whether the authentication time is required, used for validation of the 
+     *      authentication response
+     * @param acrs the ACRs to request, used for validation of the authentication response. May be null if no 
+     *      ACRs are requested.
+     * @return
+     */
+    public static AuthenticationRequestStateData buildAuthenticationRequestStateData(
+            final Duration maxAge, final boolean authTimeRequired, 
+            @Nullable final List<String> acrs) {
+        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+                .setNonce(TestConstants.ID_TOKEN_NONCE)            
+                .setMaxAge(maxAge)
+                .setAuthTimeRequired(authTimeRequired);        
+        state.setIssuer(TestConstants.CLIENT_ID)
+            .setAuthenticationAuthority(TestConstants.ISSUER)
+            .setResponseLocation(TestConstants.RESPONSE_URL)
+            .setRequestTime(Instant.now())
+            .setRawResource(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+        
+        if (acrs != null) {
+            state.setAcrs(acrs);
+        }
+        return state;
+    }
+
+
+    /**
+     * Build the authentication request state JSON for the given parameters. This is used for the state value, and 
+     * contains important information about the authentication request.
+     * 
+     * @param maxAge the maximum age of the authentication request, used for validation of the authentication response
+     * @param authTimeRequired a flag whether the authentication time is required, used for validation of the 
+     *      authentication response
+     * @param acrs the ACRs to request, used for validation of the authentication response. May be null if no 
+     *      ACRs are requested.
+     * @return the authentication request state JSON for the given parameters
+     * @throws URISyntaxException on error.
+     */
+    public static String buildAuthenticationState(final Duration maxAge, final boolean authTimeRequired, 
+            @Nullable final List<String> acrs) throws URISyntaxException {
+        
+        final var state = buildAuthenticationRequestStateData(maxAge, authTimeRequired, acrs);
+        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 "";        
+    }
+
+
+    /**
+     * Builds a dummy OIDC authorization code response.
+     * 
+     * @param redirectURI
+     *            redirect URI
+     * @param mode
+     *            response mode
+     * 
+     * @return authentication success response for the token consumer flow
+     */
+    public static AuthenticationSuccessResponse buildOIDCAuthorizationCodeResponse(
+            @Nonnull final String redirect, @Nonnull final ResponseMode mode, final String stateString) {
+    
+        final AuthorizationCode code = new AuthorizationCode("dummy-auth-code");
+        final State state = new State(stateString);
+        URI redirectURI = null;
+        try {
+            redirectURI = new URI(redirect);
+        } catch (final URISyntaxException e) {
+            fail("Bad redirect URI", e);
+        }
+    
+        // Build a dummy success response (authorization code)
+        return new AuthenticationSuccessResponse(
+            redirectURI,
+            code, 
+            null, 
+            null,            
+            state, 
+            null,            
+            mode 
+        );
+    }
+
+
+    /**
+     * Build a invalid_request error response, from openID connect core.
+     * 
+     * @return the error response
+     */
+    public static AuthenticationErrorResponse buildErrorResponse(final String stateString) {
+    
+        final URI redirectURI = URI.create(TestConstants.RESPONSE_URL);
+        final State state = new State(stateString);
+        
+        final ErrorObject error = new ErrorObject(
+            "invalid_request",
+            "Unsupported response_type value",
+            400,
+            null);
+        
+        return new AuthenticationErrorResponse(
+            redirectURI,
+            error,
+            state,
+            ResponseMode.QUERY);
+    
+    }
+
 }
diff --git a/sp-oidc-bom/pom.xml b/sp-oidc-bom/pom.xml
index 1cd15dd..65ec242 100644
--- a/sp-oidc-bom/pom.xml
+++ b/sp-oidc-bom/pom.xml
@@ -34,12 +34,7 @@
                 <artifactId>sp-oidc-conf-impl</artifactId>
                 <version>${project.version}</version>
             </dependency>
-            
-            <dependency>
-                <groupId>${project.groupId}</groupId>
-                <artifactId>sp-oidc-testing</artifactId>
-                <version>${project.version}</version>
-            </dependency>
+
         </dependencies>
     </dependencyManagement>
     
diff --git a/sp-oidc-conf-impl/pom.xml b/sp-oidc-conf-impl/pom.xml
index 91c9944..315a27a 100644
--- a/sp-oidc-conf-impl/pom.xml
+++ b/sp-oidc-conf-impl/pom.xml
@@ -196,12 +196,13 @@
             <version>${shibd.version}</version>
             <type>test-jar</type>
             <scope>test</scope>
-        </dependency>
+        </dependency>      
         
         <dependency>
             <groupId>${project.groupId}</groupId>
-            <artifactId>sp-oidc-testing</artifactId>
+            <artifactId>sp-oidc-api</artifactId>
             <version>${project.version}</version>
+            <type>test-jar</type>
             <scope>test</scope>
         </dependency>
 
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
index d7900e2..77a07b9 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/AbstractOIDCTokenConsumerFlowTest.java
@@ -19,7 +19,6 @@ import static org.testng.Assert.fail;
 
 import java.io.IOException;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.time.Instant;
@@ -54,18 +53,14 @@ import com.nimbusds.jose.JWEAlgorithm;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.AuthorizationResponse;
 import com.nimbusds.oauth2.sdk.ErrorObject;
-import com.nimbusds.oauth2.sdk.ResponseMode;
 import com.nimbusds.oauth2.sdk.TokenErrorResponse;
 import com.nimbusds.oauth2.sdk.TokenResponse;
-import com.nimbusds.oauth2.sdk.id.State;
 import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
-import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
@@ -84,6 +79,7 @@ import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.flows.AbstractSPFlowTest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.oidc.testing.TestHelper;
 import net.shibboleth.sp.profile.ConsumerConstants;
 
@@ -195,7 +191,6 @@ public abstract class AbstractOIDCTokenConsumerFlowTest extends AbstractSPFlowTe
         
         final ErrorObject error = new ErrorObject(code, description, httpCode);
         final TokenErrorResponse errorResponse = new TokenErrorResponse(error);
-
         return errorResponse;
     }
 
@@ -418,65 +413,6 @@ public abstract class AbstractOIDCTokenConsumerFlowTest extends AbstractSPFlowTe
     }
 
 
-    /**
-     * Builds a dummy OIDC authorization code response.
-     * 
-     * @param redirectURI
-     *            redirect URI
-     * @param mode
-     *            response mode
-     * 
-     * @return authentication success response for the token consumer flow
-     */
-    protected AuthenticationSuccessResponse buildOIDCAuthorizationCodeResponse(
-            @Nonnull final String redirect, @Nonnull final ResponseMode mode, final String stateString) {
-
-        final AuthorizationCode code = new AuthorizationCode("dummy-auth-code");
-        final State state = new State(stateString);
-        URI redirectURI = null;
-        try {
-            redirectURI = new URI(redirect);
-        } catch (final URISyntaxException e) {
-            fail("Bad redirect URI", e);
-        }
-
-        // Build a dummy success response (authorization code)
-        return new AuthenticationSuccessResponse(
-            redirectURI,
-            code, 
-            null, 
-            null,            
-            state, 
-            null,            
-            mode 
-        );
-    }
-    
-    /**
-     * Build a invalid_request error response, from openID connect core.
-     * 
-     * @return the error response
-     */
-    protected AuthenticationErrorResponse buildErrorResponse(final String stateString) {
-
-        final URI redirectURI = URI.create(TestConstants.RESPONSE_URL);
-        final State state = new State(stateString);
-        
-        final ErrorObject error = new ErrorObject(
-            "invalid_request",
-            "Unsupported response_type value",
-            400,
-            null);
-        
-        return new AuthenticationErrorResponse(
-            redirectURI,
-            error,
-            state,
-            ResponseMode.QUERY);
-
-    }
-    
-    
     /**
      * Encodes an OIDC response into the query string of a redirect URI embedded in a remoted message.
      * 
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 8cf10fd..2c9123a 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
@@ -64,6 +64,7 @@ import net.shibboleth.sp.flows.AbstractSPFlowTest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
 import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 import net.shibboleth.sp.oidc.profile.OIDCInitiatorConstants;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
 
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 26f52a2..2e724ea 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
@@ -35,6 +35,7 @@ import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
 import net.shibboleth.idp.test.PreferFileSystemApplicationContextInitializer;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.testing.TestConstants;
 import net.shibboleth.sp.oidc.testing.TestHelper;
 
 /**
@@ -86,19 +87,20 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
     @Test
     public void testSuccess_SignedIDToken_PlainUserInfo() throws Exception {
         
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, null), 
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(
+                Instant.now().plusSeconds(3600), Instant.now(), null, null), 
                 constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -125,15 +127,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.FORM_POST, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.FORM_POST, 
                         TestConstants.STATE_TOKEN);        
         final DDF input = buildRemotedFormPostResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID_POST_RESPONSE_MODE,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID_POST_RESPONSE_MODE, input);
 
@@ -155,19 +157,20 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
      */
     @Test
     public void testSuccess_SignedIDToken_PlainUserInfo_PrivateKeyJWT() throws Exception {
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, null), 
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(
+                Instant.now().plusSeconds(3600), Instant.now(), null, null), 
                 constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         // Use application which requires private_key_jwt
         setApplicationRequest(TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT, input);
@@ -194,15 +197,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 Map.of("auth_time",Date.from(Instant.now()))), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);   
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null), true));
+                TestHelper.buildAuthenticationState(Duration.ofMinutes(1), true, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -228,15 +231,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null), true));
+                TestHelper.buildAuthenticationState(Duration.ofMinutes(1), true, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -260,15 +263,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 Instant.now(), null, null), constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -294,15 +297,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null, null), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -329,15 +332,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null, Map.of("acr","loa2")), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1")), true));
+                TestHelper.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1")), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -366,15 +369,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null, Map.of("acr","loa1")), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1")), true));
+                TestHelper.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1")), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -401,15 +404,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null, Map.of("azp", TestConstants.CLIENT_ID)), constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -434,15 +437,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 null, Map.of("azp", "bad-azp")), constructJWTUserInfoResponseSigned());
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -467,15 +470,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 Instant.now(), "bad-idtoken-nonce", null), constructJWTUserInfoResponseSigned()); 
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -496,15 +499,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 constructJWTUserInfoResponseSigned());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);         
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
         
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
         final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
@@ -526,15 +529,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
                 Instant.now(), TestConstants.ID_TOKEN_NONCE, null), constructJWTUserInfoResponseSigned()); 
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN,
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -557,15 +560,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
     @Test
     public void testFail_StateValueMisMatch() throws Exception {
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         "different-state"); 
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -584,13 +587,13 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
      */
     @Test
     public void testFail_ErrorResponse() throws Exception {
-        final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_TOKEN));
+        final DDF input = buildRemotedQueryStringResponse(TestHelper.buildErrorResponse(TestConstants.STATE_TOKEN));
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -605,8 +608,8 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
     /**
      * Test error response from OP followed by a replayed success response. It should fail as the state has been
      * replayed. That is, if the state originally sent that resulted in an error from the OP, where added to a 
-     * forged success response, it will not be allowed by the SP even if it matches to the one in stored state (a cookie
-     * for example).
+     * forged success response, it will not be allowed by the SP even if it matches to the one in stored state 
+     * (a cookie for example).
      * 
      * @throws IOException on error
      */
@@ -614,13 +617,13 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
     public void testFail_ErrorResponse_FollowedByReplayedSuccess() throws Exception {
         
         // First create a failed response
-        final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_TOKEN));
+        final DDF input = buildRemotedQueryStringResponse(TestHelper.buildErrorResponse(TestConstants.STATE_TOKEN));
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));
+                TestHelper.buildAuthenticationState(null, false, null), true));
 
         setApplicationRequest(TestConstants.APPLICATION_ID, input);
 
@@ -635,15 +638,15 @@ public class OIDCTokenConsumerFlowTest extends AbstractOIDCTokenConsumerFlowTest
         // should fail early.
         
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN); 
         final DDF inputSuccessResponse = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        inputSuccessResponse.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        inputSuccessResponse.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
-                TestConstants.buildAuthenticationState(null, false, null), true));       
+                TestHelper.buildAuthenticationState(null, false, null), true));       
         
         setApplicationRequest(TestConstants.APPLICATION_ID, inputSuccessResponse);
         
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowUsingStorageServiceTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowUsingStorageServiceTest.java
index 8c124ae..eb8b658 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowUsingStorageServiceTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowUsingStorageServiceTest.java
@@ -41,6 +41,8 @@ import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+import net.shibboleth.sp.oidc.testing.TestHelper;
 import net.shibboleth.sp.state.impl.StorageServiceStateManager;
 
 /**
@@ -98,7 +100,7 @@ public class OIDCTokenConsumerFlowUsingStorageServiceTest extends AbstractOIDCTo
 
         // Add an authentication record to the storage service
         final AuthenticationRequestStateData stateData = 
-                TestConstants.buildAuthenticationRequestStateData(null, false, null);
+                TestHelper.buildAuthenticationRequestStateData(null, false, null);
         
         final String serializedState = om.writeValueAsString(stateData);
         final String encoded = Base64Support.encode(serializedState.getBytes(StandardCharsets.UTF_8), false);
@@ -108,7 +110,8 @@ public class OIDCTokenConsumerFlowUsingStorageServiceTest extends AbstractOIDCTo
         builder.append('!').append("testsp.example.org").append('!').append(TestConstants.APPLICATION_ID);
         final String context = builder.toString();
         storageService.create(context, 
-                TestConstants.STATE_COOKIE_STORAGE_KEY, encoded, Instant.now().plus(Duration.ofMinutes(30)).toEpochMilli());
+                TestConstants.STATE_COOKIE_STORAGE_KEY, encoded, 
+                Instant.now().plus(Duration.ofMinutes(30)).toEpochMilli());
         
     }
     
@@ -121,16 +124,17 @@ public class OIDCTokenConsumerFlowUsingStorageServiceTest extends AbstractOIDCTo
     @Test
     public void testSuccess_SignedIDToken_PlainUserInfo() throws Exception {
         
-        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, null), 
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(
+                Instant.now().plusSeconds(3600), Instant.now(), null, null), 
                 constructJSONUserInfoResponse());
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                TestHelper.buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
                         TestConstants.STATE_TOKEN);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+        input.addmember("http.headers.Cookie").unsafe_string(TestHelper.buildCookieHeader(
                 TestConstants.STATE_TOKEN, 
                 TestConstants.APPLICATION_ID,
                 TestConstants.STATE_COOKIE_STORAGE_KEY, false));
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
deleted file mode 100644
index f187e52..0000000
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.sp.oidc.flows;
-
-import static org.testng.Assert.fail;
-
-import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-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 {
-
-    /** Private constructor.*/
-    private TestConstants() {
-        
-    }
-    
-    /** The client_id to use, must match the issuer in oidc-test-agents.xml.*/
-    @Nonnull @NotEmpty public static final String CLIENT_ID = "testspclientid";
-
-    /** Flow ID. */
-    @Nonnull public static final String FLOW_ID = "sp/token-consumer";
-
-    /** Issuer. */
-    @Nonnull public static final String ISSUER = "https://op.example.org";
-
-    /** REDIRECT URI. */
-    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
-
-    /** Resource URL. */
-    @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
-    
-    /** The nonce in the ID token.*/
-    @Nonnull public static final String ID_TOKEN_NONCE = "bd1b5f211250c57e";    
-    
-    /** The key to the storage record that gets held in the request state cookie set by the Storage Service.*/
-    @Nonnull public static final String STATE_COOKIE_STORAGE_KEY = "c516767a37f59fe32e049ac1cb1a8921";
-
-    /** State token string used in cookie names. */
-    public static final String STATE_TOKEN = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";    
-
-    /** Application ID used in cookie names. */
-    public static final String APPLICATION_ID = "test-oidc-application-with-ro";
-    
-    /** Application ID used when private_key_jwt has been configured.*/
-    public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
-    
-    /** Application ID used when the form_post response mode has been configured.*/
-    public static final String APPLICATION_ID_POST_RESPONSE_MODE = "test-oidc-application-with-post-responsemode";
-
-    /** Target URL encoded in cookie. */
-    public static final String TARGET_URL = "https://sp.example.org/secure";
-    
-    /** The defaulted prefix for state tokens.*/
-    public static final String STATE_TOKEN_PREFIX = "__Host-shibsp_state_";   
-    
-    
-    /**
-     * Build cookie header bytes for the given parameters. These take the form of:
-     * <pre>
-     * __Host-shibsp_state_{appId}_{stateToken}={contents};
-     * </pre>
-     * 
-     * @param stateToken the token value used for mapping the OAuth 2.0 state to the cookie
-     * @param appID the application ID
-     * @param stateCookieContents the contents of the state cookie, either the authentication request state JSON 
-     *                          or a key to the authentication request state in the storage manager
-     * @param urlEncode TODO
-     * @return the cookie header bytes
-     * @throws Exception on error
-     */
-    public static byte[] buildCookieHeader(final String stateToken, final String appID, 
-            final String stateCookieContents, final boolean urlEncode) throws Exception {
-        
-        String contentsEncoded = null;
-        if (urlEncode) {
-            contentsEncoded = 
-                    Base64Support.encodeURLSafe(stateCookieContents.getBytes(StandardCharsets.UTF_8));
-        } else {
-            contentsEncoded = stateCookieContents;
-        }
-
-        // Compose the header with semicolons and spacing as in the example
-        final StringBuilder sb = new StringBuilder();
-       
-        sb.append(STATE_TOKEN_PREFIX).append(appID).append('_').append(stateToken)
-          .append('=').append(contentsEncoded).append("; \n");
-       
-        System.out.println(sb.toString());
-        return sb.toString().getBytes("UTF-8");
-        
-    }
-    
-    /**
-     * Build the authentication request state data for the given parameters. 
-     * 
-     * @param maxAge the maximum age of the authentication request, used for validation of the authentication response
-     * @param authTimeRequired a flag whether the authentication time is required, used for validation of the 
-     *      authentication response
-     * @param acrs the ACRs to request, used for validation of the authentication response. May be null if no 
-     *      ACRs are requested.
-     * @return
-     */
-    public static AuthenticationRequestStateData buildAuthenticationRequestStateData(
-            final Duration maxAge, final boolean authTimeRequired, 
-            @Nullable final List<String> acrs) {
-        final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
-                .setNonce(ID_TOKEN_NONCE)            
-                .setMaxAge(maxAge)
-                .setAuthTimeRequired(authTimeRequired);        
-        state.setIssuer(CLIENT_ID)
-            .setAuthenticationAuthority(ISSUER)
-            .setResponseLocation(RESPONSE_URL)
-            .setRequestTime(Instant.now())
-            .setRawResource(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
-        
-        if (acrs != null) {
-            state.setAcrs(acrs);
-        }
-        return state;
-    }
-
-
-    /**
-     * Build the authentication request state JSON for the given parameters. This is used for the state value, and 
-     * contains important information about the authentication request.
-     * 
-     * @param maxAge the maximum age of the authentication request, used for validation of the authentication response
-     * @param authTimeRequired a flag whether the authentication time is required, used for validation of the 
-     *      authentication response
-     * @param acrs the ACRs to request, used for validation of the authentication response. May be null if no 
-     *      ACRs are requested.
-     * @return the authentication request state JSON for the given parameters
-     * @throws URISyntaxException on error.
-     */
-    public static String buildAuthenticationState(final Duration maxAge, final boolean authTimeRequired, 
-            @Nullable final List<String> acrs) throws URISyntaxException {
-        
-        final var state = buildAuthenticationRequestStateData(maxAge, authTimeRequired, acrs);
-        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-impl/pom.xml b/sp-oidc-impl/pom.xml
index 1e1c2a1..2b48b14 100644
--- a/sp-oidc-impl/pom.xml
+++ b/sp-oidc-impl/pom.xml
@@ -177,16 +177,17 @@
         </dependency>
         
         <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>sp-oidc-testing</artifactId>
-            <version>${project.version}</version>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-server-impl</artifactId>
+            <version>${shibd.version}</version>
+            <type>test-jar</type>
             <scope>test</scope>
         </dependency>
         
         <dependency>
-            <groupId>${shibd.groupId}</groupId>
-            <artifactId>sp-server-impl</artifactId>
-            <version>${shibd.version}</version>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sp-oidc-api</artifactId>
+            <version>${project.version}</version>
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
index 34c0d1d..0ad6505 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
@@ -31,7 +31,6 @@ import com.nimbusds.oauth2.sdk.TokenResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 
-import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
 import net.shibboleth.oidc.profile.core.OidcEventIds;
 import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
@@ -45,7 +44,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * 
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @event {@link OidcEventIds#INVALID_TOKEN}
  * 
  * @post Add the Access Token Response to the {@link AccessTokenResponseContext}.
  */
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
index 27e8976..4412505 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExtractOIDCClaims.java
@@ -73,6 +73,19 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
 /**
  * An action that extracts OIDC End-User claims from the combined id_token and UserInfo response claims in the 
  * {@link EndUserClaimsContext}.
+ * 
+ * <p>Attributes decoded from the assertion(s) are in an unfiltered state and subject to the filtering
+ * service. Any other data extracted is stored directly in the filtered set.</p>
+ * 
+ * <p>There are three supplemental sets: the optional "standard" set extracted from the message,
+ * an optional profile config-supplied strategy function, and an optional use of the {@link AttributeResolver}
+ * associated with the {@link Application}.</p>
+ * 
+ * <p>The final set of attributes is stored back in the {@link AttributeContext} for use by later actions.</p>
+ * 
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
  */
 public class ExtractOIDCClaims extends AbstractApplicationAction {
     
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
deleted file mode 100644
index bf546f1..0000000
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.sp.oidc.profile.impl;
-
-import java.nio.charset.StandardCharsets;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import net.minidev.json.JSONObject;
-import net.minidev.json.JSONValue;
-import net.minidev.json.parser.ParseException;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.DecodingException;
-import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.PredicateSupport;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.shared.security.DataSealer;
-import net.shibboleth.shared.security.DataSealerException;
-import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
-
-
-
-/**
- * Consumer that decodes a correlation cookie value and stores it on a {@link CorrelationCookieStateContext}. The
- * cookie value is expected to be the raw value of the cookie, decoding happens here.
- *
- * <p>The supplied value is expected to represent a JSON object encoded either as:
- * <ul>
- *   <li>a URL-safe Base64 representation of UTF-8 JSON, or</li>
- *   <li>a URL-safe Base64 representation of UTF-8 JSON sealed using a {@link DataSealer}.</li>
- * </ul>
- *
- * <p>On failure, this consumer returns {@code false}.</p>
- */
- at ThreadSafeAfterInit
-public class SetCorrelationCookieValueToContextConsumer extends AbstractIdentifiableInitializableComponent
-            implements BiFunction<ProfileRequestContext, String, Boolean> {
-    
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(SetCorrelationCookieValueToContextConsumer.class);
-    
-    /** The strategy to locate the {@link CorrelationCookieStateContext} to operate on. */
-    @NonnullAfterInit 
-    private Function<ProfileRequestContext, CorrelationCookieStateContext> correlationCookieStateContextLookupStrategy;
-    
-    /** Optional data sealer to use. */
-    @Nullable private DataSealer dataSealer;
-    
-    /** 
-     * If the dataSealer is provided should it be used to unwrap the correlation cookie state? Defaults to true, 
-     * that is, if the dataSealer is provided, always attempt to unwrap the state.
-     */
-    @Nonnull private Predicate<ProfileRequestContext> unsealState;
-    
-    /** Constructor.*/
-    public SetCorrelationCookieValueToContextConsumer() {
-        unsealState = PredicateSupport.alwaysTrue();
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-
-        if (correlationCookieStateContextLookupStrategy == null) {
-            throw new ComponentInitializationException("CorrelationCookieStateContextLookupStrategy "
-                    + "lookup strategy can not be null");
-        }
-    }
-    
-    /**
-     * Set the strategy to locate the {@link CorrelationCookieStateContext} to operate on.
-     * 
-     * @param strategy The strategy to set.
-     */
-    public void setCorrelationCookieStateContextLookupStrategy(
-            final Function<ProfileRequestContext, CorrelationCookieStateContext> strategy) {
-        checkSetterPreconditions();
-        correlationCookieStateContextLookupStrategy = Constraint.isNotNull(strategy,
-                "CorrelationCookieStateContextLookupStrategy can not be null");
-    }
-    
-    /**
-     * Sets {@link DataSealer} to use.
-     * 
-     * @param sealer data sealer
-     */
-    public void setDataSealer(@Nullable final DataSealer sealer) {
-        checkSetterPreconditions();
-        
-        dataSealer = sealer;
-    }
-    
-    /**
-     * Set the predicate to determine whether to unseal the state.
-     * 
-     * @param predicate the seal state predicate to set.
-     */
-    public void setUnsealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
-        checkSetterPreconditions();
-        
-        unsealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
-    }
-    
-    /**
-     * Set the flag to determine whether to unseal the state.
-     * 
-     * @param flag the flag to set.
-     */
-    public void setUnsealState(final boolean flag) {
-        checkSetterPreconditions();
-        
-        unsealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
-        // Value should be b64 encoded
-        if (value == null ||  profileRequestContext == null) {
-            return false;
-        }
-        final CorrelationCookieStateContext context = 
-                correlationCookieStateContextLookupStrategy.apply(profileRequestContext);
-        if (context == null) {
-            log.debug("Correlation cookie context not found, can not set correlation cookie onto the context");
-            return false;
-        }
-        log.trace("Attempting to set correlation cookie onto the context '{}'", value);
-        Object correlationCookieAsObject = null;        
-        try {
-            final byte[] decoded = Base64Support.decodeURLSafe(value);
-            final String decodedString = new String(decoded, StandardCharsets.UTF_8);
-            log.trace("Attempting to set base64 URL decoded correlation cookie '{}'", decodedString);
-            final var localDataSealer = dataSealer;
-            if (localDataSealer != null && unsealState.test(profileRequestContext)) {
-                log.trace("Attempting to unseal correlation cookie");
-                final String unsealed = localDataSealer.unwrap(decodedString);
-                correlationCookieAsObject = JSONValue.parseWithException(unsealed);                
-            } else {                
-                correlationCookieAsObject = JSONValue.parseWithException(decodedString);                
-            }
-            
-        } catch (final DataSealerException | DecodingException | ParseException e) {
-            log.warn("Unable to process correlation cookie, was it sealed and you are not unsealing it?"
-                    , e);
-            return false;
-        }
-        log.trace("Decoded correlation cookie as '{}'", correlationCookieAsObject);
-        if (correlationCookieAsObject instanceof final JSONObject json) {
-            context.setValue(json);   
-        } else {
-            log.error("Decoded correlation cookie was not a JSON object");
-            return false;
-        }
-             
-        return true;
-        
-    }
-
-}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
new file mode 100644
index 0000000..442e412
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.assertNotNull;
+import static org.testng.Assert.fail;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.core.io.ClassPathResource;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+import net.shibboleth.sp.oidc.testing.TestHelper;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
+
+/** Abstract class for tests that require context setup appropriate for an OIDC request/response.*/
+public abstract class BaseOIDCAuthenticationTest extends BaseApplicationActionTest {
+    
+    /** The peer entity context for outbound.*/
+    protected OIDCPeerEntityContext peerEntityCtxOutbound;
+    
+    /** The peer entity context for inbound.*/
+    protected OIDCPeerEntityContext peerEntityCtxInbound;
+    
+    /** The authentication request.*/
+    protected OIDCAuthenticationRequest authnRequest;
+    
+    /** The RP config.*/
+    protected DefaultOIDCAuthorizationConfiguration partyConfig;
+    
+    /** The OIDC provider metadata context for outbound.*/
+    @NonnullBeforeExec protected OIDCProviderMetadataContext providerCtxOutbound;
+    
+    /** The OIDC provider metadata context for inbound.*/
+    @NonnullBeforeExec protected OIDCProviderMetadataContext providerCtxInbound;
+    
+    /** The OP metadata to use.*/
+    private OIDCProviderMetadata metadata;
+    
+    
+    /** 
+     * Setup the various contexts.
+     * @throws ComponentInitializationException 
+     * 
+     * @throws Exception on error
+     */
+    @Override
+    public void beforeMethod() throws ComponentInitializationException  {
+        super.beforeMethod();        
+        
+        try {
+            final var metadataFromFile = new ClassPathResource("metadata/openid-configuration.json");
+            final String json = new String(metadataFromFile.getInputStream().readAllBytes(), StandardCharsets.UTF_8);        
+            metadata = OIDCProviderMetadata.parse(JSONObjectUtils.parse(json));    
+        } catch (final Exception e) {
+            fail(e.getMessage());
+        }
+        
+        // Set context for outbound messages
+        final MessageContext outMsgCtx = new MessageContext();
+        authnRequest = new OIDCAuthenticationRequest(new ClientID(TestConstants.CLIENT_ID));
+        outMsgCtx.setMessage(authnRequest);
+        prc.setOutboundMessageContext(outMsgCtx);        
+        peerEntityCtxOutbound = outMsgCtx.ensureSubcontext(OIDCPeerEntityContext.class);
+        providerCtxOutbound = new OIDCProviderMetadataContext();
+        providerCtxOutbound.setProviderInformation(metadata);
+        assert providerCtxOutbound != null;
+        peerEntityCtxOutbound.addSubcontext(providerCtxOutbound);   
+        outMsgCtx.addSubcontext(peerEntityCtxOutbound);
+        
+        // Set context for inbound messages
+        final MessageContext inMsgCtx = new MessageContext();
+        inMsgCtx.setMessage(TestHelper.buildOIDCAuthorizationCodeResponse(
+                TestConstants.RESPONSE_URL, ResponseMode.QUERY, "state"));
+        // Add peer entity and metadata to inbound as well as outbound, depending on which side of the flow is tested
+        peerEntityCtxInbound = inMsgCtx.ensureSubcontext(OIDCPeerEntityContext.class);
+        providerCtxInbound = new OIDCProviderMetadataContext();
+        providerCtxInbound.setProviderInformation(metadata);
+        peerEntityCtxInbound.addSubcontext(providerCtxInbound);   
+        inMsgCtx.addSubcontext(peerEntityCtxInbound);
+        prc.setInboundMessageContext(inMsgCtx);
+
+
+    }
+    
+    /**
+     * Convert the given HTTP response to a JSONObject. This is used to convert the response from the token endpoint
+     * to a JSONObject for easier access to the values.
+     * 
+     * @param response the HTTP response to convert.
+     * @return the converted JSONObject.
+     * @throws Exception on error.
+     */
+    protected JSONObject convertHttpResponseToJSONObject(final ClassicHttpResponse response) throws Exception {
+        final ObjectMapper mapper = new ObjectMapper();
+        final Map<String, Object> tokenResponseAsMap = mapper.readValue(
+                response.getEntity().getContent(), new TypeReference<Map<String, Object>>() {});
+        return new JSONObject(tokenResponseAsMap);
+    }
+    
+    
+    /**
+     * Get the outbound message context from the given PRC. If the PRC is null or the outbound message context
+     * is null, the assertions will fail.
+     * 
+     * @param prc the profile request context
+     * 
+     * @return the message context. Never null at that point.
+     */
+    @Nonnull protected MessageContext getOutboundMessageContextFailIfNull(
+            @Nullable final ProfileRequestContext prc){
+        assertNotNull(prc);
+        assert prc != null;
+        final var outboundMsgCtx = prc.getOutboundMessageContext();
+        assertNotNull(outboundMsgCtx);
+        assert outboundMsgCtx != null;
+        return outboundMsgCtx;
+    }
+    
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObjectTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObjectTest.java
new file mode 100644
index 0000000..0fe2f12
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObjectTest.java
@@ -0,0 +1,512 @@
+/*
+ * 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 static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.List;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.ResponseType;
+import com.nimbusds.openid.connect.sdk.Nonce;
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.assurance.claims.VerifiedClaimsSetRequest;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+
+/** Tests for the {@link BuildRequestObject} action.*/
+public class BuildRequestObjectTest extends BaseOIDCAuthenticationTest {
+    
+    /** The action to test.*/
+    private BuildRequestObject action;
+    
+    /** The RPC.*/
+    private RelyingPartyContext rpc;
+    
+    /** The profile config.*/
+    private DefaultOIDCAuthorizationConfiguration oidcAuthzConfig;
+       
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        action = new BuildRequestObject(); 
+        
+        rpc = prc.ensureSubcontext(RelyingPartyContext.class); 
+        oidcAuthzConfig = new DefaultOIDCAuthorizationConfiguration();
+        final RelyingPartyConfiguration rpConfig = new BasicRelyingPartyConfiguration();
+        rpc.setProfileConfig(oidcAuthzConfig);
+        rpc.setConfiguration(rpConfig);
+        // Setup a basic authentication request
+        authnRequest.setNonce(new Nonce());
+        authnRequest.setResponseType(Constraint.isNotNull(ResponseType.CODE, "response code is null"));
+        authnRequest.setResponseMode(ResponseMode.QUERY);
+        authnRequest.setDefaultResponseMode(ResponseMode.QUERY);
+        authnRequest.setMaxAge(Duration.ofSeconds(60));
+        authnRequest.setProviderSupportsClaimsParameter(true);
+        try {
+            authnRequest.setRedirectURI(new URI(TestConstants.RESPONSE_URL));
+        } catch (final URISyntaxException e) {
+            throw new ComponentInitializationException(e);
+        }
+    }
+    
+    /**
+     * Build a request object with the correct claims.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public void testBuildRequestObject_Success() throws ComponentInitializationException {
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);     
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(
+                ((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /**
+     * Build a request object with the correct claims and custom claims.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    public void testBuildRequestObject_CustomClaims_Success() throws ComponentInitializationException {
+        
+        action.setCustomClaimsStrategy((prc,set) -> {
+            set.setClaim("custom", "value");
+        });
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);     
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(
+                ((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getStringClaim("custom"));
+        assertEquals(claims.getStringClaim("custom"),"value");
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /**
+     * Build a request object with the correct set of ACRs.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRs_Success() throws ComponentInitializationException {
+        
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        
+        final Object claimsObject = claims.getClaim("claims");
+        assertTrue(claimsObject instanceof OIDCClaimsRequest);
+        final OIDCClaimsRequest requestedClaims =(OIDCClaimsRequest) claimsObject;
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr"));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /** 
+     * Test building acr_values when the claims claim is not supported.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRs_ClaimsNotSupported_Success() throws ComponentInitializationException {
+        
+        authnRequest.setProviderSupportsClaimsParameter(false);
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNull(claims.getClaim("claims"));
+        
+        // Now test acr_values
+        assertEquals(claims.getStringClaim("acr_values"),"urn:mace:incommon:iap:silver");
+    }
+    
+    /**
+     * Test build a request object with requested claims and ACRs.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRsAndExistingRequestedClaims_Success() throws ComponentInitializationException {
+        
+        final ClaimsSetRequest.Entry nameEntry = 
+                new ClaimsSetRequest.Entry("first_name").withClaimRequirement(ClaimRequirement.ESSENTIAL);
+        final ClaimsSetRequest idTokenClaimsRequest = new ClaimsSetRequest().add(nameEntry);
+        final OIDCClaimsRequest requested = 
+                new OIDCClaimsRequest().withIDTokenClaimsRequest(idTokenClaimsRequest);
+        
+        authnRequest.setRequestedClaims(requested);
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        
+        final Object claimsObject = claims.getClaim("claims");
+        assertTrue(claimsObject instanceof OIDCClaimsRequest);
+        final OIDCClaimsRequest requestedClaims =(OIDCClaimsRequest) claimsObject;
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("first_name"));
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr"));
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList());
+        assertEquals(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().get(0).toString(),"urn:mace:incommon:iap:silver");
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /**
+     * Test build a request object with ACRS when ACRs have been requested and claims configured. 
+     * These should be merged in.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRsAndExistingACRs_Success() throws ComponentInitializationException {
+        
+        // Add some to the existing requested claims set
+        final ClaimsSetRequest.Entry acrEntry = 
+                new ClaimsSetRequest.Entry("acr").withValues(List.of(new ACR("urn:mace:incommon:iap:bronze")))
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL);
+        final ClaimsSetRequest idTokenClaimsRequest = new ClaimsSetRequest().add(acrEntry);
+        final OIDCClaimsRequest requested = 
+                new OIDCClaimsRequest().withIDTokenClaimsRequest(idTokenClaimsRequest);
+        
+        authnRequest.setRequestedClaims(requested);
+        
+        // Now add ACRs
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver"), new ACR("urn:mace:incommon:iap:purple")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        
+        final Object claimsObject = claims.getClaim("claims");
+        assertTrue(claimsObject instanceof OIDCClaimsRequest);
+        final OIDCClaimsRequest requestedClaims =(OIDCClaimsRequest) claimsObject;
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr"));        
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList());
+        assertEquals(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList().size(),3);
+        
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:bronze")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:silver")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:purple")));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /** 
+     * Test build a request object with ACRS when ACRs have been requested and claims configured. 
+     * These should be merged in.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRsAndExistingACRs_WithSingleValue_Success() 
+            throws ComponentInitializationException {
+        
+        // Add some to the existing requested claims set. Set using with Value, not Value(s)
+        final ClaimsSetRequest.Entry acrEntry = 
+                new ClaimsSetRequest.Entry("acr").withValue(new ACR("urn:mace:incommon:iap:bronze"))
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL);
+        final ClaimsSetRequest idTokenClaimsRequest = new ClaimsSetRequest().add(acrEntry);
+        final OIDCClaimsRequest requested = 
+                new OIDCClaimsRequest().withIDTokenClaimsRequest(idTokenClaimsRequest);
+        
+        authnRequest.setRequestedClaims(requested);
+        
+        // Now add ACRs
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver"), new ACR("urn:mace:incommon:iap:purple")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        
+        final Object claimsObject = claims.getClaim("claims");
+        assertTrue(claimsObject instanceof OIDCClaimsRequest);
+        final OIDCClaimsRequest requestedClaims =(OIDCClaimsRequest) claimsObject;
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr"));        
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList());
+        assertEquals(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList().size(),3);
+        
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:bronze")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:silver")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:purple")));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /**
+     * Test build ACRs into an existing set of requested claims.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithACRsAndExistingACRsAndClaims_Success() throws ComponentInitializationException {
+        
+        final ClaimsSetRequest.Entry nameEntry = 
+                new ClaimsSetRequest.Entry("first_name").withClaimRequirement(ClaimRequirement.ESSENTIAL);
+        // Add some to the existing requested claims set
+        final ClaimsSetRequest.Entry acrEntry = 
+                new ClaimsSetRequest.Entry("acr").withValues(List.of(new ACR("urn:mace:incommon:iap:bronze")))
+                .withClaimRequirement(ClaimRequirement.ESSENTIAL);
+        final ClaimsSetRequest idTokenClaimsRequest = new ClaimsSetRequest().add(acrEntry).add(nameEntry);
+        final OIDCClaimsRequest requested = 
+                new OIDCClaimsRequest().withIDTokenClaimsRequest(idTokenClaimsRequest);
+        
+        authnRequest.setRequestedClaims(requested);
+        
+        // Now add ACRs
+        authnRequest.setAcrs(List.of(new ACR("urn:mace:incommon:iap:silver"), new ACR("urn:mace:incommon:iap:purple")));
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getNumberClaim("max_age").longValue(),60);
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        
+        final Object claimsObject = claims.getClaim("claims");
+        assertTrue(claimsObject instanceof OIDCClaimsRequest);
+        final OIDCClaimsRequest requestedClaims =(OIDCClaimsRequest) claimsObject;
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("first_name"));
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr"));        
+        assertNotNull(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList());
+        assertEquals(requestedClaims.getIDTokenClaimsRequest().get("acr").getValuesAsRawList().size(),3);
+        
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:bronze")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:silver")));
+        assertTrue(requestedClaims.getIDTokenClaimsRequest().get("acr")
+                .getValuesAsRawList().stream().anyMatch(acr -> acr.toString().equals("urn:mace:incommon:iap:purple")));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /**
+     * Test build request object with requested claims.
+     * 
+     * @throws ComponentInitializationException on error
+     */
+    @Test
+    public void testBuildRequestObject_WithRequestedClaims_Success() throws ComponentInitializationException {
+        
+        final OIDCClaimsRequest requestedClaims =  new OIDCClaimsRequest()
+                .withIDTokenClaimsRequest(new VerifiedClaimsSetRequest().add("given_name"))
+                .withUserInfoClaimsRequest(new VerifiedClaimsSetRequest().add("family_name"));
+        authnRequest.setRequestedClaims(requestedClaims);
+        
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc); 
+        final var outboundMsg = outboundMsgCtx.getMessage();
+        assert outboundMsg != null;
+        assertNotNull(outboundMsg);
+        assertNotNull(((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        final ClaimsSet claims = (((OIDCAuthenticationRequest)outboundMsg).getRequestObjectClaimsSet());
+        assert claims != null;
+        assertEquals(claims.getStringClaim("scope"),"openid");
+        assertEquals(claims.getStringClaim("iss"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("client_id"),TestConstants.CLIENT_ID);
+        assertEquals(claims.getStringClaim("response_type"),"code");
+        assertEquals(claims.getStringClaim("redirect_uri"),TestConstants.RESPONSE_URL);
+        assertNotNull(claims.getStringClaim("nonce"));
+        assertNotNull(claims.getClaim("aud"));
+        assertNotNull(claims.getClaim("claims"));
+        assertTrue(isValidJSON(claims.getClaim("claims").toString()));
+        
+        assertTrue(isValidJSON(claims.toJSONString()));
+    }
+    
+    /** 
+     * Is the string valid JSON.
+     * 
+     * @param json the json to test
+     * 
+     * @return true if valid, false otherwise.
+     */
+    private boolean isValidJSON(final String json)  {
+        try{ 
+            final ObjectMapper objectMapper = new ObjectMapper();
+            objectMapper.readTree(json);
+        } catch(final Exception e){
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/CreateAuthenticationStateDataTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/CreateAuthenticationStateDataTest.java
index 0129b63..79714d0 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/CreateAuthenticationStateDataTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/CreateAuthenticationStateDataTest.java
@@ -14,9 +14,139 @@
 
 package net.shibboleth.sp.oidc.profile.impl;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.nio.charset.StandardCharsets;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.context.StateDataContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.profile.impl.BaseApplicationActionTest;
+
 /**
  * Tests for {@link CreateAuthenticationStateData}.
  */
-public class CreateAuthenticationStateDataTest {
+public class CreateAuthenticationStateDataTest extends BaseApplicationActionTest {
+    
+    /** The action being tested. */
+    private CreateAuthenticationStateData action;
+    
+    /** The RelyingPartyContext to be used in the tests. */
+    private RelyingPartyContext partyContext;    
+    
+    /** The RP config.*/
+    private DefaultOIDCAuthorizationConfiguration rpConfig;
+    
+    /** The AuthenticationRequestStateData to be returned by the lookup strategy. */
+    private AuthenticationRequestStateData authnState;
+    
+    /** The DDF input to be passed to the action. */
+    private DDF input;
+    
+    /** The state context to be created by the action. */
+    private StateDataContext stateContext;
+    
+    @SuppressWarnings("null")
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        
+        action = new CreateAuthenticationStateData();
+
+        
+        partyContext = prc.ensureSubcontext(RelyingPartyContext.class);
+        rpConfig = new DefaultOIDCAuthorizationConfiguration();
+        partyContext.setProfileConfig(rpConfig);
+        prc.addSubcontext(partyContext);
+        
+        authnState = new AuthenticationRequestStateData();
+        
+        input = new DDF(null).structure();
+        arc.setInput(input);
+        input.addmember(SPConstants.TARGET).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8)); 
+        
+        action.setStateDataLookupStrategy(prc -> authnState);
+        stateContext = new StateDataContext();
+        action.setStateDataContextCreationStrategy(prc -> stateContext);
+        
+
+    }
+    
+    @Test
+    public void testSuccess_SuccessfulConstruction() throws ComponentInitializationException {
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        assertEquals(authnState.getResource(), TestConstants.RESOURCE_URL);
+        
+    }
+    
+    @Test
+    public void testFail_NoInput() throws ComponentInitializationException {
+        arc.setInput(null);
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testFail_NoStateData() throws ComponentInitializationException {
+        action.setStateDataLookupStrategy(prc -> null);
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testSuccess_NoTarget() throws ComponentInitializationException {
+        input.getmember(SPConstants.TARGET).remove();
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        assertNull(authnState.getResource());
+    }
+   
+    @Test
+    public void testSuccess_CheckAddress() throws ComponentInitializationException {
+        input.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).addmember(RemotedHttpServletRequest.REMOTE_ADDR)
+            .string("192.168.1.1");
+        rpConfig.setCheckAddress(true);
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        assertEquals(authnState.getResource(), TestConstants.RESOURCE_URL);
+        assertEquals(authnState.getClientAddress(), "192.168.1.1");
+    }
+    
+    @Test
+    public void testSuccess_OldStateHasResource() throws ComponentInitializationException {
+        input.getmember(SPConstants.TARGET).remove();
+        authnState.setResource("https://sp.example.org/old");
+        stateContext.setStateData(authnState);
+        action.initialize();
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        assertEquals(authnState.getResource(), "https://sp.example.org/old");
+    }
 
 }
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategyTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategyTest.java
new file mode 100644
index 0000000..a291e95
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategyTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.parser.JSONParser;
+import net.minidev.json.parser.ParseException;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/** Test for the DefaultClaimMergingStrategy.*/
+public class DefaultClaimMergingStrategyTest {
+    
+    /** Mock response from the UserInfo endpoint.*/
+    @Nonnull @NotEmpty
+    private final String USERINFO_RESPONSE ="{\n"
+            + "  \"sub\": \"jdoe\",\n"
+            + "  \"website\": \"https://openid.net/\",\n"
+            + "  \"zoneinfo\": \"America/Los_Angeles\",\n"
+            + "  \"birthdate\": \"2000-02-03\",\n"
+            + "  \"gender\": \"female\",\n"
+            + "  \"preferred_username\": \"d.tu\",\n"
+            + "  \"given_name\": \"From UserInfo\",\n"
+            + "  \"middle_name\": \"From UserInfo\",\n"
+            + "  \"locale\": \"en-US\",\n"
+            + "  \"updated_at\": 1580000000,\n"
+            + "  \"name\": \"Demo T. User\",\n"
+            + "  \"family_name\": \"User\"\n"
+            + "}";
+    
+    @Test
+    public void testMerge() throws ParseException {
+        
+        final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
+        final ClaimsSet userInfo = new ClaimsSet((JSONObject)parser.parse(USERINFO_RESPONSE));
+        
+        final JWTClaimsSet idToken = new JWTClaimsSet.Builder().subject("jdoe")
+                .claim("given_name", "FromIdToken")
+                .claim("nickname", "FromIdToken")
+                .build();
+        final ClaimsSet idTokenClaims = new ClaimsSet();
+        idTokenClaims.putAll(idToken.getClaims());
+        
+        final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+        final ClaimsSet merged = strategy.apply(userInfo, idTokenClaims);
+        
+        // This is a merged claim
+        assertEquals(merged.getClaim("given_name"), "FromIdToken");
+        
+        // This is a claim only in the id_token
+        assertEquals(merged.getClaim("nickname"), "FromIdToken");
+        
+        // This is a claim only in the userInfo response
+        assertEquals(merged.getClaim("middle_name"), "From UserInfo");
+    }
+    
+    @Test
+    public void testMergeNullUserInfo() throws ParseException {
+        
+        final JWTClaimsSet idToken = new JWTClaimsSet.Builder().subject("jdoe")
+                .claim("given_name", "FromIdToken")
+                .claim("nickname", "FromIdToken")
+                .build();
+        final ClaimsSet idTokenClaims = new ClaimsSet();
+        idTokenClaims.putAll(idToken.getClaims());
+        final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+        final ClaimsSet merged = strategy.apply(null, idTokenClaims);
+        
+
+        assertEquals(merged.getClaim("given_name"), "FromIdToken");        
+        assertEquals(merged.getClaim("nickname"), "FromIdToken");
+        // not in id_token
+        assertNull(merged.getClaim("middle_name"));
+    }
+    
+    @Test
+    public void testMergeNullIdToken() throws ParseException {
+        
+        final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
+        final ClaimsSet userInfo = new ClaimsSet((JSONObject)parser.parse(USERINFO_RESPONSE));
+        
+        final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
+        final ClaimsSet merged = strategy.apply(userInfo, null);
+
+        assertEquals(merged.getClaim("given_name"), "From UserInfo");      
+
+        assertEquals(merged.getClaim("middle_name"), "From UserInfo");
+        // not in UserInfo claims
+        assertNull(merged.getClaim("nickname"));
+
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessTokenTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessTokenTest.java
new file mode 100644
index 0000000..e800cd4
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessTokenTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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 static org.testng.Assert.fail;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.net.URIBuilder;
+import org.mockito.Mockito;
+import org.springframework.http.MediaType;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.oauth2.sdk.TokenErrorResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+
+/**
+ * Tests for the {@link ExchangeCodeForAccessToken} action.
+ */
+public class ExchangeCodeForAccessTokenTest extends BaseOIDCAuthenticationTest {
+    
+    /** The action to test.*/
+    private ExchangeCodeForAccessToken exchangeAction;
+    
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {  
+        super.beforeMethod();        
+        exchangeAction = new ExchangeCodeForAccessToken();      
+    }
+    
+    /** 
+     * Test the token exchange works using a mocked HTTP client and response.
+     * Does not check the signature is valid. Given the amount of mocking, is not
+     * a very thorough test.
+     * 
+     * @throws Exception on error.
+     */
+    @Test
+    public void testTokenExchange() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(
+                new StringEntity(TestConstants.ACCESS_TOKEN_RESPONSE, ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+
+        
+        final OIDCTokenResponse responseToken = OIDCTokenResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+        		(HttpContext) Mockito.any(), (HttpClientResponseHandler<OIDCTokenResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+
+        // create new client with mock response
+        exchangeAction.setHttpClient(httpClient);
+        
+        exchangeAction.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/token")
+                        .build();
+            } catch (final URISyntaxException e) {
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });  
+        
+        // Add a dummy response decoder, it is not used as the response is mocked.
+        exchangeAction.setHttpResponseDecoderStrategy(response -> {
+            try {
+                return OIDCTokenResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+            } catch (final Exception e) {                
+                return null;
+            }
+        });
+        
+        exchangeAction.initialize();
+
+        final Event event = exchangeAction.execute(src);
+        //Null is success
+        assertNull(event);
+
+    }
+    
+    @Test
+    public void testTokenExchange_WrongTokenType() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        
+        final OIDCTokenResponse responseToken = null;
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any(), (HttpClientResponseHandler<OIDCTokenResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+
+
+        // create new client with mock response
+        exchangeAction.setHttpClient(httpClient);
+        
+        exchangeAction.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/token")
+                        .build();
+            } catch (final URISyntaxException e) {
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });     
+        exchangeAction.setHttpResponseDecoderStrategy(response -> {
+            return null;
+        });
+        
+        exchangeAction.initialize();
+
+        final Event event = exchangeAction.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(OidcEventIds.INVALID_TOKEN,event.getId());
+
+    }
+
+    @Test
+    public void testTokenExchange_ErrorResponse() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(httpResponse.getCode()).thenReturn(400);
+        Mockito.when(httpResponse.getEntity()).thenReturn(
+                new StringEntity(TestConstants.TOKEN_RESPONSE_ERROR, ContentType.parse(MediaType.APPLICATION_JSON_VALUE)));
+        
+        final TokenErrorResponse responseToken = TokenErrorResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any(), (HttpClientResponseHandler<TokenErrorResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+
+        // create new client with mock response
+        exchangeAction.setHttpClient(httpClient);
+        
+        exchangeAction.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/token")
+                        .build();
+            } catch (final URISyntaxException e) {
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.getMimeType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });     
+        exchangeAction.setHttpResponseDecoderStrategy(response -> {
+            try {
+                return TokenErrorResponse.parse(convertHttpResponseToJSONObject(httpResponse));
+            } catch (final Exception e) {                
+                fail(e.getMessage());
+                return null;
+            }
+        });
+        
+        exchangeAction.initialize();
+
+        final Event event = exchangeAction.execute(src);
+        assert event != null;
+        assertNotNull(event);
+        assertEquals(OidcEventIds.INVALID_TOKEN,event.getId());
+
+    }
+    
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaimsTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaimsTest.java
new file mode 100644
index 0000000..ea5b35a
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaimsTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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 static org.testng.Assert.fail;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
+
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.EndUserClaimsContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.relyingparty.BasicRelyingPartyConfiguration;
+import net.shibboleth.profile.relyingparty.RelyingPartyConfiguration;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/** Tests for {@link ProcessEndUserClaims}.*/
+public class ProcessEndUserClaimsTest extends BaseOIDCAuthenticationTest {
+    
+    /** The action to test.*/
+    private ProcessEndUserClaims action;
+    
+    /** The RPC.*/
+    private RelyingPartyContext rpc;
+    
+    /** The profile config.*/
+    private DefaultOIDCAuthorizationConfiguration oidcAuthzConfig;
+    
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        action = new ProcessEndUserClaims(); 
+        
+        rpc = prc.ensureSubcontext(RelyingPartyContext.class); 
+        oidcAuthzConfig = new DefaultOIDCAuthorizationConfiguration();
+        final RelyingPartyConfiguration rpConfig = new BasicRelyingPartyConfiguration();
+        rpc.setProfileConfig(oidcAuthzConfig);
+        rpc.setConfiguration(rpConfig);
+
+        // Merge by putting all from each
+        action.setClaimMergingStrategy((idToken, userInfo) -> {
+            final ClaimsSet out = new ClaimsSet();
+            out.putAll(idToken.toJSONObject());
+            out.putAll(userInfo.toJSONObject());
+            return out;
+        });
+        
+        //remove some validation claims
+        action.setClaimSanitizationStrategy(claims -> {
+            final Set<String> validationClaims = Set.of(                  
+                    JWTClaims.ISSUER_CLAIM.getClaimName(),
+                    JWTClaims.AUDIENCE_CLAIM.getClaimName(),
+                    JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName());
+            
+            final ClaimsSet sanitizedClaims = new ClaimsSet();
+            final Map<String, Object> filteredMap = claims.toJSONObject().entrySet()
+                .stream()
+                .filter(c -> !validationClaims.contains(c.getKey()))
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+            sanitizedClaims.putAll(filteredMap);
+            return sanitizedClaims;
+        });
+        
+        action.setAccessTokenResponseContextLookupStrategy(prc -> {
+            final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+            final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+            atrc.setTokenResponse(tokenResponse);
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .issuer("https://op.example.com")
+                    .audience("https://rp.example.com")
+                    .subject("jdoe")
+                    .claim("family_name", "blogs")
+                    .claim("name","jdoe")
+                    .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+                    .build();
+            final OIDCTokens tokens = new OIDCTokens(new PlainJWT(claimsSet), 
+                    new BearerAccessToken("access_token_value"), 
+                    new RefreshToken("refresh_token_value"));
+            Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+            return atrc;
+        });
+        
+        action.setUserInfoResponseContextLookupStrategy(prc -> {
+            final UserInfoResponseContext uirc = new UserInfoResponseContext();
+            final ClaimsSet claims = new ClaimsSet();
+            claims.setClaim("given_name", "joe");
+            claims.setClaim("email", "joe at example.com");
+            claims.setClaim("sub", "jdoe");
+            final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new UserInfo(claims.toJSONObject()));
+            uirc.setUserInfo(uir);
+            return uirc;
+        });
+    }
+    
+    @Test
+    public void testSuccess() throws Exception {
+        action.initialize();
+        action.execute(src);
+
+        assertNotNull(prc.getSubcontext(EndUserClaimsContext.class));
+        final EndUserClaimsContext claimsContext = 
+                prc.getSubcontext(EndUserClaimsContext.class);
+        assert claimsContext != null;
+        final var claims = claimsContext.getEndUserClaims();
+        assert claims != null;
+        assertNull(claims.getClaim("iss"));
+        assertNull(claims.getClaim("aud"));
+        assertEquals(claims.getClaim("sub"), "jdoe");
+        assertEquals(claims.getClaim("family_name"), "blogs");
+        assertEquals(claims.getClaim("given_name"), "joe");
+        assertEquals(claims.getClaim("email"), "joe at example.com");
+    }
+    
+    @Test
+    public void testSuccesfulMerge_ExpirySanatized() throws ComponentInitializationException {
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNotNull(prc.getSubcontext(EndUserClaimsContext.class));
+        final EndUserClaimsContext claimsContext = 
+                prc.getSubcontext(EndUserClaimsContext.class);
+        assert claimsContext != null;
+        final var claims = claimsContext.getEndUserClaims();
+        assert claims != null;
+        assertEquals(claims.getClaim("name"),"jdoe");
+        assertEquals(claims.getClaim("sub"),"jdoe");
+        assertEquals(claims.getClaim("given_name"),"joe");
+        assertEquals(claims.getClaim("email"),"joe at example.com");
+        assertNull(claims.getIssuer());
+        assertNull(claims.getAudience());
+        assertNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+    }
+    
+    @Test
+    public void testSuccesfulMerge_NoSanitization() throws ComponentInitializationException {
+        
+        action.setEnableClaimSanitizationStrategy(false);
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNotNull(prc.getSubcontext(EndUserClaimsContext.class));
+        final EndUserClaimsContext claimsContext = 
+                prc.getSubcontext(EndUserClaimsContext.class);
+        assert claimsContext != null;
+        final var claims = claimsContext.getEndUserClaims();
+        assert claims != null;
+        assertEquals(claims.getClaim("name"),"jdoe");
+        assertEquals(claims.getClaim("sub"),"jdoe");
+        assertEquals(claims.getClaim("given_name"),"joe");
+        assertEquals(claims.getClaim("email"),"joe at example.com");
+        assertNotNull(claims.getIssuer());
+        assertNotNull(claims.getAudience());
+        assertNotNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+    }
+    
+    @Test
+    public void testSuccess_NoUserInfoClaims() throws Exception {
+        
+        action.setUserInfoResponseContextLookupStrategy(prc -> null);
+        
+        action.initialize();
+        action.execute(src);
+        
+        assertNotNull(prc.getSubcontext(EndUserClaimsContext.class));
+        final EndUserClaimsContext claimsContext = 
+                prc.getSubcontext(EndUserClaimsContext.class);
+        assert claimsContext != null;
+        final var claims = claimsContext.getEndUserClaims();
+        assert claims != null;
+        assertNull(claims.getClaim("iss"));
+        assertNull(claims.getClaim("aud"));
+        assertEquals(claims.getClaim("sub"), "jdoe");
+        assertEquals(claims.getClaim("family_name"), "blogs");
+        assertNull(claims.getClaim("given_name"), "joe");
+        assertNull(claims.getClaim("email"), "joe at example.com");
+    }
+    
+    @Test
+    public void testSuccess_NoOIDCToken() throws Exception {
+        
+        action.setAccessTokenResponseContextLookupStrategy(prc -> {
+            final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+            final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+            Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(null);
+            atrc.setTokenResponse(tokenResponse); 
+            return atrc;
+        });
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), EventIds.INVALID_PROFILE_CTX);
+
+    }
+    
+    @Test
+    public void testSuccess_NoIDTokenClaims() throws Exception {
+        
+        action.setAccessTokenResponseContextLookupStrategy(prc -> {
+            final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+            final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+            atrc.setTokenResponse(tokenResponse);
+            try {
+                final OIDCTokens tokens = new OIDCTokens(new PlainJWT(new ClaimsSet().toJWTClaimsSet()), 
+                        new BearerAccessToken("access_token_value"), 
+                        new RefreshToken("refresh_token_value"));
+                Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+            } catch (final ParseException e) {
+               fail(e.getMessage());              
+            }
+            return atrc;
+        });
+        
+        action.initialize();
+        action.execute(src);        
+        
+        assertNotNull(prc.getSubcontext(EndUserClaimsContext.class));
+        final EndUserClaimsContext claimsContext = 
+                prc.getSubcontext(EndUserClaimsContext.class);
+        assert claimsContext != null;
+        final var claims = claimsContext.getEndUserClaims();
+        assert claims != null;
+        assertNull(claims.getClaim("iss"));
+        assertNull(claims.getClaim("aud"));
+        assertEquals(claims.getClaim("given_name"), "joe");
+        assertEquals(claims.getClaim("email"), "joe at example.com");
+
+    }
+    
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookupTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookupTest.java
new file mode 100644
index 0000000..296e595
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookupTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 static org.testng.Assert.fail;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.net.URIBuilder;
+import org.mockito.Mockito;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jose.util.StandardCharset;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+
+/**
+ * Tests for the {@link UserInfoEndpointLookup} action.
+ */
+public class UserInfoEndpointLookupTest extends BaseOIDCAuthenticationTest {
+    
+    /** The action to test.*/
+    private UserInfoEndpointLookup action;
+    
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {        
+        super.beforeMethod();        
+        action = new UserInfoEndpointLookup();
+    }
+    
+    /** 
+     * Test the token exchange works using a mocked HTTP client and response.
+     * Does not check the signature is valid. Given the amount of mocking, is not
+     * a very thorough test.
+     * 
+     * @throws Exception on error.
+     */
+    @Test
+    public void testLookupSuccess() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);      
+        Mockito.when(httpResponse.getCode()).thenReturn(200);
+        Mockito.when(httpResponse.getEntity()).thenReturn(new StringEntity(TestConstants.USERINFO_RESPONSE));
+        
+        final UserInfoSuccessResponse responseToken = new
+                UserInfoSuccessResponse(new UserInfo(convertHttpResponseToJSONObject(httpResponse)));
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any(), (HttpClientResponseHandler<UserInfoSuccessResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+        
+        // create new client with mock response
+        action.setHttpClient(httpClient);
+        
+        action.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/userinfo")
+                        .build();
+            } catch (final URISyntaxException e) {
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });     
+        // Not used, just needed to avoid null response decoder
+        action.setHttpResponseDecoderStrategy(response -> {
+            try {                
+                return new UserInfoSuccessResponse(new UserInfo(convertHttpResponseToJSONObject(httpResponse)));
+            } catch (final Exception e) { 
+                fail(e.getMessage());
+                return null;
+            }
+        });
+        
+        action.initialize();
+
+        final Event event = action.execute(src);
+        //Null is success
+        assertNull(event);
+        final var inboundMsgCtx = prc.getInboundMessageContext();
+        assert inboundMsgCtx != null;
+        final var userInfoCtx = inboundMsgCtx.getSubcontext(UserInfoResponseContext.class);
+        assertNotNull(userInfoCtx);
+        assert userInfoCtx != null;        
+        assertNotNull(userInfoCtx.getUserInfo());
+        final var userInfo = userInfoCtx.getUserInfo();
+        assert userInfo != null;
+        assertEquals(userInfo.getEntityContentType(), ContentType.APPLICATION_JSON);
+        assertNotNull(userInfo.getUserInfo().getClaim("sub"),"248289761001");
+
+    }
+    
+    @Test
+    public void testUserInfoLookup_ErrorResponse() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);        
+        final UserInfoErrorResponse responseToken = UserInfoErrorResponse.parse(TestConstants.USERINFO_RESPONSE_ERROR);
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any(), (HttpClientResponseHandler<UserInfoErrorResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+
+        // create new client with mock response
+        action.setHttpClient(httpClient);
+        
+        action.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/userinfo")
+                        .build();
+            } catch (final URISyntaxException e) {
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });     
+        // This is a just to avoid a null response decoder, is not used.
+        action.setHttpResponseDecoderStrategy(response -> {
+            try {                
+                return UserInfoErrorResponse.parse(TestConstants.USERINFO_RESPONSE_ERROR);
+            } catch (final Exception e) {      
+                fail(e.getMessage());
+                return null;
+            }
+        });
+        
+        action.initialize();
+
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), OidcEventIds.INVALID_USERINFO_CLAIMS);
+
+    }
+    
+    @Test
+    public void testNullUserInfoResponse() throws Exception {
+
+        final HttpClient httpClient = Mockito.mock(HttpClient.class);
+
+        final OIDCTokenResponse responseToken = null;
+        Mockito.when(httpClient.execute((HttpUriRequest) Mockito.any(), 
+                (HttpContext) Mockito.any(), (HttpClientResponseHandler<OIDCTokenResponse>)Mockito.any()))
+                    .thenReturn(responseToken);
+        
+        
+        // create new client with mock response
+        action.setHttpClient(httpClient);
+        
+        action.setHttpRequestEncoderStrategy(prc -> {
+            URI uri;
+            try {
+                uri = new URIBuilder().setScheme("https")
+                        .setHost("op.example.com")
+                        .setPath("/userinfo")
+                        .build();
+            } catch (final URISyntaxException e) {
+                fail(e.getMessage());
+                return null;
+            }                 
+            
+            // Add headers and create request.
+            final ClassicRequestBuilder rb = ClassicRequestBuilder.post().setUri(uri)
+                    .setHeader("Content-Type", ContentType.APPLICATION_URLENCODED.getType())
+                    .setCharset(StandardCharset.UTF_8);
+            return rb.build();
+        });     
+        action.setHttpResponseDecoderStrategy(response -> null);
+        
+        action.initialize();
+
+        final Event event = action.execute(src);
+
+        //Null is success
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(),OidcEventIds.INVALID_USERINFO_CLAIMS);
+
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaimsTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaimsTest.java
new file mode 100644
index 0000000..18d7cbf
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaimsTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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 java.time.Instant;
+import java.util.Date;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
+
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/** Tests for {@link ValidateUserInfoJSONObjectClaims}.*/
+public class ValidateUserInfoJSONObjectClaimsTest extends BaseOIDCAuthenticationTest {
+    
+    /** The action to test*/
+    private ValidateUserInfoJSONObjectClaims action;
+    
+    private UserInfoResponseContext userInfoCtx;
+    
+    private AccessTokenResponseContext accessTokenCtx;
+    
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        action = new ValidateUserInfoJSONObjectClaims();
+        userInfoCtx = new UserInfoResponseContext();
+        accessTokenCtx = new AccessTokenResponseContext();
+        
+        action.setTokenResponseContextLookupStrategy(prc -> {
+            final AccessTokenResponseContext atrc = new AccessTokenResponseContext();
+            final OIDCTokenResponse tokenResponse = Mockito.mock(OIDCTokenResponse.class);
+            atrc.setTokenResponse(tokenResponse);
+            final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                    .issuer("https://op.example.com")
+                    .audience("https://rp.example.com")
+                    .subject("jdoe")
+                    .claim("family_name", "blogs")
+                    .claim("name","jdoe")
+                    .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+                    .build();
+            final OIDCTokens tokens = new OIDCTokens(new PlainJWT(claimsSet), 
+                    new BearerAccessToken("access_token_value"), 
+                    new RefreshToken("refresh_token_value"));
+            Mockito.when(tokenResponse.getOIDCTokens()).thenReturn(tokens);
+            return atrc;
+        });
+        
+        action.setUserInfoResponseContextLookupStrategy(prc -> {
+            final UserInfoResponseContext uirc = new UserInfoResponseContext();
+            final ClaimsSet claims = new ClaimsSet();
+            claims.setClaim("given_name", "joe");
+            claims.setClaim("email", "joe at example.com");
+            claims.setClaim("sub", "jdoe");
+            final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new UserInfo(claims.toJSONObject()));
+            uirc.setUserInfo(uir);
+            return uirc;
+        });
+
+    }
+    
+    @Test
+    public void testValidate_Fail_NoTokenResponse() throws Exception  {       
+        action.setTokenResponseContextLookupStrategy(prc -> new AccessTokenResponseContext());
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testValidate_Fail_NoTokenResponseContext() throws Exception  {
+        action.setTokenResponseContextLookupStrategy(prc -> null);
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testValidate_Fail_NoUserInfoResponseContext() throws Exception  {
+        action.setUserInfoResponseContextLookupStrategy(prc -> null);
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testValidate_Fail_NoUserInfo() throws Exception  {
+        action.setUserInfoResponseContextLookupStrategy(prc ->  new UserInfoResponseContext());
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test
+    public void testValidate_Success() throws Exception  {
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+    }
+    
+    @Test
+    public void testValidate_Success_UserInfoJWT() throws Exception  {
+        action.setUserInfoResponseContextLookupStrategy(prc -> {
+            final UserInfoResponseContext uirc = new UserInfoResponseContext();
+            final JWTClaimsSet claims = new JWTClaimsSet.Builder().subject("jdoe")
+                    .claim("given_name", "joe")
+                    .claim("email", "joe at example.com").build();
+            final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new PlainJWT(claims));
+            uirc.setUserInfo(uir);
+            return uirc;
+        });
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+    }
+    
+    @Test
+    public void testValidate_Fail_SubDoesNotMatchIdToken_UserInfoJWT() throws Exception  {
+        action.setUserInfoResponseContextLookupStrategy(prc -> {
+            final UserInfoResponseContext uirc = new UserInfoResponseContext();
+            final JWTClaimsSet claims = new JWTClaimsSet.Builder().subject("new-sub-does-not-match")
+                    .claim("given_name", "joe")
+                    .claim("email", "joe at example.com").build();
+            final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new PlainJWT(claims));
+            uirc.setUserInfo(uir);
+            return uirc;
+        });
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), OidcEventIds.INVALID_USERINFO_CLAIMS);
+    }
+    
+    @Test
+    public void testValidate_Fail_SubDoesNotMatchIdToken() throws Exception  {
+        action.setUserInfoResponseContextLookupStrategy(prc -> {
+            final UserInfoResponseContext uirc = new UserInfoResponseContext();
+            final ClaimsSet claims = new ClaimsSet();
+            claims.setClaim("given_name", "joe");
+            claims.setClaim("email", "joe at example.com");
+            claims.setClaim("sub", "new-sub-does-not-match");
+            final UserInfoSuccessResponse uir = new UserInfoSuccessResponse(new UserInfo(claims.toJSONObject()));
+            uirc.setUserInfo(uir);
+            return uirc;
+        });
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(), OidcEventIds.INVALID_USERINFO_CLAIMS);
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/resources/metadata/openid-configuration.json b/sp-oidc-impl/src/test/resources/metadata/openid-configuration.json
new file mode 100644
index 0000000..592e3b4
--- /dev/null
+++ b/sp-oidc-impl/src/test/resources/metadata/openid-configuration.json
@@ -0,0 +1,60 @@
+{
+"issuer": "https://op.example.org",
+"authorization_endpoint": "https://op.example.org/auth",
+"token_endpoint": "https://oauth2.op.example.org/token",
+"userinfo_endpoint": "https://op.example.org/userinfo",
+"revocation_endpoint": "https://op.example.org/revoke",
+"jwks_uri": "https://op.example.org/keys",
+"request_parameter_supported" : true,
+"request_object_signing_alg_values_supported" : ["RS256"],
+"response_types_supported": [
+"code",
+"token",
+"id_token",
+"code token",
+"code id_token",
+"token id_token",
+"code token id_token",
+"none"
+],
+"request_object_encryption_enc_values_supported" : ["A128CBC-HS256"],
+"request_object_encryption_alg_values_supported" : ["RSA-OAEP-256"],
+"subject_types_supported": [
+"public"
+],
+"id_token_signing_alg_values_supported": [
+"RS256"
+],
+"scopes_supported": [
+"openid",
+"email",
+"profile"
+],
+"claims_parameter_supported" : true,
+"token_endpoint_auth_methods_supported": [
+"client_secret_post",
+"client_secret_basic"
+],
+"claims_supported": [
+"aud",
+"email",
+"email_verified",
+"exp",
+"family_name",
+"given_name",
+"iat",
+"iss",
+"locale",
+"name",
+"picture",
+"sub"
+],
+"code_challenge_methods_supported": [
+"plain",
+"S256"
+],
+"grant_types_supported": [
+"authorization_code",
+"refresh_token"
+]
+}
\ No newline at end of file
diff --git a/sp-oidc-testing/.checkstyle b/sp-oidc-testing/.checkstyle
deleted file mode 100644
index 1253c35..0000000
--- a/sp-oidc-testing/.checkstyle
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
-  <local-check-config name="Shibboleth Checkstyle" location="/java-plugin-shibd-oidc/resources/checkstyle/checkstyle.xml" type="project" description="">
-    <additional-data name="protect-config-file" value="false"/>
-  </local-check-config>
-  <fileset name="all" enabled="true" check-config-name="Shibboleth Checkstyle" local="true">
-    <file-match-pattern match-pattern="." include-pattern="true"/>
-  </fileset>
-</fileset-config>
diff --git a/sp-oidc-testing/pom.xml b/sp-oidc-testing/pom.xml
deleted file mode 100644
index d88b86d..0000000
--- a/sp-oidc-testing/pom.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- See LICENSE.txt file in the root directory of this repository for the
-copyright/license information. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>net.shibboleth.sp</groupId>
-        <artifactId>sp-oidc-parent</artifactId>
-        <version>0.0.1-SNAPSHOT</version>
-    </parent>
-
-    <artifactId>sp-oidc-testing</artifactId>
-    <name>Shibboleth SP :: OIDC Protocol Plugin :: Testing API</name>
-    <description>Testing APIs for OIDC SP components</description>
-    <packaging>jar</packaging>
-
-    <properties>
-        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
-        <automatic.module.name>net.shibboleth.sp.oidc.testing</automatic.module.name>
-    </properties>
-
-    <dependencies>
-        <!-- Compile dependencies -->
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>sp-oidc-api</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>${project.groupId}</groupId>
-            <artifactId>sp-server-api</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.testng</groupId>
-            <artifactId>testng</artifactId>
-            <scope>compile</scope>
-        </dependency>        
-        <!-- Provided dependencies -->
-        <dependency>
-            <groupId>${shib-profile.groupId}</groupId>
-            <artifactId>shib-profile-impl</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>${oidc-common.groupId}</groupId>
-            <artifactId>oidc-common-crypto-impl</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>${opensaml.groupId}</groupId>
-            <artifactId>opensaml-core-impl</artifactId>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>${oidc-common.groupId}</groupId>
-            <artifactId>oidc-common-crypto-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>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-core</artifactId>
-            <scope>provided</scope>
-        </dependency>
-    </dependencies>
-</project>
\ No newline at end of file

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


More information about the commits mailing list