[java-plugin-shibd-oidc] branch main updated: WIP: Complete the token consumer flow past the UserInfo lookup

Codeberg noreply at shibboleth.net
Wed Nov 19 16:41:13 UTC 2025


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/0a834fc98ed618e78b578332ce4efe7d68ff31d0

The following commit(s) were added to refs/heads/main by this push:
     new 0a834fc  WIP: Complete the token consumer flow past the UserInfo lookup
0a834fc is described below

commit 0a834fc98ed618e78b578332ce4efe7d68ff31d0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Nov 19 16:41:02 2025 +0000

    WIP: Complete the token consumer flow past the UserInfo lookup
    
     - UserInfo lookup is now operational
     - Some UserInfo response validation is in place (inc. signature
    checks). But some have been disabled as they need to be fixed for the
    id_token first.
     - Add a dummy OP signing key and Mock up an OP JWKS response that
    includes the public component of the key
     - Improve the flow test logic
---
 ...bstractUserInfoTokenResponseLookupStrategy.java |  66 ++++
 .../EncryptedUserInfoJWTLookupStrategy.java        |  91 ++++++
 ...nfoInUserInfoResponseContextUpdateStrategy.java |  75 +++++
 .../navigate/UserInfoJWTLookupStrategy.java        |  76 +++++
 .../sp/oidc/exception/OIDCRPException.java         |  65 ----
 .../AbstractUserInfoResponseTypeCondition.java     |  84 +++++
 .../logic/UserInfoPlainResponseTypeCondition.java  |  59 ++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  23 --
 .../idp/flows/sp/consumer/oidc/oidc-beans.xml      | 155 ++++++++-
 .../idp/flows/sp/consumer/oidc/oidc-flow.xml       |  28 +-
 .../shibboleth/idp/flows/sp/oidc-common-beans.xml  |   9 +
 .../net/shibboleth/sp/service/agent/postconfig.xml |  29 +-
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   | 247 +++++++++------
 .../shibboleth/sp/oidc/flows/TestConstants.java    |  74 +++++
 .../shibboleth/sp/oidc/flows/TestTokenHelper.java  | 349 +++++++++++++++++++++
 .../resources/metadata/openid-configuration.json   |   8 +-
 .../idp/module/credentials/op/op-signing-rsa.jwk   |  14 +
 .../net/shibboleth/sp/oidc-test-beans.xml          |  16 +-
 .../impl/AbstractAuthorizationResponseAction.java  |   4 +-
 .../oidc/profile/impl/AbstractHttpOAuthAction.java |  14 +-
 .../profile/impl/DefaultClaimMergingStrategy.java  |  83 +++++
 .../impl/DefaultClaimSanitizationStrategy.java     |  72 +++++
 .../profile/impl/ExchangeCodeForAccessToken.java   |   6 +-
 .../sp/oidc/profile/impl/ProcessEndUserClaims.java | 293 +++++++++++++++++
 .../oidc/profile/impl/UserInfoEndpointLookup.java  | 107 +++++++
 .../sp/oidc/profile/impl/ValidateTokenClaims.java  |   8 +-
 .../impl/ValidateUserInfoJSONObjectClaims.java     | 221 +++++++++++++
 27 files changed, 2017 insertions(+), 259 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java
new file mode 100644
index 0000000..6492be9
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/** Base class for looking up the UserInfo token response context.*/
+public abstract class AbstractUserInfoTokenResponseLookupStrategy {
+    
+    /** Strategy used to look up the {@link UserInfoResponseContext}. */
+    @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext> 
+            userInfoResponseContextLookupStrategy;
+    
+    
+    /** Constructor.*/
+    protected AbstractUserInfoTokenResponseLookupStrategy() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
+                        new InboundMessageContextLookup()); 
+    }
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the strategy used look up the {@link UserInfoResponseContext}.
+     */
+    protected AbstractUserInfoTokenResponseLookupStrategy(final
+            Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        
+        userInfoResponseContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "userInfoResponseContextLookupStrategy can not be null");
+    }
+   
+    /**
+     * Get the UserInfo response context lookup strategy.
+     * 
+     * @return the lookup strategy
+     */
+    @Nonnull 
+    protected Function<ProfileRequestContext, UserInfoResponseContext> getUserInfoResponseContextLookupStrategy() {
+        return userInfoResponseContextLookupStrategy;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java
new file mode 100644
index 0000000..7ea0dc4
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * Function that extracts the UserInfo JWT from the {@link UserInfoResponseContext} iff it is an {@link EncryptedJWT} 
+ * type. If not {@code null} is returned.
+ */
+ at ThreadSafe
+public class EncryptedUserInfoJWTLookupStrategy extends AbstractUserInfoTokenResponseLookupStrategy 
+                                                    implements Function<ProfileRequestContext, JWT>{
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(EncryptedUserInfoJWTLookupStrategy.class);
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the strategy used look up the {@link AccessTokenResponseContext}.
+     */
+    public EncryptedUserInfoJWTLookupStrategy(
+            @ParameterName(name="userInfoResponseContextLookupStrategy") final
+        Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        super(strategy);
+    }
+    
+    /** Constructor.*/
+    public EncryptedUserInfoJWTLookupStrategy() {
+        super();
+    }
+
+    @Override
+    @Nullable public JWT apply(@Nullable final ProfileRequestContext prc) {
+        final UserInfoResponseContext userInfoContext = getUserInfoResponseContextLookupStrategy().apply(prc);
+        final UserInfoSuccessResponse userInfo = userInfoContext != null ? userInfoContext.getUserInfo() : null;
+        if (userInfo == null) {
+            return null;
+        }
+        if (userInfo.getUserInfoJWT() == null) {
+            log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is null, nothing to return");
+            return null;
+        }
+        final JWT token = userInfo.getUserInfoJWT();
+        if (token instanceof EncryptedJWT) {
+            log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is encrypted using algorithm '{}'", 
+                    token.getHeader().getAlgorithm());
+            return token;
+        } else if (token instanceof SignedJWT){
+            log.trace("EncryptedUserInfoToken Lookup:  UserInfo response JWT is signed and not encrypted,"
+                    + " nothing to return");
+            return null;
+        } else {
+            log.trace("EncryptedUserInfoToken Lookup:  UserInfo response JWT is neither signed nor encrypted, "
+                    + "nothing to return");
+            return null;
+        }
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
new file mode 100644
index 0000000..0110089
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.context.navigate;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * Consumer strategy to update the UserInfo JWT in the {@link UserInfoResponseContext}.
+ * Note, replaces the entire {@link UserInfoResponse} object.
+ */
+public class UserInfoInUserInfoResponseContextUpdateStrategy extends AbstractUserInfoTokenResponseLookupStrategy
+                    implements  BiConsumer<ProfileRequestContext, JWT> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(UserInfoInUserInfoResponseContextUpdateStrategy.class);
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the strategy used look up the {@link AccessTokenResponseContext}.
+     */
+    public UserInfoInUserInfoResponseContextUpdateStrategy(
+            @ParameterName(name="userInfoResponseContextLookupStrategy") final
+        Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        super(strategy);
+    }
+    
+    /** Constructor.*/
+    public UserInfoInUserInfoResponseContextUpdateStrategy() {
+        super();
+    }
+
+    @Override
+    public void accept(final ProfileRequestContext profileRequestContext, final JWT token) {
+        
+        final UserInfoResponseContext context = 
+                getUserInfoResponseContextLookupStrategy().apply(profileRequestContext);  
+        if (context != null) {
+            // Create a new UserInfo element with the new JWT response
+            context.setUserInfo(new UserInfoSuccessResponse(token));
+        } else {
+            log.warn("Unable to set UserInfo back onto response context");
+        }
+        
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java
new file mode 100644
index 0000000..951857a
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/** 
+ * Function that extracts the UserInfo claims as a JWT from the {@link UserInfoResponseContext}. If not found, returns
+ * {@code null}.
+ */
+ at ThreadSafe
+public class UserInfoJWTLookupStrategy implements Function<ProfileRequestContext, JWT> {
+    
+    /** Strategy used to look up the {@link UserInfoResponseContext}. */
+    @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext> 
+            userInfoResponseContextLookupStrategy;
+    
+    /** Constructor.*/
+    public UserInfoJWTLookupStrategy() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the UserInfo response context lookup strategy to use.
+     */
+    public UserInfoJWTLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        userInfoResponseContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
+    }
+
+
+    @Override
+    @Nullable public JWT apply(@Nullable final ProfileRequestContext prc) {
+        
+        final UserInfoResponseContext userInfoContext = userInfoResponseContextLookupStrategy.apply(prc);
+        final UserInfoSuccessResponse userInfo = userInfoContext != null ? userInfoContext.getUserInfo() : null;
+        if (userInfo == null || userInfo.getEntityContentType() != ContentType.APPLICATION_JWT) {
+            return null;
+        }
+        return userInfo.getUserInfoJWT();
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java
deleted file mode 100644
index e928416..0000000
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java
+++ /dev/null
@@ -1,65 +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.exception;
-
-import javax.annotation.concurrent.ThreadSafe;
-
-/** 
- * An exception to signal a general OIDC RelyingParty error.
- */
- at ThreadSafe
-public class OIDCRPException extends Exception{
-
-    /** Serial UID. */
-    private static final long serialVersionUID = -2380145079984333546L;
-
-    /** Constructor. */
-    public OIDCRPException() {
-        super();
-        
-    }
-
-    /**
-     * Constructor.
-     * 
-     * @param message exception message
-     * @param cause exception to be wrapped by this one
-     */
-    public OIDCRPException(final String message, final Throwable cause) {
-        super(message, cause);
-        
-    }
-
-    /**
-     * Constructor.
-     * 
-     * @param message exception message
-     */
-    public OIDCRPException(final String message) {
-        super(message);
-        
-    }
-
-    /**
-     * Constructor.
-     * 
-     * @param cause exception to be wrapped by this one
-     */
-    public OIDCRPException(final Throwable cause) {
-        super(cause);
-        
-    }
-
-}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java
new file mode 100644
index 0000000..6d865c6
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.messaging.context.logic;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract predicate for pulling out the {@link UserInfoResponseContext}. If either the
+ * profile request context or extracted UserInfo response context are {@code null} false is returned
+ * immediately. 
+ */
+public abstract class AbstractUserInfoResponseTypeCondition implements Predicate<ProfileRequestContext> {
+    
+    /** Strategy used to look up the {@link UserInfoResponseContext}. */
+    @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext> 
+            userInfoResponseContextLookupStrategy; 
+    
+    /** Constructor.*/
+    protected AbstractUserInfoResponseTypeCondition() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the UserInfo response context lookup strategy to use.
+     */
+    protected AbstractUserInfoResponseTypeCondition(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        userInfoResponseContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
+    }
+    
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext prc) {
+        if (prc == null) {
+            return false;
+        } 
+        final UserInfoResponseContext context = userInfoResponseContextLookupStrategy.apply(prc);
+        if (context == null) {
+            return false;
+        } 
+        return doTest(prc, context);
+        
+    }
+
+    /** 
+     * Implementations should override this method to provide necessary logic.
+     * 
+     * @param prc the profile request context
+     * @param context the UserInfo response context
+     * 
+     * @return the result of this test
+     */
+    protected abstract boolean doTest(@Nonnull final ProfileRequestContext prc, 
+            @Nonnull final UserInfoResponseContext context);
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
new file mode 100644
index 0000000..03e227a
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.messaging.context.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+
+/**
+ * Condition that returns true if the UserInfo response was an plain JSON type i.e. not a signed and or encrypted JWT.
+ */
+public class UserInfoPlainResponseTypeCondition extends AbstractUserInfoResponseTypeCondition {
+    
+    /** Constructor.*/
+    public UserInfoPlainResponseTypeCondition() {
+        super();
+    }
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the UserInfo response context lookup strategy to use.
+     */
+    public UserInfoPlainResponseTypeCondition(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+        super(strategy);
+    }
+
+    @Override
+    protected boolean doTest(@Nonnull final ProfileRequestContext prc, 
+            @Nonnull final UserInfoResponseContext context) {
+        final UserInfoSuccessResponse userInfo = context.getUserInfo();
+        if (userInfo == null) {
+            return false;
+        }
+        return userInfo.getEntityContentType() == ContentType.APPLICATION_JSON &&
+                userInfo.getUserInfo() != null && userInfo.getUserInfoJWT() == null;
+    }
+}
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index cc42e57..1a1057f 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -88,29 +88,6 @@
     </bean>
 
 
-    <!-- Functions use by the flow and global beans TODO: IS there a better place for these -->
-
-    <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContext"
-        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
-        c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext) }" />
-
-    <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound" parent="shibboleth.Functions.Compose">
-        <constructor-arg name="g">
-            <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
-        </constructor-arg>
-        <constructor-arg name="f">
-            <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
-        </constructor-arg>
-    </bean>
-
-    <bean id="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" parent="shibboleth.Functions.Compose"
-        c:g-ref="shibboleth.ChildLookup.OIDCPeerEntityContext" c:f-ref="shibboleth.MessageContextLookup.Outbound" />
-
-    <bean id="shibboleth.ChildLookup.OIDCPeerEntityContext"
-        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
-        c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
-
-
     <!-- OpenID Provider information resolver service beans. -->
     <bean id="shibboleth.authn.oidc.rp.ProviderMetadataResolver"
         class="net.shibboleth.oidc.metadata.impl.ReloadingProviderMetadataProvider"
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index d6bfeae..07fb82c 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -230,8 +230,7 @@
                             <property name="providerMetadataLookupStrategy">
                                 <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
                             </property>
-                        </bean>
-                        
+                        </bean>                        
                     </list>
                 </property>
             </bean>
@@ -258,8 +257,8 @@
     
     <util:list id="IDTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
         <ref bean="IDTokenRequiredClaimsValidator" />
-        <ref bean="IssuerClaimsValidator" />
-        <ref bean="AudienceClaimsValidator" />
+       <!--  <ref bean="IssuerClaimsValidator" /> -->
+        <!-- <ref bean="AudienceClaimsValidator" /> -->
         <ref bean="AzpClaimRequiredValidator" />
         <!-- ref bean="AzpClaimsValidator" /> -->
         <ref bean="ExpiryClaimsValidator" />
@@ -365,7 +364,7 @@
         
     <bean id="AtHashValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.AccessTokenHashValidator"
         p:allowMissing="%{sp.oidc.tokenresponse.allowMissingAtHash:true}"
-        p:accessTokenLookupStrategy="#{getObject('%{sp.oidc.jwt.AccessTokenLookupStrategy;}') ?: 
+        p:accessTokenLookupStrategy="#{getObject('%{sp.oidc.jwt.AccessTokenLookupStrategy:}') ?: 
                                 getObject('DefaultAccessTokenLookupStrategy')}"
         p:joseHeaderLookupStrategy="#{getObject('%{sp.oidc.jwt.IDTokenJOSEHeaderLookupStrategy:}') ?: 
                                 getObject('DefaultIDTokenJOSEHeaderLookupStrategy')}"/>
@@ -405,4 +404,150 @@
                 c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
     
     <!-- - End ID Token Claims Validation -->
+    
+    <bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
+    
+    
+    <bean id="UserInfoEndpointLookup" scope="prototype"
+        class="net.shibboleth.sp.oidc.profile.impl.UserInfoEndpointLookup"
+        p:httpClient="#{getObject('%{sp.oidc.HttpClient:}') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:httpClientSecurityParameters="#{getObject('%{sp.oidc.httpClientSecurityParameters:}')}"
+        p:httpResponseDecoderStrategy="#{getObject('%{sp.oidc.userInfoResponseDecoder:}') ?: getObject('DefaultUserInfoResponseDecoder')}"
+        p:httpRequestEncoderStrategy="#{getObject('%{sp.oidc.userInfoRequestEncoder:}') ?: getObject('DefaultUserInfoRequestEncoder')}" />
+
+    <bean id="DefaultUserInfoResponseDecoder" scope="prototype"
+        class="net.shibboleth.oidc.profile.decoding.impl.UserInfoResponseDecoder"
+        p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}"/>
+
+    <bean id="DefaultUserInfoRequestEncoder" scope="prototype"
+        class="net.shibboleth.oidc.profile.encoding.impl.UserInfoRequestEncoder" 
+         p:providerMetadataLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromInboundMessageContext"
+          p:oAuth2ClientAuthenticationContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromInboundMessageContext"/>
+        
+     <bean id="CheckUserInfoPlainResponseTypeCondition"
+        class="net.shibboleth.sp.oidc.messaging.context.logic.UserInfoPlainResponseTypeCondition" />
+
+     <!-- This is a very simplified and hard coded version of the claims verification used for a JWT. Maybe look to replace -->
+    <bean id="ValidateUserInfoPlainResponseClaims" scope="prototype"
+        class="net.shibboleth.sp.oidc.profile.impl.ValidateUserInfoJSONObjectClaims"/>
+        
+    <bean id="ProcessEndUserClaims" class="net.shibboleth.sp.oidc.profile.impl.ProcessEndUserClaims"
+        scope="prototype"
+        p:claimMergingStrategy="#{getObject('%{sp.oidc.claimMergingStrategy:}') ?: getObject('DefaultClaimMergingStrategy')}"
+        p:claimSanitizationStrategy="#{getObject('%{sp.oidc.claimSanitizationStrategy:}') ?: getObject('DefaultClaimSanitizationStrategy')}" />
+        
+    <bean id="DefaultClaimMergingStrategy"
+        class="net.shibboleth.sp.oidc.profile.impl.DefaultClaimMergingStrategy" />
+
+    <bean id="DefaultClaimSanitizationStrategy"
+        class="net.shibboleth.sp.oidc.profile.impl.DefaultClaimSanitizationStrategy" />
+        
+    <!-- Will populate the same security params context as the id_token, but overwrite the decryption config. -->
+    <bean id="PopulateUserInfoDecryptionParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTDecryptionParameters" scope="prototype"
+        p:decryptionParametersResolver-ref="JWTDecryptionParametersResolver">
+        <property name="configurationLookupStrategy">
+            <bean id="UserInfoTokenDecryptionConfigurationLookup" scope="prototype"
+               class="net.shibboleth.oidc.profile.config.navigate.JWTDecryptionConfigurationLookupFunction"/>
+        </property>
+    </bean>
+    
+    <bean id="DecryptUserInfoJWE" class="net.shibboleth.oidc.security.impl.DecryptJWE" scope="prototype">
+        <property name="jwtTokenLookupStrategy">
+            <bean class="net.shibboleth.sp.oidc.context.navigate.EncryptedUserInfoJWTLookupStrategy"/>
+        </property>
+        <property name="jwtUpdateStrategy">
+            <bean class="net.shibboleth.sp.oidc.context.navigate.UserInfoInUserInfoResponseContextUpdateStrategy" />
+        </property>
+    </bean>
+    
+    <!-- 
+    Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
+    the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
+    have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
+    -->
+    <bean id="UserInfoTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                    
+                        <bean scope="prototype" class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParametersHandler">
+                            <property name="signatureValidationParametersResolver">
+                                <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
+                            </property>
+                            <property name="configurationLookupStrategy">
+                                 <bean class="net.shibboleth.oidc.profile.config.navigate.MessageContextLookupFunctionAdaptor">
+                                    <constructor-arg>
+                                        <bean class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureValidationConfigurationLookupFunction"/>
+                                    </constructor-arg>
+                                </bean>
+                            </property>                            
+                        </bean>  
+                        
+                       <!--  <bean
+                            class="net.shibboleth.idp.plugin.authn.oidc.rp.metadata.impl.OIDCProviderMetadataLookupHandler"
+                            scope="prototype" p:copyContextStrategy-ref="shibboleth.ChildLookup.OutboundOIDCMetadataContextLookup"
+                            p:providerMetadataResolver-ref="shibboleth.authn.oidc.rp.ProviderMetadataResolver" />     -->                        
+                            
+                        <bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
+                            scope="prototype">
+                            <property name="jwtTokenLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+                                    c:expression="#input.getSubcontext(T(net.shibboleth.oidc.profile.context.UserInfoResponseContext)).getUserInfo().getUserInfoJWT()" />
+                            </property>
+                            <property name="providerMetadataLookupStrategy">
+                                <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
+                            </property>
+                        </bean>                        
+                    </list>
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
+    </bean>
+    
+    <bean id="ValidateUserInfoTokenClaims" scope="prototype"
+        class="net.shibboleth.sp.oidc.profile.impl.ValidateTokenClaims"
+        p:cleanupHook="#{getObject('%{sp.oidc.userinfo.jwt.claims.CleanUpHook:}')}" 
+        p:claimsValidator="#{getObject('%{sp.oidc.userinfo.userInfoTokenClaimsValidator:}') 
+           ?: getObject('DefaultUserInfoTokenClaimsValidator')}"
+        p:jwtLookupStrategy="#{getObject('%{sp.oidc.userinfo.userInfoTokenLookupStrategy:}') 
+           ?: getObject('DefaultUserInfoTokenLookupStrategy')}" />
+
+    <bean id="DefaultUserInfoTokenClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="UserInfoClaimsValidators" />
+   
+    <bean id="DefaultUserInfoTokenLookupStrategy"
+        class="net.shibboleth.sp.oidc.context.navigate.UserInfoJWTLookupStrategy" />
+
+    <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="UserInfoTokenRequiredClaimsValidator" />
+        <ref bean="SubMatchesIDTokenClaimValidator" />
+       <!--  <ref bean="IssuerClaimsValidator" />
+        <ref bean="AudienceClaimsValidator" /> -->
+    </util:list>
+
+    <bean id="UserInfoTokenRequiredClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator">
+        <property name="requiredClaims">
+            <list>
+                <value>sub</value>
+            </list>
+        </property>
+    </bean>
+
+    <bean id="SubMatchesIDTokenClaimValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator" p:claimName="sub">
+        <property name="valueToMatchLookupStrategy">
+            <bean class="net.shibboleth.oidc.security.jwt.claims.impl.SubFromIDTokenLookupFunction" />
+        </property>
+    </bean>
+    
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
index 1e054fe..afd13c1 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
@@ -50,57 +50,57 @@
         <evaluate expression="PopulateIDTokenDecryptionParameters" />
         <evaluate expression="DecryptIDTokenJWE" />
         <!--Validation of the JWT signature is optional if TLS server validation was performed -->
-        <!-- <evaluate expression="IDTokenSignatureValidation" /> -->
+        <evaluate expression="IDTokenSignatureValidation" />
     <evaluate expression="ValidateIDTokenClaims" />
       <!--   <evaluate expression="TokenResponsePopulateAuditContext" /> -->
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="proceed" />
+        <transition on="proceed" to="CheckUserInfoClaimsRequired" />
     </action-state>
     
         <!--  Should we use the access_token to request UserInfo claims? -->
     <decision-state id="CheckUserInfoClaimsRequired">
-        <if test="CheckUserInfoRequiredCondition.test(ProxyProfileRequestContextLookup.apply(opensamlProfileRequestContext))"
+        <if test="CheckUserInfoRequiredCondition.test(opensamlProfileRequestContext)"
             then="UserInfoRequest" else="FinalizeResponse" />
     </decision-state>
 
     <action-state id="UserInfoRequest">
-<!--         <evaluate expression="UserInfoEndpointLookup" /> -->
+        <evaluate expression="UserInfoEndpointLookup" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="CheckUserInfoResponseType" />
     </action-state>
 
      <decision-state id="CheckUserInfoResponseType">
-        <if test="CheckUserInfoPlainResponseTypeCondition.test(ProxyProfileRequestContextLookup.apply(opensamlProfileRequestContext))"
+        <if test="CheckUserInfoPlainResponseTypeCondition.test(opensamlProfileRequestContext)"
             then="ValidateUserInfoPlainClaimsSet" else="ValidateUserInfoJWT" />
     </decision-state>
 
     <!-- Actions to perform if the UserInfo response is a JWT type -->
     <action-state id="ValidateUserInfoJWT">
-<!--         <evaluate expression="PopulateUserInfoDecryptionParameters" />
+        <evaluate expression="PopulateUserInfoDecryptionParameters" />
         <evaluate expression="DecryptUserInfoJWE" />
         <evaluate expression="UserInfoTokenSignatureValidation" />
         <evaluate expression="ValidateUserInfoTokenClaims" />
-        <evaluate expression="PostJWTUserInfoResponsePopulateAuditContext" /> -->
+      <!--  <evaluate expression="PostJWTUserInfoResponsePopulateAuditContext" /> -->
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="FinalizeResponse" />
     </action-state>
 
     <!-- Plain UserInfo response types will skip straight to this stage -->
     <action-state id="ValidateUserInfoPlainClaimsSet">
-<!--         <evaluate expression="ValidateUserInfoPlainResponseClaims" />
-        <evaluate expression="PostPlainUserInfoResponsePopulateAuditContext" /> -->
+        <evaluate expression="ValidateUserInfoPlainResponseClaims" />
+    <!--     <evaluate expression="PostPlainUserInfoResponsePopulateAuditContext" /> -->
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="FinalizeResponse" />
     </action-state>
     
     <action-state id="FinalizeResponse">
-<!--         <evaluate expression="ProcessEndUserClaims" />        
-        <evaluate expression="PostResponsePopulateAuditContext" />
-        <evaluate expression="ValidateOIDCAuthentication" />
-        <evaluate expression="PopulateSubjectCanonicalizationContext" />
+        <evaluate expression="ProcessEndUserClaims" />        
+  <!--      <evaluate expression="PostResponsePopulateAuditContext" />-->
+   <!--     <evaluate expression="ValidateOIDCAuthentication" /> -->
+   <!-- <evaluate expression="PopulateSubjectCanonicalizationContext" />
         <evaluate expression="WriteAuditLog" /> -->
         <evaluate expression="'proceed'" />        
-        <transition on="proceed" to="CallSubjectCanonicalization" />   
+        <transition on="proceed" to="proceed" />   
     </action-state>
 
         
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
index 04caae8..920bfcf 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
@@ -132,4 +132,13 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
+        </constructor-arg>
+    </bean>
+
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index fbb3ec0..27924fd 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -35,8 +35,8 @@
     <bean id="shibboleth.oidc.RemoteJwkSetCache"
         class="net.shibboleth.oidc.jwk.RemoteJwkSetCache"
         p:storage-ref="#{'%{idp.oidc.jwk.StorageService:shibboleth.StorageService}'.trim()}"
-        p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
-        p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}" />
+        p:httpClient="#{getObject('%{sp.oidc.HttpClient:}') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:httpClientSecurityParameters="#{getObject('%{sp.oidc.httpClientSecurityParameters:}')}" />
     
     <!-- 
         Security Configuration Defaults. These settings establish the default security configurations for 
@@ -255,11 +255,6 @@
         class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
         <constructor-arg>
             <list>
-                <!-- Used by the OP -->
-                <bean id="ClientInformationCredentialResolver"
-                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
-                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" 
-                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
                 <!--  Used by the RP -->
                 <bean id="OIDCProviderMetadataCredentialResolver"
                     class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
@@ -275,11 +270,6 @@
         class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
         <constructor-arg>
             <list>
-                <!-- Used by the OP -->
-                <bean id="ClientInformationCredentialResolver"
-                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
-                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" 
-                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
                 <!--  Used by the RP -->
                 <bean id="ClientSecretCriterionCredentialResolver"
                     class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -307,11 +297,6 @@
                             c:credentials-ref="shibboleth.oidc.EncryptionCredentials" />
                     </constructor-arg>
                 </bean>
-                <!-- Used by the OP -->
-                <bean id="ClientInformationCredentialResolver"
-                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
-                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" 
-                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
                 <!--  Used by the RP -->
                 <bean id="CriterionCredentialResolver"
                     class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -323,11 +308,6 @@
         class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
         <constructor-arg>
             <list>
-                <!-- Used by the OP -->
-                <bean id="ClientInformationCredentialResolver"
-                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
-                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" 
-                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
                 <!--  Used by the RP -->
                 <bean id="CriterionCredentialResolver"
                     class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -354,11 +334,6 @@
         class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
         <constructor-arg>
             <list>
-                <!-- Used by the OP -->
-                <bean id="ClientInformationCredentialResolver"
-                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
-                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache" 
-                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
                 <!--  Used by the RP -->
                 <bean id="OIDCProviderMetadataCredentialResolver"
                     class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
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 899f49b..a088c9e 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
@@ -14,6 +14,7 @@
 
 package net.shibboleth.sp.oidc.flows;
 
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.fail;
 
 import java.io.ByteArrayInputStream;
@@ -21,6 +22,7 @@ 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;
 import java.util.Date;
 import java.util.HashSet;
@@ -30,24 +32,19 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.apache.hc.client5.http.classic.HttpClient;
-import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.mockito.Mockito;
 import org.opensaml.core.xml.XMLObject;
-import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.io.UnmarshallingException;
 import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.saml.saml2.core.NameID;
 import org.opensaml.saml.saml2.core.NameIDType;
-import org.opensaml.security.SecurityException;
-import org.opensaml.security.credential.Credential;
-import org.opensaml.xmlsec.SignatureSigningParameters;
-import org.opensaml.xmlsec.signature.SignableXMLObject;
-import org.opensaml.xmlsec.signature.support.SignatureConstants;
-import org.opensaml.xmlsec.signature.support.SignatureException;
-import org.opensaml.xmlsec.signature.support.SignatureSupport;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.context.ApplicationContext;
@@ -58,10 +55,10 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.ResponseMode;
 import com.nimbusds.oauth2.sdk.id.State;
@@ -70,9 +67,13 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
 
+import net.minidev.json.JSONObject;
 import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.security.credential.JWKCredential;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.DecodingException;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -97,36 +98,22 @@ import net.shibboleth.sp.profile.ConsumerConstants;
                 }
         )
 @WebAppConfiguration
-public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
-    
-    /** Flow ID. */
-    @Nonnull public static final String FLOW_ID = "sp/token-consumer";
-
-    /** Issuer. */
-    @Nonnull public static final String ISSUER = "https://op.example.org";
-
-    /** Audience. */
-    @Nonnull public static final String AUDIENCE = "https://testsp.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";
+ at SuppressWarnings({ "unchecked", "rawtypes", "null" })
+public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {    
 
     @Autowired
     @Qualifier("shibboleth.SessionIDGenerator")
     protected IdentifierGenerationStrategy idGenerator;
     
-    /** Dummy signing key. */
-    @Autowired @Qualifier("dummy.idp.Credential") protected Credential idpCredential;
+    /** Dummy signing key of the dummy OP we are testing against. */
+    @Autowired @Qualifier("dummy.op.signing.Credential") protected JWKCredential opSigningCredential;
     
     /** The mocked HttpClient to use when responding to Token and UserInfo requests.*/
     private HttpClient httpClient;
 
     /** Constructor. */
     public OIDCTokenConsumerFlowTest() {
-        super(FLOW_ID);
+        super(TestConstants.FLOW_ID);
     }
     
     /** Pre-test work. */
@@ -144,66 +131,65 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
         }
     }
     
+
     /**
-     * Construct a successful OIDC token response.
+     * Test successful flow.
      * 
-     * @param expiry expiry time
-     * @param issuedAt issue time
-     * @return the tokens
+     * @throws IOException 
      */
-    // TODO need to sign the ID token
-    private OIDCTokens constructSuccessfulTokenResponse(@Nonnull final Instant expiry, @Nonnull final Instant issuedAt) {
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Test
+    public void testSuccess_PlainUserInfo() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()), 
+                constructJSONUserInfoResponse());
 
-         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
-                 .subject("fake-user")
-                 .issuer("https://op.example.org")
-                 .audience("mock-client-id")
-                 .expirationTime(Date.from(expiry))
-                     .issueTime(Date.from(issuedAt))
-                     .build();
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
+        final DDF input = buildRemotedQueryStringResponse(response);
         
-         final SignedJWT idToken = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
-         final AccessToken accessToken = new BearerAccessToken("fake-access-token-value", 3600, null);
-         final RefreshToken refreshToken = new RefreshToken("fake-refresh-token-value");
-         return new OIDCTokens(idToken, accessToken, refreshToken);
+        // Add cookies
+        input.addmember("http.headers.Cookie")
+            .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+        
+        setApplicationRequest("test-oidc-application-with-ro", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("testSuccess output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.singleton("mail"), TestConstants.RESOURCE_URL);
     }
     
-
     /**
      * Test successful flow.
      * 
      * @throws IOException 
      */
-    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Test
-    public void testSuccess() throws IOException {
+    public void testSuccess_JWTUserInfo() throws Exception {
         
-        // Create a mocked successful token response
-        final OIDCTokenResponse accessTokenResponse = new OIDCTokenResponse(
-                constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()));    
-        Mockito.when(httpClient.execute((ClassicHttpRequest) Mockito.any(), (HttpContext) Mockito.any(), 
-                (HttpClientResponseHandler) Mockito.any())).thenReturn(accessTokenResponse);
-
+        mockOIDCEndpoints(constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()), 
+                constructJWTUserInfoResponse());     
 
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(RESPONSE_URL, ResponseMode.QUERY, 
-                        "eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0");
-        
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);        
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
-        input.addmember("http.headers.Cookie")
-            .unsafe_string("__Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;".getBytes("UTF-8"));
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
         
         setApplicationRequest("test-oidc-application-with-ro", input);
 
-        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertFlowExecutionResult(result, FLOW_ID);
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
         final DDF output = assertOutputMessageSuccess(result);
         assert output != null;
         System.out.println("testSuccess output: " + output.toString());
-        validateOutputMessage(result, CollectionSupport.singleton("mail"), RESOURCE_URL);
+        validateOutputMessage(result, CollectionSupport.singleton("mail"), TestConstants.RESOURCE_URL);
     }
     
     /**
@@ -214,21 +200,118 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
     @Test
     public void testFail_BadState() throws IOException {
         final AuthenticationSuccessResponse response = 
-                buildOIDCAuthorizationCodeResponse(RESPONSE_URL, ResponseMode.QUERY, "eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0");
-
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
         final DDF input = buildRemotedQueryStringResponse(response);
         
         // Add cookies
         input.addmember("http.headers.Cookie")
-            .unsafe_string("__Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;".getBytes("UTF-8"));
+            .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
 
         setApplicationRequest("test-oidc-application-with-ro", input);
 
-        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertFlowExecutionResult(result, FLOW_ID);
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
         assertOutputMessageEvent(result, AuthnEventIds.NO_CREDENTIALS);
     }
+    
+    /**
+     * Construct a successful OIDC token response.
+     * 
+     * @param expiry expiry time
+     * @param issuedAt issue time
+     * @return the tokens
+     * @throws ParseException 
+     * @throws JOSEException 
+     */
+    private OIDCTokenResponse constructSuccessfulTokenResponse(
+            @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+
+         final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                 .subject("fake-user")
+                 .issuer("https://op.example.org")
+                 .audience("mock-client-id")
+                 .expirationTime(Date.from(expiry))
+                     .issueTime(Date.from(issuedAt))
+                     .build();
+         final JWT signedIdToken = 
+                 TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
+         
+         final AccessToken accessToken = new BearerAccessToken("fake-access-token-value", 3600, null);
+         final RefreshToken refreshToken = new RefreshToken("fake-refresh-token-value");
+         return new OIDCTokenResponse((new OIDCTokens(signedIdToken, accessToken, refreshToken)));
+    }
+    
+    /**
+     * Construct a successful OIDC user info response in JSON format. application/json.
+     * 
+     * @return the user info response
+     */
+    private UserInfoSuccessResponse constructJSONUserInfoResponse() {
+        final JSONObject json = new JSONObject();
+        json.appendField("sub", "fake-user");
+        json.appendField("name", "Fake User");
+        return  new UserInfoSuccessResponse(new UserInfo(json));
+    }
+    
+    /**
+     * Construct a successful OIDC user info response in JWT format.
+     * 
+     * @return the user info response
+     * @throws ParseException 
+     * @throws JOSEException 
+     */
+    private UserInfoSuccessResponse constructJWTUserInfoResponse() throws JOSEException, ParseException {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .subject("fake-user")
+                .issuer("https://op.example.org")
+                .audience("mock-client-id")
+                .claim("name", "Fake User").build();
+       
+        final JWT signedUserInfoToken = 
+                TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
+        return  new UserInfoSuccessResponse(signedUserInfoToken);
+    }
+    
+    /**
+     * Mock OIDC Token, UserInfo, and Keys endpoints. Returning the supplied responses. 
+     * Noting these mocks may not exercise the response decoders, as the responses are returned directly.
+     * 
+     * @param tokenResponse the token response
+     * @param userInfoResponse the user info response
+     * 
+     * @throws IOException on error.
+     */
+    private void mockOIDCEndpoints(final OIDCTokenResponse tokenResponse, 
+            final UserInfoSuccessResponse userInfoResponse) throws IOException {
+        
+        Mockito.when(httpClient.execute(
+                Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/token")),
+                Mockito.any(HttpContext.class),
+                Mockito.any(HttpClientResponseHandler.class)))
+            .thenReturn(tokenResponse);
+
+        Mockito.when(httpClient.execute(
+                Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/userinfo")),
+                Mockito.any(HttpContext.class),
+                Mockito.any(HttpClientResponseHandler.class)))
+            .thenReturn(userInfoResponse);
+        
+        // Fetch from the keys endpoint mocking the call used by the RemoteJwkSetCache     
+        final String jwk = TestTokenHelper.createJWKJSONStringFrom(opSigningCredential);
+        assertNotNull(jwk);
+        final ClassicHttpResponse classicHttpResponse = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(classicHttpResponse.getCode()).thenReturn(200);
+        Mockito.when(classicHttpResponse.getEntity())
+               .thenReturn(new StringEntity("{\"keys\":["+jwk+"]}", ContentType.APPLICATION_JSON));
+
+        Mockito.when(httpClient.executeOpen(
+                Mockito.nullable(HttpHost.class),
+                Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/keys")),
+                Mockito.any(HttpContext.class)))
+            .thenReturn(classicHttpResponse);
+    }
+    
 
     
     /**
@@ -399,38 +482,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
             throws IOException {                
 
         final DDF obj = new DDF(null).structure();
-        obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+        obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
         final DDF http = obj.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
         
         http.addmember(RemotedHttpServletRequest.METHOD).string("GET");
         http.addmember(RemotedHttpServletRequest.REMOTE_ADDR).string("192.168.1.1");
-        http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
+        http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(TestConstants.RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
         final URI responseURI = response.toURI();
         http.addmember(RemotedHttpServletRequest.QUERY_STRING).string(Constraint
                 .isNotNull(responseURI.getQuery(), "Query string is null"));
         return obj;
     }
     
-
-    /**
-     * Sign object.
-     * 
-     * @param signable object to sign
-     * 
-     * @throws IOException 
-     */
-    public void sign(@Nonnull final SignableXMLObject signable) throws IOException {
-
-        final SignatureSigningParameters signingParameters = new SignatureSigningParameters();
-        signingParameters.setSigningCredential(idpCredential);
-        signingParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
-        signingParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
-
-        try {
-            SignatureSupport.signObject(signable, signingParameters);
-        } catch (final SecurityException | MarshallingException | SignatureException e) {
-            throw new IOException(e);
-        }
-    }
-
 }
\ No newline at end of file
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
new file mode 100644
index 0000000..da9c9de
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
@@ -0,0 +1,74 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+/** A class to hold constants used for tests.*/
+public final class TestConstants {
+
+    /** Private constructor.*/
+    private TestConstants() {
+        
+    }
+
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/token-consumer";
+
+    /** Issuer. */
+    @Nonnull public static final String ISSUER = "https://op.example.org";
+
+    /** Audience. */
+    @Nonnull public static final String AUDIENCE = "https://testsp.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";
+
+    /** 
+     * A returned state String consisting of a state and a nonce: 
+     * 
+     * <code>
+     * {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
+     * "nonce":"f26d922221226cd183729bf13247dbe2"}
+     * </code>
+     * */
+    public static final String STATE_STRING = """
+        eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
+        """;
+
+    /**
+     * Cookie headers representing the state string {@literal STATE_STRING}:
+     * 
+     * __Host-_shibsp_req_{state}={nonce};
+     * _Host-shibsp_state__{appId}_{state}={targetURL};
+     * __Host-_shibsp_iss_{state}={issuer};
+     * 
+     */
+    public static final String COOKIE_HEADER = """
+        __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; 
+        _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; 
+        __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
+        """;
+    
+    //TODO this is not bad state
+    public static final String COOKIE_HEADER_BAD_STATE = """
+       __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; 
+       _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; 
+       __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
+        """;
+}
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java
new file mode 100644
index 0000000..dcaffa9
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java
@@ -0,0 +1,349 @@
+/*
+ * 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 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.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.AESEncrypter;
+import com.nimbusds.jose.crypto.DirectEncrypter;
+import com.nimbusds.jose.crypto.ECDHEncrypter;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/** A helper class for generating tokens for testing.*/
+public final class TestTokenHelper {
+    
+    /** Private constructor.*/
+    private TestTokenHelper() {
+        
+    }
+    
+    /**
+     * Build a basic {@link JWTClaimsSet} from the supplied parameters.
+     * 
+     * @param overrideClaims claims to override
+     * 
+     * @return the JWT claims set.
+     */
+    @SuppressWarnings("null")
+    @Nonnull public static JWTClaimsSet createBasicClaims(final Map<String, Object> overrideClaims) {
+        
+        return new JWTClaimsSet.Builder()
+                .issuer(getClaimValue(overrideClaims, "iss", "test-issuer", String.class))
+                .audience(getClaimValue(overrideClaims, "aud", List.of("test-client"), List.class))
+                .subject(getClaimValue(overrideClaims, "sub", "jdoe", String.class))
+                .claim("preferred_username", getClaimValue(overrideClaims, "preferred_username", "d.tu", String.class))
+                .claim("given_name", getClaimValue(overrideClaims, "given_name", "Demo", String.class))
+                .claim("family_name", getClaimValue(overrideClaims, "family_name", "User", String.class))
+                .claim("nonce", getClaimValue(overrideClaims, "nonce", "abadnonce", String.class))
+                .claim("nickname", getClaimValue(overrideClaims, "nickname", "Dee", String.class))
+                .claim("azp", getClaimValue(overrideClaims, "azp", "test-client", String.class))
+                .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class))
+                .claim("acr",getClaimValue(overrideClaims, "acr", "urn:mace:incommon:iap:silver", String.class))
+                .claim("amr",getClaimValue(overrideClaims, "amr", List.of("pwd", "otp"), List.class))
+                .claim("auth_time", getClaimValue(overrideClaims, "auth_time", new Date(), Date.class))
+                .issueTime(getClaimValue(overrideClaims, "iat", new Date(), Date.class))
+                .expirationTime(getClaimValue(overrideClaims, "auth_time", 
+                        Date.from(Instant.now().plusSeconds(120)), Date.class))
+                .build();
+    }
+    
+    /** 
+     * Create a JWK JSON string from the supplied JWKCredential. Return an empty string if the key type is 
+     * not supported.
+     * 
+     * @param credential the JWK credential
+     * @return the JWK JSON string, or empty string if the key type is not supported.
+     */
+    @Nonnull public static String createJWKJSONStringFrom(final JWKCredential credential) {
+        final PublicKey publicKey = credential.getPublicKey();        
+        if (publicKey instanceof final RSAPublicKey rsa) {
+            final RSAKey publicKeyJWK = new RSAKey.Builder(rsa)
+                .keyID(credential.getKid())
+                .algorithm(credential.getAlgorithm())
+                .keyUse(KeyUse.SIGNATURE)
+                .build();
+            return publicKeyJWK.toJSONString();
+        }
+        return "";
+    }
+    
+    /**
+     * Build a basic {@link JWTClaimsSet} from the supplied parameters to mock a UserInfo response.
+     * 
+     * @param overrideClaims claims to override
+     * 
+     * @return the JWT claims set.
+     */
+    @SuppressWarnings("null")
+    @Nonnull public static JWTClaimsSet createBasicUserInfoClaims(final Map<String, Object> overrideClaims) {
+        
+        return new JWTClaimsSet.Builder()
+                .issuer(getClaimValue(overrideClaims, "iss", "test-issuer", String.class))
+                .audience(getClaimValue(overrideClaims, "aud", CollectionSupport.listOf("test-client"), List.class))
+                .subject(getClaimValue(overrideClaims, "sub", "jdoe", String.class))
+                .claim("preferred_username", getClaimValue(overrideClaims, "preferred_username", "d.tu", String.class))
+                .claim("given_name", getClaimValue(overrideClaims, "given_name", "Demo", String.class))
+                .claim("family_name", getClaimValue(overrideClaims, "family_name", "User", String.class))
+                .claim("nickname", getClaimValue(overrideClaims, "nickname", "Dee", String.class))
+                .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class))
+                .build();
+    }
+    
+    /** 
+     * Extract any override from the overrides map, or return the default value if there is none.
+     * 
+     * @param <T> the type to look for and return
+     * @param overrideClaims the overrides map
+     * @param claimName the claim name
+     * @param defaultValue the default value if no override is found
+     * @param type the type to return
+     * 
+     * @return the override or the default value.
+     */
+    private static <T> T getClaimValue(final Map<String, Object> overrideClaims, final String claimName, 
+            final Object defaultValue,  final Class<T> type) {
+        
+        if (overrideClaims.containsKey(claimName)) {
+            final Object override = overrideClaims.get(claimName);
+            if (override == null) {
+                return null;
+            }
+            if (type.isInstance(override)) {
+                //check empty string case, set null if so
+                if (override instanceof String && ((String)override).isEmpty()) {
+                    return null;
+                }
+                return type.cast(override);
+            }
+        }
+        return type.cast(defaultValue);
+    }
+    
+    /**
+     * Create a JWT from the given payload. The JWT can either be plain, or signed and encrypted. If encrypted, it must
+     * be signed. 
+     * 
+     * @param payload the payload to create the JWT from
+     * @param sigAlg the signature alg to use. {@code Null} if the JWT is not going to be signed and/or encrypted.
+     * @param jweAlg the encryption alg to use. {@code Null} if the JWT is not going to be encrypted. Note, if encrypted
+     *                  the signature algorithm must also be supplied, otherwise a plain JWT will be returned.
+     * @param enc the content encryption algorithm to use.
+     * @param sigKey the key to use for signing
+     * @param encKey the key to use for encryption
+     * 
+     * @return and plain JWT, JWS, or JWE.
+     * 
+     * @throws JOSEException on error.
+     * @throws ParseException on error.
+     */
+    @Nonnull public static JWT createJWT(@Nonnull final JWTClaimsSet payload, @Nullable final JWSAlgorithm sigAlg, 
+            @Nullable final JWEAlgorithm jweAlg, @Nullable final EncryptionMethod enc, 
+            @Nullable final Credential sigKey, @Nullable final Credential encKey) throws JOSEException, ParseException {
+        
+        // Sign first.
+        if (sigAlg != null && sigKey != null) {
+            final var header = new JWSHeader.Builder(sigAlg)
+                    .type(JOSEObjectType.JWT)
+                    .build();
+            final var signedJWT = new SignedJWT(header,payload);
+            
+            if (JWSAlgorithm.Family.HMAC_SHA.contains(sigAlg)){
+                signedJWT.sign(new MACSigner(sigKey.getSecretKey()));
+            } else if (JWSAlgorithm.Family.RSA.contains(sigAlg)) {
+                signedJWT.sign(new RSASSASigner(sigKey.getPrivateKey()));
+            } else if (JWSAlgorithm.Family.EC.contains(sigAlg)) {
+                signedJWT.sign(new ECDSASigner((ECPrivateKey)sigKey.getPrivateKey()));
+            }
+            
+            if (jweAlg != null && encKey != null) {
+                final JWEObject jweObject = 
+                        new JWEObject(new JWEHeader.Builder(jweAlg, enc)
+                        .contentType("JWT")
+                        .build(),
+                        new Payload(signedJWT));
+                
+                if (JWEAlgorithm.Family.RSA.contains(jweAlg)) {                    
+                    jweObject.encrypt(new RSAEncrypter((RSAPublicKey)encKey.getPublicKey()));                    
+                    final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+                    assert encJwt != null;
+                    return encJwt;
+                    
+                } else if (JWEAlgorithm.Family.AES_KW.contains(jweAlg) 
+                        || JWEAlgorithm.Family.AES_GCM_KW.contains(jweAlg)) {
+                    
+                    jweObject.encrypt(new AESEncrypter(encKey.getSecretKey()));
+                    final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+                    assert encJwt != null;
+                    return encJwt;
+                    
+                } else if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlg)) {
+                    jweObject.encrypt(new ECDHEncrypter((ECPublicKey) encKey.getPublicKey()));
+                    final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+                    assert encJwt != null;
+                    return encJwt;
+                } else if (JWEAlgorithm.DIR == jweAlg) {
+                    jweObject.encrypt(new DirectEncrypter(encKey.getSecretKey()));
+                    final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+                    assert encJwt != null;
+                    return encJwt;
+                }
+            } else {
+                return signedJWT;
+            }
+        } 
+        return new PlainJWT(payload);        
+    }
+    
+    /**
+     * Create a JWT UserInfo response. Can be a JWS or JWE depending on the input values.
+     * @param overrideClaims claims to override
+     * @param sigAlg the signature algorithm
+     * @param jweAlg the encryption algorithm
+     * @param enc the content encryption algorithm
+     * @param sigKey the signing key
+     * @param encKey the encryption key
+     * 
+     * @return the created JWT
+     * @throws JOSEException on error
+     * @throws ParseException on error
+     */
+    @Nonnull public static JWT createJWTUserInfoResponse(final Map<String, Object> overrideClaims,
+            @Nullable final JWSAlgorithm sigAlg, @Nullable final JWEAlgorithm jweAlg, 
+            @Nullable final EncryptionMethod enc, @Nullable final Credential sigKey, @Nullable final Credential encKey) 
+                    throws JOSEException, ParseException {
+        
+        final var payload = createBasicUserInfoClaims(overrideClaims);
+        return createJWT(payload, sigAlg, jweAlg, enc, sigKey, encKey);
+        
+    }
+    
+    /**
+     * Create an access token response JSON object and build a suitable id_token to include in the response. Any claim
+     * contained in the overrides list overrides the default value built by the basic claims method.
+     * 
+     * @param overrideClaims claims to override in the id_token
+     * @param sigAlg the signature algorithm
+     * @param jweAlg the encryption algorithm
+     * @param enc the content encryption algorithm
+     * @param sigKey the signing key
+     * @param encKey the encryption key
+     * 
+     * @return an access token response JSON object
+     * 
+     * @throws Exception on error.
+     */
+    @Nonnull public static String createAccessTokenResponseJSON(final Map<String, Object> overrideClaims,
+            @Nullable final JWSAlgorithm sigAlg, 
+            @Nullable final JWEAlgorithm jweAlg, @Nullable final EncryptionMethod enc, 
+            @Nullable final Credential sigKey, @Nullable final Credential encKey) throws Exception {
+        
+        
+        final var payload = createBasicClaims(overrideClaims);
+        final JWT idToken = createJWT(payload, sigAlg, jweAlg, enc, sigKey, encKey);
+        
+        return buildTemplateAccessTokenJSONResponse(idToken.serialize());
+    }
+    
+    
+    /**
+     * Create a plain JSON object based UserInfo response.
+     * 
+     * @param overrideClaims claims to override
+     * 
+     * @return a plain JSON object representing the UserInfo response
+     * 
+     * @throws JsonProcessingException on error
+     */
+    @Nonnull public static String createPlainUserInfoResponseString(final Map<String, Object> overrideClaims) 
+            throws JsonProcessingException {
+        final JWTClaimsSet claims = createBasicUserInfoClaims(overrideClaims);
+        final String valueAsString = new ObjectMapper().writeValueAsString(claims.toJSONObject());
+        assert valueAsString != null;
+        return valueAsString;
+    }
+    
+
+    /**
+     * Create a Plain UserInfo response JWT.
+     * 
+     * @param overrideClaims claims to override
+     * 
+     * @return the signed JWT
+     * 
+     * @throws JOSEException on error
+     */
+    @Nonnull public static PlainJWT createPlainJWTUserInfoResponseJSON(final Map<String, Object> overrideClaims) 
+            throws JOSEException {        
+        
+        final var payload = createBasicUserInfoClaims(overrideClaims);        
+        return new PlainJWT(payload);
+    }
+
+    
+    /**
+     * Build a simple OAuth2.0/OIDC Access Token JSON response using the serialized JWT.
+     * 
+     * @param serializedJWT the id_token serialized
+     * 
+     * @return An access token response
+     */
+    @Nonnull private static String buildTemplateAccessTokenJSONResponse(final String serializedJWT) {
+        return "{\n"
+                + "  \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+                + "  \"token_type\": \"Bearer\",\n"
+                + "  \"id_token\": \""+serializedJWT+"\",\n"
+                + "  \"scope\": \"openid\"\n"
+                + "}";
+    }
+
+}
diff --git a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
index 9433cbb..592e3b4 100644
--- a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
+++ b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
@@ -1,10 +1,10 @@
 {
 "issuer": "https://op.example.org",
-"authorization_endpoint": "https://op.example.org/o/oauth2/v2/auth",
+"authorization_endpoint": "https://op.example.org/auth",
 "token_endpoint": "https://oauth2.op.example.org/token",
-"userinfo_endpoint": "https://openidconnect.op.example.org/v1/userinfo",
-"revocation_endpoint": "https://oauth2.op.example.org/revoke",
-"jwks_uri": "https://op.example.org/oauth2/v3/certs",
+"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": [
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk
new file mode 100644
index 0000000..2f01609
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk
@@ -0,0 +1,14 @@
+{
+  "kty": "RSA",
+  "use": "sig",
+  "alg": "RS256",
+  "kid": "op-signing-key",
+  "d": "bnhwivAdsfkabz_kVXMoFDwcUCTtx5v8QtMhweQ_2WHIHV56ViOu7eYR6MmLV-yrLukgazOlxXGAUNV_ET_yjiTVvDFdpMynOUI2ZrFzoEdu9l7DVgOG5l-_CvKpKh4P14gLV0HawYSZ2aJEPaHP5NgIgYa9oMm4c_uVRzc-OiQAn7THeVInfeaXsDavCB-YdJmsuIfL-GWdbz8t3J6yGRuoVXtIx2b9rkFOWA6DLTuTRXYpBT92qfxk7s-e2x3KYpUr14sbPR68Jb7O48k5NmIEUcKdCysi9NDBAgwmzkHPXep0mf3S2arHd1u8J14nPe_HSkF9mY6pLhD_qaM4qQ",
+  "n": "oRDRa8TF-kZw1ezz7CHWmovmgnkdMl3-PTVvkzuX4MECDoyIkyczWH97C1poGBorjfvIHtKpBHjoeCJDCXW4Eeb8ZSRVpUbh_0GkIUJcy9xSIqMBv0XohhSaGun1zHFHEvEPi0E-ljx08mlwxq8C5Zuim3miNV8yaZEWTpjSgMyUdPSyBcaAQKMQnsawFsZaAyc528NwM14w0cQbd5dHRgnxilb7eHSOKEv0GzewxsKDlhGJ3m7h_wSpVieVqKnZ9j7wXlpXJD0X85bI7pBlEm4xUoRnojONfSUdoou2EDb7uheVfJRdkNuoSu6aKcL73ZJLu82Mofwcpstg3KL5nQ",
+  "e": "AQAB",
+  "p": "x5ClON9tpwcH4adEZ5jqMMJs8xSl7aVFNqzDPThrhlgOWevoP51GEPlqQENqwfqIrHJpuElC9vPeUdL0bK-fZ2FFjsnHstKZLBHXIVHMBQ5jY2f11mTc6d1e-mejKzvDn7tl-kRyrR4kxqABrR0MOSLZUzR3UBv6aRH3kl0ISO8",
+  "q": "zp0LPOHncNTVpDoMug-hlpaoBX-AK8sEFok2gsKLhTMFhyROdpZIPJHKYgSJijuqaL-pg1tIWWLFNdu5H_Vy-v3cGveM37UUNmUzQKxzeXnESD1HwoQPhD5mGDAu8RRY8nPkFfXEvQ9qKRW89rDqqdRSioIdl7WVP7yLd7A2rjM",
+  "dp": "kOl64Gp1sFTNp1ETvfpvDEsSXA0BVCivsr0O5NSkV6B3g1pnglIM_-RtAA80ZXksZ7SJCjXAt5xsMpRxwr5gjOw4WzAwn6yHZ1XpFpvT_1PsXdGu1SjWtcd24XQCKzGxorqsmWe8sfLyl3y22uL97jtc_itZ_ETkuAlA8fo2Ouc",
+  "dq": "Gi-JJxQycvhmg0PgLQyiHCXH8bpxEhvOztRhFj111VHcF36geyMc0KBhl_6fN-fL9y6jW0SMbNe5ytOoKMbA5dKavMg3EHswrpww3Ld-gxzNpaIuoBaAqPAHnHUu3dsIUuIhPC9D2fpEKCTbKCDP_Oot8-P9wCaBXeyVeHva0kU",
+  "qi": "DuuQsro41wxx5BZ-BbOpSmDbK60qkZa2jU3jcNe9V6AuhlInYo0-9QPyuETqzYyWd8oMGnLEd9-StXB7UOsK0pmAiv0R8ALIKci0a64z27Rw8wuQoMZMW1SOYS_zhG6p2iK2LXHy-rzr1PAKEGOEK7DA16zXFqwNqky2PNX-teg"
+}
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
index caf31d8..4a969c3 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
@@ -19,23 +19,17 @@
     <util:list id="test.sp.oidc.AgentResolverResources">
         <value>%{idp.home}/conf/sp/oidc-test-agents.xml</value>
     </util:list>
-
-    <bean id="dummy.idp.X509Certificate" class="net.shibboleth.shared.spring.security.factory.X509CertificateFactoryBean"
-        p:resource="%{idp.home}/credentials/idp-signing.crt" />
-
-    <bean id="dummy.idp.PrivateKey" class="net.shibboleth.shared.spring.security.factory.PrivateKeyFactoryBean"
-        p:resource="%{idp.home}/credentials/idp-signing.key" />
-
-     <bean id="dummy.idp.Credential" class="org.opensaml.security.x509.BasicX509Credential"
-        c:entityCertificate-ref="dummy.idp.X509Certificate"
-        c:privateKey-ref="dummy.idp.PrivateKey"
-        p:entityId="https://idp.example.org" />
      
     <bean id="dummy.oidc.BasicEncryptionConfiguration" abstract="true"
         class="net.shibboleth.oidc.security.jose.impl.BasicEncryptionConfiguration"
         p:includedAlgorithms="#{getObject('shibboleth.oidc.IncludedEncryptionAlgorithms')}"
         p:excludedAlgorithms="#{getObject('shibboleth.oidc.ExcludedEncryptionAlgorithms')}" />
+    
+    <bean id="shibboleth.oidc.JWKCredential" abstract="true"
+        class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean" />
 
+    <bean id="dummy.op.signing.Credential" parent="shibboleth.oidc.JWKCredential"
+        p:resource="%{idp.home}/credentials/op/op-signing-rsa.jwk" p:throwIfNull="false" />
 
     <!-- Mockito mock for HttpClient -->
     <bean id="Mock.HttpClient"
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
index 3730631..9ff06d6 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
@@ -30,11 +30,11 @@ import org.slf4j.Logger;
 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 
+import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
 import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.profile.AbstractAgentAction;
 
 /**
  * Abstract class for actions based on the {@link AuthenticationResponse} located under
@@ -42,7 +42,7 @@ import net.shibboleth.sp.profile.AbstractAgentAction;
  * 
  * <p>Also makes available the OpenID Provider metadata context.</p>
  */
-public abstract class AbstractAuthorizationResponseAction extends AbstractAgentAction {
+public abstract class AbstractAuthorizationResponseAction extends AbstractProfileAction {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthorizationResponseAction.class);
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
index a710590..eecb03c 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
@@ -25,6 +25,7 @@ import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.security.httpclient.HttpClientSecurityParameters;
 import org.opensaml.security.httpclient.HttpClientSecuritySupport;
@@ -40,7 +41,6 @@ import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
 
 /**
  * An abstract class for OIDC/OAuth actions that make synchronous HTTP requests and return types of 
@@ -159,25 +159,25 @@ public abstract class AbstractHttpOAuthAction<T extends Response>
      * 
      * @return a successful decoded response. Never {@code null}. 
      * 
-     * @throws OIDCRPException on error making the request, or if an error response is returned.
+     * @throws MessageHandlerException on error making the request, or if an error response is returned.
      */
     @Nonnull protected T handleRequest(@Nonnull final ProfileRequestContext profileRequestContext, 
-                @Nullable final AbstractAuthenticatableOIDCContext authenticatableContext) throws OIDCRPException {
+                @Nullable final AbstractAuthenticatableOIDCContext authenticatableContext) throws MessageHandlerException {
         try {              
             final ClassicHttpRequest request = getHttpRequestEncoderStrategy().apply(profileRequestContext);
             if (request == null) {
-                throw new OIDCRPException("Unable to encode HTTP request");
+                throw new MessageHandlerException("Unable to encode HTTP request");
             }
             final T response = executeHttpRequest(request, authenticatableContext);
             if (response == null) {
-                throw new OIDCRPException("Unable to process HTTP response");                
+                throw new MessageHandlerException("Unable to process HTTP response");                
             } else if (!response.indicatesSuccess()) {
-                throw new OIDCRPException(formatErrorResponse(((ErrorResponse)response).getErrorObject()));
+                throw new MessageHandlerException(formatErrorResponse(((ErrorResponse)response).getErrorObject()));
             }
             return response;            
         } catch (final IOException e) {
             log.error("{} Unable to perform HTTP request and return response",getLogPrefix(),e);
-            throw new OIDCRPException(e);
+            throw new MessageHandlerException(e);
         }  
     }
     
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java
new file mode 100644
index 0000000..6e6d1b7
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BinaryOperator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A default merging strategy for combing claims in the UserInfo response with those from the id_token. 
+ * <ol>
+ * <li>If one of UserInfo or id_token claims are null, the other is returned.</li>
+ * <li>If both input claims are null, an empty claims set is returned.</li>
+ * <li>Merges the id_token claims into the UserInfo claims, the value of a claim from the id_token
+ * is taken over that from the UserInfo response if the claim keys clash.</li>
+ * </ol>
+ */
+public class DefaultClaimMergingStrategy implements BinaryOperator<ClaimsSet> {
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultClaimMergingStrategy.class);
+
+    @Override
+    @Nonnull public ClaimsSet apply(@Nullable final ClaimsSet userInfo, @Nullable final ClaimsSet idToken) {
+        
+        if (userInfo == null && idToken != null) {
+            final ClaimsSet singleSet = new ClaimsSet();
+            singleSet.putAll(idToken.toJSONObject());
+            return singleSet;
+        }
+        if (userInfo != null && idToken == null) {
+            final ClaimsSet singleSet = new ClaimsSet();
+            singleSet.putAll(userInfo.toJSONObject());
+            return singleSet;
+        }
+        // Now catch if either is null
+        if (userInfo == null || idToken == null) {
+            // return empty claimsset
+            return new ClaimsSet();
+        }
+        
+        final Map<String, Object> idTokenAsMap = idToken.toJSONObject();
+        // Treat JSONObject as the base map representation.
+        final Map<String, Object> userInfoClaimsAsMap = userInfo.toJSONObject();
+        
+        // Add UserInfo claims as a base
+        final Map<String, Object> mergedClaimsMap = new HashMap<>(userInfoClaimsAsMap);
+        
+        // Merge id_token claims into userInfo claims, take id_token claim if conflict
+        idTokenAsMap.forEach((key, value) ->
+            mergedClaimsMap.merge(key, value, (v1, v2)-> {
+                log.trace("Claim '{}' exists in id_token and UserInfo response, taking id_token value '{}'",key, v2);
+                return v2;   
+            }));
+
+        
+        final ClaimsSet mergedClaimsSet = new ClaimsSet();
+        mergedClaimsSet.putAll(mergedClaimsMap);
+        return mergedClaimsSet;
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java
new file mode 100644
index 0000000..3991bfb
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.oidc.security.jwt.claims.impl.IDTokenClaims;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
+
+/**
+ * Produce a claims set from the JWT claims set without either the validation claims or claims with null values.
+ * Leaving the identity, authorization, and misc. claims.
+ * 
+ */
+public class DefaultClaimSanitizationStrategy implements UnaryOperator<ClaimsSet> {
+    
+    /** The set of validation claims to filter out of the input claims.*/
+    @Nonnull private final Set<String> validationClaims;
+    
+    /** Constructor.*/
+    public DefaultClaimSanitizationStrategy() {
+        final Set<String> validationClaimsBuilt = Set.of(IDTokenClaims.AUTHORIZED_PARTY.getClaimName(),
+                IDTokenClaims.NONCE.getClaimName(),
+                IDTokenClaims.AUTHENTICATION_TIME.getClaimName(),
+                IDTokenClaims.AUTHENTICATION_CONTEXT_CLASS_REFERENCE.getClaimName(),
+                IDTokenClaims.AUTHENTICATION_METHODS_REFERENCES.getClaimName(),
+                JWTClaims.ISSUER_CLAIM.getClaimName(),
+                JWTClaims.ISSUED_AT_CLAIM.getClaimName(),
+                JWTClaims.AUDIENCE_CLAIM.getClaimName(),
+                JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName(),
+                "at_hash");
+        assert validationClaimsBuilt != null;
+        validationClaims = validationClaimsBuilt;
+    }
+
+    @Override
+    public ClaimsSet apply(@Nullable final ClaimsSet jwtClaims) {
+        if (jwtClaims == null) {
+            return new ClaimsSet();
+        }
+        final ClaimsSet sanitizedClaims = new ClaimsSet();
+        final Map<String, Object> filteredMap = jwtClaims.toJSONObject().entrySet()
+            .stream()
+            .filter(c -> c.getValue() != null)
+            .filter(c -> !validationClaims.contains(c.getKey()))            
+            .filter(c -> c.getKey() != null)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        sanitizedClaims.putAll(filteredMap);
+        return sanitizedClaims;
+    }
+
+}
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 c372a70..c353322 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
@@ -20,6 +20,7 @@ import javax.annotation.Nonnull;
 
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.MessageHandlerException;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -35,7 +36,6 @@ import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
 import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
 
 
 /**
@@ -108,10 +108,10 @@ public class ExchangeCodeForAccessToken extends AbstractHttpOAuthAction<TokenRes
                   log.trace("{} Token request response was successful '{}'",getLogPrefix(), 
                 		  tokenResponse.indicatesSuccess());
               } else {
-                  throw new OIDCRPException("Token response was not of the expected format, expected OIDC token "
+                  throw new MessageHandlerException("Token response was not of the expected format, expected OIDC token "
                           + "response, got " + responseObject.getClass().getSimpleName());
               }           
-          } catch (final OIDCRPException e) {
+          } catch (final MessageHandlerException e) {
               log.error("{} Failed to exchange authorisation code for token result",getLogPrefix(), e);
               ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
           }          
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
new file mode 100644
index 0000000..859c88d
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.EndUserClaimsContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Process the end-user claims from the id_token and possible UserInfo claims sets.
+ *  
+ * <p>Sanitized both claims sets using a replaceable strategy. For example, 
+ * by default to remove standard JWT 'validation' claims that should not be exposed further by the system.</p>
+ * 
+ * <p>Merge the claims sets together to produce an aggregate claims set. The UserInfo claims can 
+ * be empty i.e. claims from the UserInfo endpoint were not requested.</p>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @post Add a set of merged and sanatized claims to the {@link EndUserClaimsContext}.
+ */
+public class ProcessEndUserClaims extends AbstractProfileAction {
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessEndUserClaims.class);
+    
+    /** Strategy used to look up the {@link UserInfoResponseContext}. */
+    @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext> 
+            userInfoResponseContextLookupStrategy;
+    
+    /** Strategy used to look up the {@link AccessTokenResponseContext} . */
+    @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext> 
+            accessTokenResponseContextLookupStrategy;
+    
+    /** Strategy used to look up the {@link EndUserClaimsContext} to set the parameters for. */
+    @Nonnull private Function<ProfileRequestContext, EndUserClaimsContext> 
+            endUserClaimsContextLookupStrategy;
+    
+    /** The strategy used to merge UserInfo claims with id_token claims.*/
+    @Nonnull private BinaryOperator<ClaimsSet> claimMergingStrategy;
+    
+    /** 
+     * The strategy used to sanitize claims in an input claimset. By default, produces a set of
+     * claims without the validation claims (e.g. nonce, exp), but leaving the identity, authorization and 
+     * misc claims.
+     */
+    @Nonnull private UnaryOperator<ClaimsSet> claimSanitizationStrategy;
+    
+    /** The stashed UserInfo claims. This is allowably {@literal empty} if the UserInfo endpoint was not used.*/
+    @NonnullBeforeExec private ClaimsSet userInfoClaims; 
+    
+    /** 
+     * The stashed id_token claims. This should never be {@literal null} or {@literal empty} once 
+     * {@link #doPreExecute(ProfileRequestContext)} has run.
+     */
+    @NonnullBeforeExec private JWTClaimsSet idTokenClaims; 
+    
+    
+    /** Constructor.*/
+    public ProcessEndUserClaims() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+        
+        accessTokenResponseContextLookupStrategy =
+                new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+        
+        // Will create context.
+        endUserClaimsContextLookupStrategy = 
+                new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        
+        claimMergingStrategy = new DefaultClaimMergingStrategy();
+        claimSanitizationStrategy = new DefaultClaimSanitizationStrategy();
+    }
+   
+    
+    /**
+     * Set the strategy used to merge UserInfo claims with id_token claims.
+     * 
+     * @param strategy the strategy to use.
+     */
+    public void setClaimMergingStrategy(@Nonnull final BinaryOperator<ClaimsSet> strategy) {
+    	checkSetterPreconditions();
+        
+        claimMergingStrategy =  Constraint.isNotNull(strategy,
+                "ClaimMergingStrategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to sanitize claims from both the id_token and UserInfo
+     * claims set to produce a clean claims set.
+     * 
+     * @param strategy the strategy to use.
+     */
+    public void setClaimSanitizationStrategy(@Nonnull final UnaryOperator<ClaimsSet> strategy) {
+    	checkSetterPreconditions();
+        
+        claimSanitizationStrategy = Constraint.isNotNull(strategy,
+                "ClaimSanatizationStrategy cannot be null");
+    }
+    
+    /**
+     * Set whether to enable claim sanitization. If true, whatever claimSanatizationStrategy
+     * is set is used. If false, a no-op strategy is created which just returns a new claims set
+     * based on the same claims that exist in the input claims set. By default, claims sanitization
+     * uses the {@link DefaultClaimSanitizationStrategy}.
+     * 
+     * @param enable enable or disable claims sanitization
+     */
+    public void setEnableClaimSanitizationStrategy(final boolean enable) {
+        if (!enable) {
+            claimSanitizationStrategy = c -> {
+                final ClaimsSet claims = new ClaimsSet();
+                claims.putAll(c);
+                return claims;
+            };
+        }
+    }
+    
+    /**
+     * Set the strategy used to lookup a {@link EndUserClaimsContext}.
+     * 
+     * @param strategy the strategy
+     */
+    public void setEndUserClaimsContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
+    	checkSetterPreconditions();
+        
+        endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+                "EndUserClaimsContextLookupStrategy cannot be null");
+    }
+
+    
+    /**
+     * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAccessTokenResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+    	checkSetterPreconditions();
+        
+        accessTokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "TokenResponseContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to look up a {@link UserInfoResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUserInfoResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+    	checkSetterPreconditions();
+        
+        userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "UserInfoResponseContext lookup strategy cannot be null");
+    }
+  
+ // Checkstyle: CyclomaticComplexity OFF
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final UserInfoResponseContext userInfoCtx = 
+                userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+        final UserInfoSuccessResponse userInfo = userInfoCtx != null ? userInfoCtx.getUserInfo() : null;
+        
+        if (userInfoCtx == null || userInfo == null) {
+            log.trace("{} No UserInfo response context returned by lookup strategy, creating empty "
+                    + "UserInfo claims", getLogPrefix());
+            userInfoClaims = new ClaimsSet();
+        } else if (userInfo.getEntityContentType() == ContentType.APPLICATION_JSON) {
+            userInfoClaims = userInfo.getUserInfo();
+        } else if (userInfo.getEntityContentType() == ContentType.APPLICATION_JWT) {            
+            try {
+                final ClaimsSet claims = new ClaimsSet();
+                claims.putAll(userInfo.getUserInfoJWT().getJWTClaimsSet().getClaims());
+                userInfoClaims = claims;
+            } catch (final ParseException e) {
+                log.warn("Unable to extract UserInfo claims from JWT claimsset", e);
+                return false;
+            }            
+        } else {
+            log.warn("Unable to extract UserInfo claims, unknown entity content type");
+            return false;
+        }
+        
+        final AccessTokenResponseContext tokenResponseCtx = 
+                accessTokenResponseContextLookupStrategy.apply(profileRequestContext);
+        final OIDCTokenResponse tokenResponse = tokenResponseCtx != null ? tokenResponseCtx.getTokenResponse() : null;
+        
+        if (tokenResponseCtx == null || tokenResponse == null) {
+            log.debug("{} No AccessTokenResponseContext or Access Token returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        if (tokenResponse.getOIDCTokens() == null || 
+                tokenResponse.getOIDCTokens().getIDToken() == null) {
+            log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        try {
+            idTokenClaims = tokenResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
+            if (idTokenClaims == null) {
+                log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+                        + "possibly still encrypted", 
+                        getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+                return false;
+            }
+        } catch (final ParseException e) {
+            log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }    
+  
+        return true;
+    }
+    
+ // Checkstyle: CyclomaticComplexity ON
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) { 
+        
+          if (log.isTraceEnabled()) {
+              log.trace("{} Processing {} claims", getLogPrefix(),
+                      userInfoClaims.toJSONObject().size() > 0 ? "UserInfo and ID Token" : "ID Token");
+          }
+          
+          final ClaimsSet idToken = new ClaimsSet();
+          idToken.putAll(idTokenClaims.toJSONObject());
+          
+          final ClaimsSet sanitizedUserInfoClaims = claimSanitizationStrategy.apply(userInfoClaims);
+          final ClaimsSet sanitizedIdTokenClaims = claimSanitizationStrategy.apply(idToken);
+          
+          final ClaimsSet mergedClaims = claimMergingStrategy.apply(sanitizedUserInfoClaims, sanitizedIdTokenClaims);
+          
+          // Add to end user claims context both the merged claims, and the parsed id_token claims.
+          // The id_token claims are stashed here to avoid re-parsing downstream.
+          final var idTokenClaimsCopy = idTokenClaims;
+          assert idTokenClaimsCopy != null;
+          endUserClaimsContextLookupStrategy.apply(profileRequestContext)
+                  .setEndUserClaims(mergedClaims != null ? mergedClaims : new ClaimsSet())
+                  .setUnprocessedIdTokenClaims(idTokenClaimsCopy);
+
+          if (log.isTraceEnabled() && mergedClaims != null) {
+              log.trace("{} Merged and sanitized claims to produce the claims set '{}'", 
+                  getLogPrefix(), mergedClaims.toJSONString());
+          }
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java
new file mode 100644
index 0000000..f972669
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Request information from the UserInfo OAuth2.0 endpoint using the access_token already present
+ * in the context. Return consented claims about the subject. 
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @post Add a claims returned from the UserInfo endpoint to the {@link UserInfoResponseContext}.
+ */
+public class UserInfoEndpointLookup extends AbstractHttpOAuthAction<UserInfoResponse> {
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(UserInfoEndpointLookup.class);
+    
+    /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+    @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext> 
+            userInfoResponseContextLookupStrategy;
+    
+    
+    /** Constructor.*/
+    public UserInfoEndpointLookup() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the strategy used to look up a {@link UserInfoResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUserInfoResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+    	checkSetterPreconditions();
+        
+        userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "UserInfoResponseContext lookup strategy cannot be null");
+    }
+
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) { 
+        
+          final MessageContext inboundMessageContext = profileRequestContext.getInboundMessageContext();
+          final String provider = inboundMessageContext != null ?  inboundMessageContext
+                        .ensureSubcontext(OIDCPeerEntityContext.class).getIdentifier() : "na";
+        
+          log.debug("{} Requesting claims from UserInfo endpoint from provider '{}'", getLogPrefix(), 
+                  provider);
+          
+          final UserInfoResponseContext userInfoCtx = 
+                  userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+          if (userInfoCtx == null) {
+              log.debug("{} No UserInfo response context returned by lookup strategy", getLogPrefix());
+              ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+              return;
+          }
+          
+          try {          
+              final UserInfoResponse response = handleRequest(profileRequestContext, userInfoCtx);
+              if (response.indicatesSuccess()) {
+                  userInfoCtx.setUserInfo(response.toSuccessResponse());
+              }
+          } catch (final MessageHandlerException e) {
+              log.error("{} Unable to return claims from UserInfo endpoint",getLogPrefix(),e);
+              ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+          }   
+    }
+    
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
index 0583054..24486f2 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
@@ -39,12 +39,10 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
 
 /**
  * Action that validates the claims of a JWT using the supplied 
- * {@link ClaimsValidator claims validator}. The verifier <b>must</b> be thread-safe and validate, at
- * minimum, the claims set against the OpenID Connect core 1.0 section 3.1.3.7 specification. 
+ * {@link ClaimsValidator claims validator}. The verifier <b>must</b> be thread-safe and validate.
  * 
  * 
  * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre> 
@@ -135,9 +133,9 @@ public class ValidateTokenClaims extends AbstractProfileAction {
             //parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
             claimsSet = token.getJWTClaimsSet();
             if (claimsSet == null) {
-                throw new OIDCRPException("JWT ClaimsSet is null");
+                throw new JWTValidationException("JWT ClaimsSet is null");
             }
-        } catch (final ParseException | OIDCRPException e) {
+        } catch (final ParseException | JWTValidationException e) {
             log.error("{} JWT Claimset is not available", getLogPrefix(),e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
             return false;
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.java
new file mode 100644
index 0000000..627b95c
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.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 java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+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.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validate a successful UserInfo JSON Object Response according to section 5.3.2 of OpenID Connect Core 1.0. 
+ * 
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link OidcEventIds#INVALID_USERINFO_CLAIMS}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ */
+public class ValidateUserInfoJSONObjectClaims extends AbstractProfileAction {
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateUserInfoJSONObjectClaims.class);
+    
+    /** Strategy used to look up the {@link UserInfoResponseContext}. */
+    @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext> userInfoResponseContextLookupStrategy;
+    
+    /** Strategy used to look up the {@link AccessTokenResponseContext}. */
+    @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext> tokenResponseContextLookupStrategy;
+    
+    /** The stashed user info response context.*/
+    @NonnullBeforeExec private UserInfoResponseContext userInfoCtx; 
+    
+    /** The stashed user info response.*/
+    @NonnullBeforeExec private UserInfoSuccessResponse userInfo;
+    
+    /** The stashed id_token claims.*/
+    @NonnullBeforeExec private JWTClaimsSet idTokenClaims; 
+
+    /** Constructor.*/
+    public ValidateUserInfoJSONObjectClaims() {
+        userInfoResponseContextLookupStrategy =
+                new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+        tokenResponseContextLookupStrategy =
+                new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTokenResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+    	checkSetterPreconditions();
+        
+        tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "TokenResponseContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to look up a {@link UserInfoResponseContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUserInfoResponseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+    	checkSetterPreconditions();
+        
+        userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "UserInfoResponseContext lookup strategy cannot be null");
+    }
+    
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        userInfoCtx = 
+                userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+        userInfo = userInfoCtx != null ? userInfoCtx.getUserInfo() : null;
+        if (userInfo == null) {
+            log.debug("{} No UserInfo response returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        final AccessTokenResponseContext tokenResponseCtx = 
+                tokenResponseContextLookupStrategy.apply(profileRequestContext);
+        if (tokenResponseCtx == null) {
+            log.debug("{} No AccessTokenResponseContext returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        final OIDCTokenResponse tokenResponse = tokenResponseCtx.getTokenResponse();
+        if (tokenResponse == null || tokenResponse.getOIDCTokens().getIDToken() == null) {
+            log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        try {
+            idTokenClaims = tokenResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
+            if (idTokenClaims == null) {
+                log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+                        + "possibly still encrypted", 
+                        getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ID_TOKEN);
+                return false;
+            }
+        } catch (final ParseException e) {
+            log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) { 
+        
+          log.trace("{} Validating UserInfo JSON Object claims", getLogPrefix());          
+
+          assert userInfo != null;
+          final ClaimsSet claims = resolveClaimsSet(userInfo);
+          if (claims == null) {
+              log.debug("{} UserInfo claims can not be resolved", getLogPrefix());
+              ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+              return;
+          }
+          final String subFromUserInfo = claims.getStringClaim("sub");
+          if (subFromUserInfo == null) {
+              log.debug("{} UserInfo claims does not contain the 'sub' claim, it must", getLogPrefix());
+              ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+              return;
+          }
+          // sub must match to id_token sub
+          if (!idTokenClaims.getSubject().equals(subFromUserInfo)){
+              log.error("{} UserInfo claims about subject '{}' but id_token about subject '{}', mismatch", 
+                      getLogPrefix(), subFromUserInfo, idTokenClaims.getSubject());
+              ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+              return;
+          }          
+          log.debug("{} UserInfo claims are valid for '{}'", getLogPrefix(), claims.getStringClaim("sub"));
+         
+    }
+    
+    /**
+     * Resolve the UserInfo claims from the user info response. If JWT type, extract from the JWT claims. If plain JSON
+     * object type, resolve directly from the UserInfo claims set. 
+     * 
+     * @param response the UserInfo response
+     * 
+     * @return the UserInfo claims. Return {@code null} if not found, wrong response type, or there was a parsing 
+     *              exception
+     */
+    @Nullable private ClaimsSet resolveClaimsSet(@Nonnull final UserInfoResponse response) {
+        if (!response.indicatesSuccess()) {
+            return null;
+        }
+        final UserInfoSuccessResponse successResponse = response.toSuccessResponse();
+        if (successResponse.getEntityContentType() == ContentType.APPLICATION_JWT) {
+            try {
+                final JWTClaimsSet claims = successResponse.getUserInfoJWT().getJWTClaimsSet();
+                if (claims == null) {
+                    log.debug("{} UserInfo claims are not available, check response is not still encrypted", 
+                            getLogPrefix());
+                    return null;
+                }
+                final ClaimsSet claimsConverted = new ClaimsSet();
+                claimsConverted.putAll(claims.toJSONObject());
+                return claimsConverted;
+            } catch (final ParseException e) {
+                log.warn("{} UserInfo claims could not be extracted from the JWT claims set", getLogPrefix(), e);
+                return null;
+            }
+            
+        } else if (successResponse.getEntityContentType() == ContentType.APPLICATION_JSON) {
+            return successResponse.getUserInfo();
+        } else {
+            log.debug("{} UserInfo claims are not available, unknown entity content type", getLogPrefix());
+            return null;
+        }
+    }
+
+}

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


More information about the commits mailing list