[java-idp-plugin-oidc-rp] branch main updated: Add UserInfo response validation and id_token merge

Phil Smart philip.smart at jisc.ac.uk
Tue Feb 15 09:56:42 UTC 2022


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-idp-plugin-oidc-rp.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=faad89ffc5af13cbed592210325e4fa8dd8595f8

The following commit(s) were added to refs/heads/main by this push:
     new faad89f  Add UserInfo response validation and id_token merge
faad89f is described below

commit faad89ffc5af13cbed592210325e4fa8dd8595f8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Feb 15 09:56:36 2022 +0000

    Add UserInfo response validation and id_token merge
---
 .../impl/DefaultUserInfoResponseDecoder.java       |  27 +++--
 .../oidc/rp/impl/DefaultClaimMergingStrategy.java  |  13 +-
 .../rp/impl/DefaultClaimSanitizationStrategy.java  |  65 ++++++++++
 .../rp/impl/MergeUserInfoAndIDTokenClaims.java     |  75 +++++++++---
 .../oidc/rp/impl/ValidateOIDCAuthentication.java   |  26 ++--
 .../authn/oidc/rp/impl/ValidateUserInfoClaims.java |   2 +-
 .../oidc-relying-party-authn-beans.xml             |   2 +-
 .../oidc-relying-party-authn-flow.xml              |   4 +-
 .../rp/impl/DefaultClaimMergingStrategyTest.java   |  28 ++++-
 .../impl/DefaultClaimSanatizationStrategyTest.java |  67 ++++++++++
 .../rp/impl/ExtractIDTokenFromResponseTest.java    |  25 ++--
 .../rp/impl/MergeUserInfoAndIDTokenClaimsTest.java | 126 +++++++++++++++++++
 .../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java  | 135 ++++++++++++++++++++-
 13 files changed, 533 insertions(+), 62 deletions(-)

diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
index 260a1c8..3b2cd1f 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/decoding/impl/DefaultUserInfoResponseDecoder.java
@@ -31,15 +31,17 @@ import org.springframework.util.MimeType;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.nimbusds.jose.util.IOUtils;
+import com.nimbusds.jwt.EncryptedJWT;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
 import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
 import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
 import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
 
-/** Response decoder for UserInfo responses.*/
+/** Response decoder for UserInfo responses. Supports both plain JSON Object and JWT responses.*/
 public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderFunction<UserInfoResponse> {
     
     /** The application/jwt media type.*/
@@ -48,6 +50,7 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUserInfoResponseDecoder.class);    
     
+ // CheckStyle: CyclomaticComplexity OFF
     @Override
     public UserInfoResponse apply(@Nonnull final HttpResponse httpResponse) {
         
@@ -78,19 +81,27 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
             
             // Is a JWT type or plain JSON object
             if (APPLICATION_JWT.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0) {
+                
                 final JWT parsedJwt = JWTParser.parse(content);
-                log.trace("UserInfo response parsed a JWT type");
+                
+                if (log.isTraceEnabled()) {
+                    log.trace("UserInfo response decoder parsed a {} JWT type", 
+                            parsedJwt instanceof SignedJWT ? "Signed" : 
+                                (parsedJwt instanceof EncryptedJWT ? "Encrypted" : "plain"));
+                }
+                
                 return new JWTUserInfoResponse(parsedJwt);
                 
             } else if (MediaType.APPLICATION_JSON.compareTo(MimeType.valueOf(contentType.getMimeType())) == 0){
-                final Map<String, Object> claims = getObjectMapper().readValue(content, 
-                        new TypeReference<Map<String, Object>>() {});
+                
+                final Map<String, Object> claims = getObjectMapper().readValue(
+                        content, new TypeReference<Map<String, Object>>() {});
                 final ClaimsSet claimsSet = new ClaimsSet();
                 claimsSet.putAll(claims);
-                log.debug("UserInfo endpoint returned claims for '{}'", claimsSet.getStringClaim("sub"));
+    
                 if (log.isTraceEnabled()) {
-                    log.trace("UserInfo endpoint claims for '{}' are '{}'", claimsSet.getStringClaim("sub"), 
-                            claimsSet.toJSONString());
+                    log.trace("UserInfo response decoder parsed a plain JSON Object for subject '{}'", 
+                            claimsSet.getStringClaim("sub"));
                 }
                 return new PlainUserInfoResponse(claimsSet);
             }            
@@ -102,5 +113,5 @@ public class DefaultUserInfoResponseDecoder extends AbstractJSONResponseDecoderF
         return null;
         
     }
-
+ // CheckStyle: CyclomaticComplexity ON
 }
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java
index a72cc7b..70f5797 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategy.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -40,17 +41,17 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
  * </ol>
  */
 //TODO maybe this should filter claims as well e.g. exp etc. Security check this logic.
-public class DefaultClaimMergingStrategy implements BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> {
+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 JWTClaimsSet idToken) {
+    @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.getClaims());
+            singleSet.putAll(idToken.toJSONObject());
             return singleSet;
         }
         if (userInfo != null && idToken == null) {
@@ -61,11 +62,9 @@ public class DefaultClaimMergingStrategy implements BiFunction<ClaimsSet, JWTCla
         if (userInfo == null && idToken == null) {
             // return empty claimsset
             return new ClaimsSet();
-        }
-        
-        
+        }    
         
-        final Map<String, Object> idTokenAsMap = idToken.getClaims();
+        final Map<String, Object> idTokenAsMap = idToken.toJSONObject();
         // Treat JSONObject as the base map representation.
         final Map<String, Object> userInfoClaimsAsMap = userInfo.toJSONObject();
         
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanitizationStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanitizationStrategy.java
new file mode 100644
index 0000000..d2f7f79
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanitizationStrategy.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.authn.oidc.rp.impl;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+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 the validation claims, 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() {
+        validationClaims = 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());
+    }
+
+    @Override
+    public ClaimsSet apply(@Nonnull final ClaimsSet jwtClaims) {
+        final ClaimsSet sanitizedClaims = new ClaimsSet();
+        final Map<String, Object> filteredMap = jwtClaims.toJSONObject().entrySet()
+            .stream()
+            .filter(c -> !validationClaims.contains(c.getKey()))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        sanitizedClaims.putAll(filteredMap);
+        return sanitizedClaims;
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java
index cd8c549..222f86e 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaims.java
@@ -18,8 +18,9 @@
 package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
 
 import java.text.ParseException;
-import java.util.function.BiFunction;
+import java.util.function.BinaryOperator;
 import java.util.function.Function;
+import java.util.function.UnaryOperator;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -39,13 +40,14 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Merge the claims in the id_token with the claims from the UserInfo response. 
+ * Merge the claims in the id_token with the claims from the UserInfo response. Before merge,
+ * both id_token and UserInfo claim sets are sanitized by a replaceable strategy. For example, 
+ * by default to remove 'validation claims' that should not be exposed further by the system. 
+ * This can be turned off by setting a no-op sanitizer, or setting 
  */
 //TODO similar too ValidateUserInfoClaims, do we need to extend OIDC action
 public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAction {
@@ -66,7 +68,14 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
             endUserClaimsContextLookupStrategy;
     
     /** The strategy used to merge UserInfo claims with id_token claims.*/
-    @NonnullAfterInit private BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> claimMergingStrategy;
+    @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 user info claims.*/
     @Nullable private ClaimsSet userInfoClaims; 
@@ -89,16 +98,9 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
                         new InboundMessageContextLookup());
         
         claimMergingStrategy = new DefaultClaimMergingStrategy();
+        claimSanitizationStrategy = new DefaultClaimSanitizationStrategy();
     }
-    
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (claimMergingStrategy == null) {
-            throw new ComponentInitializationException("ClaimMergingStrategy cannot be null");
-        }
-    }
+   
     
     /**
      * Set the strategy used to merge UserInfo claims with id_token claims.
@@ -106,13 +108,43 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
      * @param strategy the strategy to use.
      */
     public void setClaimMergingStrategy(
-            @Nonnull final BiFunction<ClaimsSet, JWTClaimsSet, ClaimsSet> strategy) {
+            @Nonnull final BinaryOperator<ClaimsSet> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         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) {
+        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}.
      * 
@@ -121,6 +153,7 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
     public void setEndUserClaimsContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
         
         endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
                 "EndUserClaimsContextLookupStrategy cannot be null");
@@ -135,6 +168,7 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
     public void setTokenResponseContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
         
         tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
                 "TokenResponseContext lookup strategy cannot be null");
@@ -148,6 +182,7 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
     public void setUserInfoResponseContextLookupStrategy(
             @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
         
         userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
                 "UserInfoResponseContext lookup strategy cannot be null");
@@ -204,13 +239,19 @@ public class MergeUserInfoAndIDTokenClaims extends AbstractOIDCAuthenticationAct
         
           log.trace("{} Merging UserInfo and id_token claims", getLogPrefix());    
           
-          final ClaimsSet mergedClaims = claimMergingStrategy.apply(userInfoClaims, idTokenClaims);
+          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
           endUserClaimsContextLookupStrategy.apply(profileRequestContext).setEndUserClaims(mergedClaims);
           
           if (log.isTraceEnabled()) {
-              log.trace("{} Merged UserInfo and id_token claims to produce '{}'", 
+              log.trace("{} Merged UserInfo and id_token claims to produce a claims set containing '{}'", 
                   getLogPrefix(), mergedClaims.toJSONString());
           }
     }
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
index 67d8fb0..aad5c6e 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateOIDCAuthentication.java
@@ -17,7 +17,6 @@
 
 package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
 
-import java.text.ParseException;
 import java.util.Collection;
 import java.util.Map;
 import java.util.function.Function;
@@ -36,13 +35,13 @@ import org.slf4j.LoggerFactory;
 
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Multimap;
-import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
 import net.minidev.json.JSONObject;
 import net.shibboleth.idp.attribute.AttributeDecodingException;
 import net.shibboleth.idp.attribute.AttributeEncodingException;
 import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.filter.AttributeFilter;
 import net.shibboleth.idp.attribute.transcoding.AttributeTranscoder;
 import net.shibboleth.idp.attribute.transcoding.AttributeTranscoderRegistry;
 import net.shibboleth.idp.attribute.transcoding.TranscoderSupport;
@@ -53,7 +52,6 @@ import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.principal.IdPAttributePrincipal;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
-import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
 import net.shibboleth.idp.profile.IdPEventIds;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
@@ -91,23 +89,20 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateOIDCAuthentication.class);
-
-    /** Avoid creating multiple principals. */
-    private boolean avoidMultiplePrincipal;
-
-    /** the subject received from id token. */
-    @Nullable private String oidcSubject;
     
     /** Transcoder registry service object. */
     @NonnullAfterInit private ReloadableService<AttributeTranscoderRegistry> transcoderRegistry;
     
+    /** Service used to get the engine used to filter attributes. */
+    @Nullable private ReloadableService<AttributeFilter> attributeFilterService;
+    
     /** Strategy used to look up a {@link RelyingPartyContext} for configuration options. */
     @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
     
     /** Store off profile config. */
     @Nullable private OIDCAuthorizationConfiguration profileConfiguration;
        
-    /** Claims Set to extract and decode claims from. */
+    /** End-user claims Set to extract and transcode claims from. */
     @Nullable private ClaimsSet claimsSet;
     
     /** Strategy used to look up the {@link EndUserClaimsContext} to set the parameters for. */
@@ -124,6 +119,17 @@ public class ValidateOIDCAuthentication extends AbstractValidationAction {
                         new InboundMessageContextLookup());
     }
     
+    /**
+     * Sets the filter service to use for inbound attributes.
+     *
+     * @param filterService optional filter service for inbound attributes
+     */
+    public void setAttributeFilter(@Nullable final ReloadableService<AttributeFilter> filterService) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        attributeFilterService = filterService;
+    }
+    
     
     /**
      * Sets the registry of transcoding rules to apply to encode attributes.
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
index 5254b4d..af84011 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateUserInfoClaims.java
@@ -146,7 +146,7 @@ public class ValidateUserInfoClaims extends AbstractOIDCAuthenticationAction {
          
           final UserInfoResponse response = userInfoCtx.getUserInfo();
           if (!response.isClaimsSetAvailable()) {
-              log.debug("{} UserInfo claims are not available, check response not still encrypted", getLogPrefix());
+              log.debug("{} UserInfo claims are not available, check response is not still encrypted", getLogPrefix());
               ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
               return;
           }
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
index 95e9857..ff70537 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
@@ -438,7 +438,7 @@
     
     <!-- UserInfo response JWT validation -->
     
-    <bean id="ValidateUserInfoTokenClaims" scope="prototype"
+    <bean id="ValidateUserInfoToken" scope="prototype"
        class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateTokenClaims"
        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
index f200c46..90af8f1 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
@@ -154,14 +154,14 @@
      
      <action-state id="ValidateSignedUserInfoJWT">
         <!-- <evaluate expression="PopulateTokenSignatureSigningParameters" /> -->
-        <evaluate expression="ValidateUserInfoTokenClaims" />
+        <evaluate expression="ValidateUserInfoToken" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ValidateUserInfoClaimsSet" />
     </action-state>
     
     <action-state id="DecryptUserInfoJWT">
         <!--  <evaluate expression="PopulateTokenEncryptionParameters" /> -->
-        <evaluate expression="ValidateUserInfoTokenClaims" />
+        <evaluate expression="ValidateUserInfoToken" /> <!--  Will die if not decrypted properly first -->
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ValidateUserInfoClaimsSet" />
     </action-state>
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java
index 1ade5f1..1b80a6a 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimMergingStrategyTest.java
@@ -1,3 +1,20 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.authn.oidc.rp.impl;
 
 import static org.junit.Assert.assertEquals;
@@ -20,7 +37,7 @@ public class DefaultClaimMergingStrategyTest {
     
     /** Mock response from the UserInfo endpoint.*/
     @Nonnull @NotEmpty
-    protected final String USERINFO_RESPONSE ="{\n"
+    private final String USERINFO_RESPONSE ="{\n"
             + "  \"sub\": \"jdoe\",\n"
             + "  \"website\": \"https://openid.net/\",\n"
             + "  \"zoneinfo\": \"America/Los_Angeles\",\n"
@@ -41,10 +58,12 @@ public class DefaultClaimMergingStrategyTest {
         final JSONParser parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
         final ClaimsSet userInfo = new ClaimsSet((JSONObject)parser.parse(USERINFO_RESPONSE));
         
-        final JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder().subject("jdoe")
+        final JWTClaimsSet idToken = new JWTClaimsSet.Builder().subject("jdoe")
                 .claim("given_name", "FromIdToken")
                 .claim("nickname", "FromIdToken")
                 .build();
+        final ClaimsSet idTokenClaims = new ClaimsSet();
+        idTokenClaims.putAll(idToken.getClaims());
         
         final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
         final ClaimsSet merged = strategy.apply(userInfo, idTokenClaims);
@@ -62,11 +81,12 @@ public class DefaultClaimMergingStrategyTest {
     @Test
     public void testMergeNullUserInfo() throws ParseException {
         
-        final JWTClaimsSet idTokenClaims = new JWTClaimsSet.Builder().subject("jdoe")
+        final JWTClaimsSet idToken = new JWTClaimsSet.Builder().subject("jdoe")
                 .claim("given_name", "FromIdToken")
                 .claim("nickname", "FromIdToken")
                 .build();
-        
+        final ClaimsSet idTokenClaims = new ClaimsSet();
+        idTokenClaims.putAll(idToken.getClaims());
         final DefaultClaimMergingStrategy strategy = new DefaultClaimMergingStrategy();
         final ClaimsSet merged = strategy.apply(null, idTokenClaims);
         
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanatizationStrategyTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanatizationStrategyTest.java
new file mode 100644
index 0000000..40b00cc
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultClaimSanatizationStrategyTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.authn.oidc.rp.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+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;
+
+
+/** Tests for DefaultClaimSanatizationStrategy.*/
+public class DefaultClaimSanatizationStrategyTest {
+    
+    @Test
+    public void testRemoveValidationClaims() {
+        final DefaultClaimSanitizationStrategy strategy = new DefaultClaimSanitizationStrategy();
+        
+        final JWTClaimsSet idToken = new JWTClaimsSet.Builder()
+                     .subject("joe")
+                     .issuer("https://op.example.com")
+                     .audience(List.of("https://rp.example.com"))
+                     .issueTime(Date.from(Instant.now()))
+                     .expirationTime(Date.from(Instant.now().plus(Duration.ofSeconds(10))))
+                     .claim(IDTokenClaims.NONCE.getClaimName(), "noncevalue")
+                     .claim("given_name", "Joe")
+                     .build();
+        
+        final ClaimsSet idTokenClaims = new ClaimsSet();
+        idTokenClaims.putAll(idToken.getClaims());
+        final ClaimsSet sanClaims = strategy.apply(idTokenClaims);
+        assertNull(sanClaims.getAudience());
+        assertNull(sanClaims.getIssuer());
+        assertNull(sanClaims.getClaim(IDTokenClaims.NONCE.getClaimName()));
+        assertNull(sanClaims.getClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+        assertNull(sanClaims.getClaim(JWTClaims.ISSUED_AT_CLAIM.getClaimName()));
+        
+        assertEquals(sanClaims.getClaim(JWTClaims.SUBJECT_CLAIM.getClaimName()),"joe");
+        assertEquals(sanClaims.getClaim("given_name"),"Joe");
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
index eb12fe0..5fad162 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ExtractIDTokenFromResponseTest.java
@@ -1,22 +1,31 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.authn.oidc.rp.impl;
 
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
 
-import java.io.ByteArrayInputStream;
-import java.util.Map;
-
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.messaging.context.navigate.ParentContextLookup;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.springframework.webflow.execution.Event;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.nimbusds.jose.shaded.json.parser.JSONParser;
-
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java
new file mode 100644
index 0000000..f4ea4db
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/MergeUserInfoAndIDTokenClaimsTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.idp.plugin.authn.oidc.rp.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.ParentContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.EndUserClaimsContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.PlainUserInfoResponse;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/** Tests for {@link MergeUserInfoAndIDTokenClaims}.*/
+public class MergeUserInfoAndIDTokenClaimsTest extends AbstractOIDCTest {
+    
+    /** Action to test.*/
+    private MergeUserInfoAndIDTokenClaims action;
+    
+    
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        action = new MergeUserInfoAndIDTokenClaims();   
+        
+        final AccessTokenResponseContext trc = new AccessTokenResponseContext();
+        final PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder()
+                .issuer("https://op.example.com")
+                .audience(List.of("https://rp.example.com"))
+                .subject("jdoe")
+                .claim("nonce", "abadnonce")
+                .claim("azp", "https://rp.example.com")
+                .claim("name","jdoe")
+                .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+                .build());
+        trc.setIdToken(jwt);
+        prc.getInboundMessageContext().addSubcontext(trc);   
+
+        final ClaimsSet claims = new ClaimsSet();
+        claims.putAll(Map.of("sub","jdoe","given_name","John","email","jdoe at example.com"));
+        final UserInfoResponse userInfoResponse = new PlainUserInfoResponse(claims);
+        final UserInfoResponseContext urc = new UserInfoResponseContext();
+        urc.setUserInfo(userInfoResponse);
+        prc.getInboundMessageContext().addSubcontext(urc);        
+
+        action.setAuthenticationContextLookupStrategy(new ParentContextLookup<>(AuthenticationContext.class));
+        
+        action.setProfileContextLookupStrategy(new ChildContextLookup<>(ProfileRequestContext.class).compose(
+                new ChildContextLookup<>(AuthenticationContext.class)
+                .compose(new WebflowRequestContextProfileRequestContextLookup())));      
+    }
+    
+    @Test
+    public void testSuccesfulMerge() throws ComponentInitializationException {
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
+        final ClaimsSet claims =
+                prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
+        assertEquals(claims.getClaim("name"),"jdoe");
+        assertEquals(claims.getClaim("sub"),"jdoe");
+        assertEquals(claims.getClaim("given_name"),"John");
+        assertEquals(claims.getClaim("email"),"jdoe at example.com");
+        assertNull(claims.getIssuer());
+        assertNull(claims.getAudience());
+        assertNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+    }
+    
+    @Test
+    public void testSuccesfulMergeNoSanitization() throws ComponentInitializationException {
+        
+        action.setEnableClaimSanitizationStrategy(false);
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNotNull(prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims());
+        final ClaimsSet claims =
+                prc.getInboundMessageContext().getSubcontext(EndUserClaimsContext.class).getEndUserClaims();
+        assertEquals(claims.getClaim("name"),"jdoe");
+        assertEquals(claims.getClaim("sub"),"jdoe");
+        assertEquals(claims.getClaim("given_name"),"John");
+        assertEquals(claims.getClaim("email"),"jdoe at example.com");
+        assertNotNull(claims.getIssuer());
+        assertNotNull(claims.getAudience());
+        assertNotNull(claims.getDateClaim(JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName()));
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
index a34bad7..3137d71 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
@@ -45,14 +45,25 @@ import org.springframework.webflow.engine.impl.FlowExecutionImpl;
 import org.springframework.webflow.execution.FlowExecution;
 import org.springframework.webflow.test.MockFlowBuilderContext;
 
+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.ECDSASigner;
 import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
 import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.ParseException;
@@ -309,7 +320,15 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         return accessTokenSerialized;
     }
     
-    private String createSignedUserInfoJWTResponseJSON(final String issuer, final String audience) 
+    /**
+     * Create a signed UserInfo response JWT.
+     * 
+     * @param issuer the issuer
+     * @param audience the audience
+     * @return the signed JWT
+     * @throws JOSEException on error
+     */
+    private SignedJWT createSignedUserInfoJWTResponseJSON(final String issuer, final String audience) 
             throws JOSEException {
         
         final var key = new ECKeyGenerator(Curve.P_256).keyID("123").generate();
@@ -328,7 +347,32 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         
         final var signedJWT = new SignedJWT(header, payload);
         signedJWT.sign(new ECDSASigner(key.toECPrivateKey()));
-        return signedJWT.serialize();
+        return signedJWT;
+    }
+    
+    /**
+     * Create a signed and encrypted UserInfo response JWT.
+     * 
+     * @param issuer the issuer
+     * @param audience the audience
+     * @return the signed JWT
+     * @throws JOSEException on error
+     */
+    private EncryptedJWT createSignedAndEncryptedUserInfoJWTResponseJSON(final String issuer, final String audience) 
+            throws Exception {
+        
+        final RSAKey keyRecipient = new RSAKeyGenerator(2048)
+                .keyID("2")
+                .keyUse(KeyUse.ENCRYPTION)
+                .generate();
+        
+        final JWEObject jweObject = 
+                new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+                .contentType("JWT")
+                .build(),
+                new Payload(createSignedUserInfoJWTResponseJSON(issuer, audience)));
+        jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
+        return EncryptedJWT.parse(jweObject.serialize());
     }
     
     /**
@@ -550,7 +594,89 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     }
     
     @Test 
-    public void testAuthnFlowFromAuthorizationCallback_UsingJWTUserInfoResponse() throws Exception {
+    public void testAuthnFlowFromAuthorizationCallback_UsingSignedJWTUserInfoResponse() throws Exception {
+        
+        setFlowPath(FLOW);
+        setFlowModelResources(flowResources);
+        setSubflows(subflows);        
+        
+        final Map<String,String> mockProperties = Map.of(
+                "idp.service.clientinfo.failFast","false",
+                "idp.oidc.rp.clientID","clientId",
+                "idp.oidc.rp.clientSecret","secret",
+                "idp.oidc.rp.providerConfigurationDocument","provider_location",
+                "idp.oidc.rp.redirectURI","https://localhost:8443/idp/profile/Authn/OIDC/RP/callback",
+                "idp.oidc.rp.scope","email",
+                "idp.entityID", "http://idp.example.com/",
+                "idp.authn.oidc.rp.proxyIssuer","https://op.example.com");
+        
+        setMockProperties(mockProperties);
+        
+        final MockWebServer mockOPServer = createSimpleServer();
+        // First is token exchange
+        mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+                .setHeader("content-type", "application/json")
+                .setBody(createAccessTokenResponseJSON()));
+        // Second is userInfo
+        mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+                .setHeader("content-type", "application/jwt")
+                .setBody(createSignedUserInfoJWTResponseJSON("https://op.example.com","demo_rp").serialize()));
+        mockOPServer.start(9918);
+        
+
+        final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+                .createFlowExecution(getFlowDefinition());
+        final ProfileRequestContext prc =  buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
+        prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority("http://op.example.com");
+        
+        // create a nested PRC under the authentication context
+        final ProfileRequestContext nestPrc = (ProfileRequestContext) 
+                prc.getSubcontext(AuthenticationContext.class).addSubcontext(new ProfileRequestContext(), true);  
+        
+        // Add under nest PRC
+        final RelyingPartyContext partyContext = new RelyingPartyContext();
+        final OIDCAuthorizationConfiguration partyConfig = new OIDCAuthorizationConfiguration();        
+        partyContext.setProfileConfig(partyConfig);
+        nestPrc.addSubcontext(partyContext);
+       
+        // Setup outbound context
+        final MessageContext outMsgCtx = new MessageContext();        
+        outMsgCtx.setMessage(createAuthenticationRequest());        
+        outMsgCtx.addSubcontext(createPeerContext());
+        outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
+        outMsgCtx.addSubcontext(createClientMedataContext());
+        nestPrc.setOutboundMessageContext(outMsgCtx);    
+        
+        // Setup inbound context.
+        final MessageContext inMsgCtx = new MessageContext();
+        inMsgCtx.setMessage(createAuthenticationResponse());
+        nestPrc.setInboundMessageContext(inMsgCtx);
+        
+        // Add prc to flow.
+        prc.getSubcontext(AuthenticationContext.class)
+        .addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
+                        flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+                        
+        
+        updateFlowExecution(flowExecution);
+        
+        //set start view and ending event to transition on.
+        externalContext.setEventId("proceed");
+        setCurrentState("AuthRequest");       
+        resumeFlow(externalContext);
+        
+        mockOPServer.shutdown();
+        
+        //assert success conditions
+        assertFlowExecutionEnded();
+        assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+        assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+        assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");      
+       
+    }
+    
+    @Test 
+    public void testAuthnFlowFromAuthorizationCallback_UsingEncryptedJWTUserInfoResponse() throws Exception {
         
         setFlowPath(FLOW);
         setFlowModelResources(flowResources);
@@ -576,7 +702,8 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         // Second is userInfo
         mockOPServer.enqueue(new MockResponse().setResponseCode(200)
                 .setHeader("content-type", "application/jwt")
-                .setBody(createSignedUserInfoJWTResponseJSON("https://op.example.com","demo_rp")));
+                .setBody(createSignedAndEncryptedUserInfoJWTResponseJSON("https://op.example.com","demo_rp")
+                        .serialize()));
         mockOPServer.start(9918);
         
 

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


More information about the commits mailing list