[java-idp-oidc] branch main updated: JOIDC-11 - Support for client_credentials grant

Scott Cantor cantor.2 at osu.edu
Tue Jan 4 22:56:56 UTC 2022


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

scantor pushed a commit to branch main
in repository java-idp-oidc.

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

The following commit(s) were added to refs/heads/main by this push:
     new 1764b564 JOIDC-11 - Support for client_credentials grant
1764b564 is described below

commit 1764b5645bc3585650037203c99b55e8f1c1eb22
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Jan 4 17:56:53 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Early flow changes to add grant_type and fix scope validation.
---
 .../context/OIDCAuthenticationResponseContext.java |  45 +++++-
 .../logic/RequestedGrantTypesCondition.java        |  68 +++++++++
 .../context/logic/package-info.java}               |  22 +--
 .../AbstractTokenClaimsLookupFunction.java         |   3 +-
 .../TokenRequestRedirectURILookupFunction.java     |   2 +-
 .../navigate/TokenRequestScopeLookupFunction.java  |  21 +--
 .../UserInfoRequestClientIDLookupFunction.java     |   4 +-
 .../OIDCAuthenticationResponseContextTest.java     |  24 ++-
 .../impl/SetAccessTokenToResponseContext.java      |   6 +-
 .../impl/SetRefreshTokenToResponseContext.java     |   3 +-
 .../plugin/oidc/op/profile/impl/ValidateGrant.java |  52 ++++---
 .../plugin/oidc/op/profile/impl/ValidatePKCE.java  |  23 +--
 .../oidc/op/profile/impl/ValidateRedirectURI.java  |   4 +-
 .../plugin/oidc/op/profile/impl/ValidateScope.java |  60 ++++++--
 ...uteConsentEnabledInTokenClaimsSetPredicate.java |   7 +-
 .../idp/flows/oidc/token/token-beans.xml           |  36 +++--
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |  11 +-
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java | 166 +++++++++-----------
 .../SetRequestedClaimsToResponseContextTest.java   |   4 +-
 .../oidc/op/profile/impl/ValidateGrantTest.java    | 107 +++++++------
 .../oidc/op/profile/impl/ValidateScopeTest.java    | 168 +++++++++++++++++++--
 .../plugin/oidc/op/profile/impl/package-info.java  |  23 +--
 22 files changed, 559 insertions(+), 300 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
index e7a7b12d..4657d71b 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
@@ -101,9 +101,9 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     @Nullable
     private RefreshToken refreshToken;
 
-    /** Token (authz code, access token) claims. */
+    /** Authorization grant (authz code, access token) claims. */
     @Nullable
-    private TokenClaimsSet tokenClaims;
+    private TokenClaimsSet authorizationGrantClaims;
 
     /** Requested claims. */
     @Nullable
@@ -171,22 +171,55 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
     }
     
     /**
-     * Get token claims.
+     * Get the claims representing the authorization grant, which may be derived from an
+     * authorization code, refresh token, or assertion.
      * 
      * @return token claims
      */
     @Nullable
+    public TokenClaimsSet getAuthorizationGrantClaimsSet() {
+        return authorizationGrantClaims;
+    }
+
+    /**
+     * Set the claims representing the authorization grant, which may be derived from an
+     * authorization code, refresh token, or assertion.
+     * 
+     * @param claims token claims
+     */
+    public void setAuthorizationGrantClaimsSet(@Nullable final TokenClaimsSet claims) {
+        authorizationGrantClaims = claims;
+    }
+    
+    /**
+     * Get the claims representing the authorization grant, which may be derived from an
+     * authorization code, refresh token, or assertion.
+     * 
+     * <p>Renamed to {@link #getAuthorizationGrantClaimsSet()}.</p>
+     * 
+     * @return token claims
+     * 
+     * @deprecated
+     */
+    @Deprecated(since="3.1.0", forRemoval=true)
+    @Nullable
     public TokenClaimsSet getTokenClaimsSet() {
-        return tokenClaims;
+        return authorizationGrantClaims;
     }
 
     /**
-     * Set token claims.
+     * Set the claims representing the authorization grant, which may be derived from an
+     * authorization code, refresh token, or assertion.
+     * 
+     * <p>Renamed to {@link #setAuthorizationGrantClaimsSet(TokenClaimsSet)}.</p>
      * 
      * @param claims token claims
+     * 
+     * @deprecated
      */
+    @Deprecated(since="3.1.0", forRemoval=true)
     public void setTokenClaimsSet(@Nullable final TokenClaimsSet claims) {
-        tokenClaims = claims;
+        authorizationGrantClaims = claims;
     }
 
     /**
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/RequestedGrantTypesCondition.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/RequestedGrantTypesCondition.java
new file mode 100644
index 00000000..c2c7686c
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/RequestedGrantTypesCondition.java
@@ -0,0 +1,68 @@
+/*
+ * 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.oidc.op.messaging.context.logic;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+/**
+ * Checks whether a {@link TokenRequest} was for one of a set of candidate grant_type values.
+ */
+public class RequestedGrantTypesCondition implements Predicate<ProfileRequestContext> {
+
+    /** Candidate grant types. */
+    @Nonnull @NonnullElements private Set<GrantType> candidates;
+    
+    /** Constructor. */
+    public RequestedGrantTypesCondition() {
+        candidates = Collections.emptySet();
+    }
+    
+    /**
+     * Set the candidate grant_type values to check for.
+     * 
+     * @param types candidate types
+     */
+    public void setGrantTypes(@Nonnull @NonnullElements final Collection<GrantType> types) {
+        candidates = Set.copyOf(types);
+    }
+    
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (input.getInboundMessageContext() != null) {
+            final Object message = input.getInboundMessageContext().getMessage();
+            if (message instanceof TokenRequest) {
+                return candidates.contains(((TokenRequest) message).getAuthorizationGrant().getType());
+            } 
+        }
+        
+        return false;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/package-info.java
similarity index 54%
copy from idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
copy to idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/package-info.java
index ec39a3cd..d2122a69 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/package-info.java
@@ -15,26 +15,8 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
-
-import javax.annotation.Nonnull;
-
-import com.nimbusds.oauth2.sdk.Scope;
-
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-
 /**
- * For Token and UserInfo end points.
- * 
- * A function that returns copy of requested scope via a lookup function. This lookup locates scope from token for token
- * request handling. If token claims are not available, null is returned.
+ * Conditions related to OIDC messaging.
  */
-public class TokenRequestScopeLookupFunction extends AbstractTokenClaimsLookupFunction<Scope> {
-
-    /** {@inheritDoc} */
-    @Override
-    Scope doLookup(@Nonnull final TokenClaimsSet tokenClaims) {
-        return tokenClaims.getScope();
-    }
 
-}
\ No newline at end of file
+package net.shibboleth.idp.plugin.oidc.op.messaging.context.logic;
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
index 57eac1b1..f74cc2e9 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
@@ -54,12 +54,11 @@ public abstract class AbstractTokenClaimsLookupFunction<T>
         if (oidcResponseContext == null) {
             return null;
         }
-        final TokenClaimsSet tokenClaims = oidcResponseContext.getTokenClaimsSet();
+        final TokenClaimsSet tokenClaims = oidcResponseContext.getAuthorizationGrantClaimsSet();
         if (tokenClaims == null) {
             return null;
         }
         return doLookup(tokenClaims);
-
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
index 79b513d5..81f4ce6d 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
@@ -57,7 +57,7 @@ public class TokenRequestRedirectURILookupFunction extends AbstractTokenRequestL
         try {
             uri = new URI(redirectURI);
         } catch (final URISyntaxException e) {
-            log.error("Unable to parse uri from token request redirect_uri {}", redirectURI);
+            log.warn("Unable to parse uri from token request redirect_uri {}", redirectURI);
         }
         return uri;
     }
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
index ec39a3cd..3192b6a7 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
@@ -20,21 +20,24 @@ package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
 import javax.annotation.Nonnull;
 
 import com.nimbusds.oauth2.sdk.Scope;
-
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import com.nimbusds.oauth2.sdk.TokenRequest;
 
 /**
- * For Token and UserInfo end points.
- * 
- * A function that returns copy of requested scope via a lookup function. This lookup locates scope from token for token
- * request handling. If token claims are not available, null is returned.
+ * A function that returns a copy of requested scopes from a {@link TokenRequest}.
  */
-public class TokenRequestScopeLookupFunction extends AbstractTokenClaimsLookupFunction<Scope> {
+public class TokenRequestScopeLookupFunction extends AbstractTokenRequestLookupFunction<Scope> {
 
     /** {@inheritDoc} */
     @Override
-    Scope doLookup(@Nonnull final TokenClaimsSet tokenClaims) {
-        return tokenClaims.getScope();
+    Scope doLookup(@Nonnull final TokenRequest req) {
+        
+        if (req.getScope() != null) {
+            final Scope requestParameterScope = new Scope();
+            requestParameterScope.addAll(req.getScope());
+            return requestParameterScope;
+        }
+        
+        return null;
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/UserInfoRequestClientIDLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/UserInfoRequestClientIDLookupFunction.java
index f156a030..64a0f7c6 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/UserInfoRequestClientIDLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/UserInfoRequestClientIDLookupFunction.java
@@ -56,10 +56,10 @@ public class UserInfoRequestClientIDLookupFunction implements ContextDataLookupF
             return null;
         }
         final OIDCAuthenticationResponseContext ctx = msgCtx.getSubcontext(OIDCAuthenticationResponseContext.class);
-        if (ctx == null || ctx.getTokenClaimsSet() == null) {
+        if (ctx == null || ctx.getAuthorizationGrantClaimsSet() == null) {
             return null;
         }
-        return ctx.getTokenClaimsSet().getClientID();
+        return ctx.getAuthorizationGrantClaimsSet().getClientID();
 
     }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContextTest.java b/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContextTest.java
index f1733f3c..ac4572c3 100644
--- a/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContextTest.java
+++ b/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContextTest.java
@@ -39,8 +39,6 @@ import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
 import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-
 /** Tests for {@link OIDCAuthenticationResponseContext}.*/
 public class OIDCAuthenticationResponseContextTest {
 
@@ -62,7 +60,7 @@ public class OIDCAuthenticationResponseContextTest {
         Assert.assertNull(ctx.getScope());
         Assert.assertNull(ctx.getProcessedToken());
         Assert.assertNull(ctx.getRequestedClaims());
-        Assert.assertNull(ctx.getTokenClaimsSet());
+        Assert.assertNull(ctx.getAuthorizationGrantClaimsSet());
         Assert.assertNull(ctx.getAuthorizationCode());
         Assert.assertNull(ctx.getAccessToken());
         Assert.assertNull(ctx.getRefreshToken());
@@ -74,27 +72,27 @@ public class OIDCAuthenticationResponseContextTest {
     public void testSetters() throws URISyntaxException, ParseException {
         ctx.setAcr("acrValue");
         ctx.setAuthTime(Instant.ofEpochMilli(1));
-        Issuer issuer = new Issuer("iss");
-        Subject sub = new Subject("sub");
-        List<Audience> aud = new ArrayList<Audience>();
+        final Issuer issuer = new Issuer("iss");
+        final Subject sub = new Subject("sub");
+        final List<Audience> aud = new ArrayList<Audience>();
         aud.add(new Audience("aud"));
-        IDTokenClaimsSet token = new IDTokenClaimsSet(issuer, sub, aud, new Date(), new Date());
+        final IDTokenClaimsSet token = new IDTokenClaimsSet(issuer, sub, aud, new Date(), new Date());
         ctx.setIDToken(token);
         ctx.setSubject("sub");
-        URI uri = new URI("https://example.org");
+        final URI uri = new URI("https://example.org");
         ctx.setRedirectURI(uri);
         ctx.setRequestedSubject("sub");
-        Scope scope = new Scope();
+        final Scope scope = new Scope();
         ctx.setScope(scope);
-        JWSHeader header = new JWSHeader(JWSAlgorithm.ES256);
-        SignedJWT sJWT = new SignedJWT(header, token.toJWTClaimsSet());
+        final JWSHeader header = new JWSHeader(JWSAlgorithm.ES256);
+        final SignedJWT sJWT = new SignedJWT(header, token.toJWTClaimsSet());
         ctx.setProcessedToken(sJWT);
         Assert.assertEquals(ctx.getAcr().toString(), "acrValue");
         ctx.setAcr(null);
-        OIDCClaimsRequest claims = new OIDCClaimsRequest();
+        final OIDCClaimsRequest claims = new OIDCClaimsRequest();
         ctx.setRequestedClaims(claims);
         ctx.setSubjectType("pairwise");
-        UserInfo info = new UserInfo(sub);
+        final UserInfo info = new UserInfo(sub);
         ctx.setUserInfo(info);
         Assert.assertNull(ctx.getAcr());
         Assert.assertEquals(ctx.getAuthTime(), Instant.ofEpochMilli(1));
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
index 463b33ad..807fea1f 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
@@ -30,6 +30,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
@@ -270,8 +271,9 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
         final AccessTokenClaimsSet claimsSet;
         if (tokenClaimsSet != null) {
             // We may not use original claims as input for scope / delivery claims as they may have been reduced.
-            claimsSet = new AccessTokenClaimsSet(tokenClaimsSet, getOidcResponseContext().getScope(), claims, claimsUI,
-                    Instant.now(), dateExp);
+            claimsSet = new AccessTokenClaimsSet(tokenClaimsSet,
+                    getOidcResponseContext().getScope() != null ? getOidcResponseContext().getScope() : new Scope(),
+                    claims, claimsUI, Instant.now(), dateExp);
         } else {
             final OIDCAuthenticationResponseConsentContext consentCtx =
                     consentContextLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
index ec02b216..52361944 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
@@ -98,7 +98,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
             return false;
         }
         
-        if (!getOidcResponseContext().getScope().contains(OIDCScopeValue.OFFLINE_ACCESS)) {
+        if (getOidcResponseContext().getScope() == null ||
+                !getOidcResponseContext().getScope().contains(OIDCScopeValue.OFFLINE_ACCESS)) {
             log.debug("{} No offline_access scope, nothing to do", getLogPrefix());
             return false;
         }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 2f372fb4..a2875afd 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -53,10 +53,17 @@ import net.shibboleth.utilities.java.support.security.DataSealer;
 import net.shibboleth.utilities.java.support.security.DataSealerException;
 
 /**
- * Action that validates authorization code / refresh token is a valid one. Code is valid if it is successfully
- * unwrapped, parsed as authz code/ refresh token , is not expired, is issued for the client and has not been used
- * before (authz code) or authz code used to produce it has not been revoked (refresh token). Validated code is stored
- * to response context retrievable as claims {@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}.
+ * Action that validates an authorization grant.
+ * 
+ * <p>A grant is valid if it is successfully unwrapped, parsed as a code or refresh token, is unexpired, was issued
+ * to the expected client and has not been used before (authz code) or the authz code used to produce it has not been
+ * revoked (refresh token).</p>
+ * 
+ * <p> The validated claims from the grant are stored to response context via
+ * {@link OIDCAuthenticationResponseContext#getAuthorizationGrantClaimsSet()}.</p>
+ * 
+ * <p>Note that the addition of support for the "client_credentials" grant type means that there may not in fact be a
+ * grant, or resulting claims set.</p>
  */
 public class ValidateGrant extends AbstractOIDCTokenResponseAction {
 
@@ -153,38 +160,40 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
         return true;
     }
     
-    // Checkstyle: CyclomaticComplexity OFF
-
+// Checkstyle: CyclomaticComplexity|MethodLength OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final AuthorizationGrant grant = getTokenRequest().getAuthorizationGrant();
+        
+        log.debug("{} Validating grant type: {}", getLogPrefix(),grant.getType());
+        
         TokenClaimsSet tokenClaimsSet = null;
-        if (grant.getType().equals(GrantType.AUTHORIZATION_CODE)) {
+        if (GrantType.AUTHORIZATION_CODE.equals(grant.getType())) {
             final AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
             if (codeGrant.getAuthorizationCode() != null && codeGrant.getAuthorizationCode().getValue() != null) {
                 try {
                     final AuthorizeCodeClaimsSet authzCodeClaimsSet =
                             AuthorizeCodeClaimsSet.parse(codeGrant.getAuthorizationCode().getValue(), dataSealer);
-                    log.debug("{} authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
+                    log.debug("{} Authz code unwrapped {}", getLogPrefix(), authzCodeClaimsSet.serialize());
                     if (!replayCache.check(getClass().getName(), authzCodeClaimsSet.getID(),
                             authzCodeClaimsSet.getExp())) {
                         log.error("{} Replay detected of authz code {}", getLogPrefix(), authzCodeClaimsSet.getID());
                         if (!revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE,
                                 authzCodeClaimsSet.getID())) {
-                            log.error("{} Fatal error! Unable to set entry to revocation cache", getLogPrefix());
+                            log.warn("{} Fatal error, unable to set entry to revocation cache", getLogPrefix());
                         }
                         ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                         return;
                     }
                     tokenClaimsSet = authzCodeClaimsSet;
                 } catch (final DataSealerException | ParseException e) {
-                    log.error("{} Obtaining authz code failed {}", getLogPrefix(), e.getMessage());
+                    log.warn("{} Obtaining authz code failed {}", getLogPrefix(), e.getMessage());
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
             }
-        } else if (grant.getType().equals(GrantType.REFRESH_TOKEN)) {
+        } else if (GrantType.REFRESH_TOKEN.equals(grant.getType())) {
             final RefreshTokenGrant refreshTokentokenGrant = (RefreshTokenGrant) grant;
             if (refreshTokentokenGrant.getRefreshToken() != null
                     && refreshTokentokenGrant.getRefreshToken().getValue() != null) {
@@ -193,37 +202,40 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
                             .parse(refreshTokentokenGrant.getRefreshToken().getValue(), dataSealer);
                     if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE,
                             refreshTokenClaimsSet.getID())) {
-                        log.error("{} authorize code {} and all derived tokens have been revoked", getLogPrefix(),
+                        log.error("{} Authz code {} and all derived tokens have been revoked", getLogPrefix(),
                                 refreshTokenClaimsSet.getID());
                         ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                         return;
                     }
                     tokenClaimsSet = refreshTokenClaimsSet;
                 } catch (final ParseException | DataSealerException e) {
-                    log.error("{} Obtaining refresh token failed {}", getLogPrefix(), e.getMessage());
+                    log.warn("{} Obtaining refresh token failed {}", getLogPrefix(), e.getMessage());
                     ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
                     return;
                 }
             }
+        } else if (GrantType.CLIENT_CREDENTIALS.equals(grant.getType())) {
+            return;
         }
+        
         if (tokenClaimsSet == null) {
-            log.error("{} Grant type not supported", getLogPrefix());
+            log.warn("{} Grant type not supported", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
         if (tokenClaimsSet.isExpired()) {
-            log.error("{} token exp is in the past {}", getLogPrefix(), tokenClaimsSet.getExp());
+            log.warn("{} token exp is in the past {}", getLogPrefix(), tokenClaimsSet.getExp());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
         if (!tokenClaimsSet.getClientID().getValue().equals(rpCtx.getRelyingPartyId())) {
-            log.error("{} token issued for client {}, expected value was {}", getLogPrefix(),
+            log.warn("{} token issued for client {}, expected value was {}", getLogPrefix(),
                     tokenClaimsSet.getClientID().getValue(), rpCtx.getRelyingPartyId());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
             return;
         }
-        getOidcResponseContext().setTokenClaimsSet(tokenClaimsSet);
+        getOidcResponseContext().setAuthorizationGrantClaimsSet(tokenClaimsSet);
     }
-    
-    // Checkstyle: CyclomaticComplexity ON
-}
+// Checkstyle: CyclomaticComplexity|MethodLength ON
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidatePKCE.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidatePKCE.java
index 8a65d32f..b80e2e6e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidatePKCE.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidatePKCE.java
@@ -124,12 +124,13 @@ public class ValidatePKCE extends AbstractOIDCResponseAction {
         if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
-        if (getOidcResponseContext().getTokenClaimsSet() == null) {
-            log.error("{} No validated token claims set available, missing a prior action", getLogPrefix());
+        if (getOidcResponseContext().getAuthorizationGrantClaimsSet() == null) {
+            log.warn("{} No validated authorization grant claims set available", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
-        if (!AuthorizeCodeClaimsSet.VALUE_TYPE_AC.equals(getOidcResponseContext().getTokenClaimsSet().getType())) {
+        if (!AuthorizeCodeClaimsSet.VALUE_TYPE_AC.equals(
+                getOidcResponseContext().getAuthorizationGrantClaimsSet().getType())) {
             log.debug("{} No authorization code presented, PKCE not applied, nothing to do", getLogPrefix());
             return false;
         }
@@ -137,7 +138,7 @@ public class ValidatePKCE extends AbstractOIDCResponseAction {
         forcePKCE = forcePKCECondition.test(profileRequestContext);
         plainPKCE = allowPKCEPlainCondition.test(profileRequestContext);
         
-        codeChallenge = getOidcResponseContext().getTokenClaimsSet().getCodeChallenge();
+        codeChallenge = getOidcResponseContext().getAuthorizationGrantClaimsSet().getCodeChallenge();
         // Checks whether PKCE needs to be validated.
         if ((codeChallenge == null || codeChallenge.isEmpty()) && !forcePKCE) {
             log.debug("{} No PKCE code challenge in request, nothing to do", getLogPrefix());
@@ -153,26 +154,26 @@ public class ValidatePKCE extends AbstractOIDCResponseAction {
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         if (codeChallenge == null || codeChallenge.isEmpty()) {
             // To save one action we have this late verification of the authentication request for PKCE parameter.
-            log.error(
+            log.warn(
                     "{} No PKCE code challenge presented in authentication request" +
                             " even though required to access token endpoint", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
             return;
         }
         if (codeVerifier == null || codeVerifier.isEmpty()) {
-            log.error("{} No PKCE code verifier for code challenge presented in token request", getLogPrefix());
+            log.warn("{} No PKCE code verifier for code challenge presented in token request", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
             return;
         }
         if (codeChallenge.startsWith("plain")) {
             if (!plainPKCE) {
-                log.error("{} Plain PKCE code challenge method not allowed", getLogPrefix());
+                log.warn("{} Plain PKCE code challenge method not allowed", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
                 return;
             }
             final String codeChallengeValue = codeChallenge.substring("plain".length());
             if (!codeVerifier.equals(codeChallengeValue)) {
-                log.error("{} PKCE code challenge {} not matching code verifier {}", getLogPrefix(), codeChallengeValue,
+                log.warn("{} PKCE code challenge {} not matching code verifier {}", getLogPrefix(), codeChallengeValue,
                         codeVerifier);
                 ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_AUTHN_ERROR);
                 return;
@@ -183,7 +184,7 @@ public class ValidatePKCE extends AbstractOIDCResponseAction {
             try {
                 md = MessageDigest.getInstance("SHA-256");
             } catch (final NoSuchAlgorithmException e) {
-                log.error("{} PKCE S256 code challenge verification requires SHA-256", getLogPrefix(),
+                log.warn("{} PKCE S256 code challenge verification requires SHA-256", getLogPrefix(),
                         codeChallengeValue, codeVerifier);
                 ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_AUTHN_ERROR);
                 return;
@@ -191,13 +192,13 @@ public class ValidatePKCE extends AbstractOIDCResponseAction {
             final byte[] hash = md.digest(codeVerifier.getBytes(Charset.forName("utf-8")));
             final String codeChallengeComparisonValue = Base64URL.encode(hash).toString();
             if (!codeChallengeComparisonValue.equals(codeChallengeValue)) {
-                log.error("{} PKCE code challenge {} not matching code verifier {}({})", getLogPrefix(),
+                log.warn("{} PKCE code challenge {} not matching code verifier {}({})", getLogPrefix(),
                         codeChallengeValue, codeVerifier, codeChallengeComparisonValue);
                 ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_AUTHN_ERROR);
                 return;
             }
         } else {
-            log.error("{} Unknown code challenge method", getLogPrefix());
+            log.warn("{} Unknown code challenge method", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
             return;
         }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
index 1009c1c6..b0dcb075 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
@@ -83,14 +83,14 @@ public class ValidateRedirectURI extends AbstractOIDCAuthenticationResponseActio
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         final URI requestRedirectURI = redirectURILookupStrategy.apply(profileRequestContext);
         if (requestRedirectURI == null) {
-            log.error("{} Redirection URI of the request not located for verification", getLogPrefix());
+            log.warn("{} Redirection URI of the request not located for verification", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
             return;
         }
         
         final Set<URI> redirectionURIs = validRedirectURIsLookupStrategy.apply(profileRequestContext);
         if (redirectionURIs == null || redirectionURIs.isEmpty()) {
-            log.error("{} Client has not registered Redirection URIs. Redirection URI cannot be validated.",
+            log.warn("{} Client has not registered Redirection URIs. Redirection URI cannot be validated.",
                     getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
             return;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
index 85164721..f22098d4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
@@ -21,6 +21,7 @@ import java.util.Iterator;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
@@ -33,11 +34,16 @@ import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestResponseTypeLookupFunction;
 import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestedScopeLookupFunction;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Action that validates requested scopes are registered ones. Validated scopes are stored to response context.
- * Offline_access scope is ignored in authentication endpoint validation unless response type contains code.
+ * Action that validates requested scopes are registered ones and stores the resulting set in the
+ * response context.
+ * 
+ * <p>Explicitly requested scopes are also filtered against, and override, any scopes previously
+ * validated as part of an authorization grant claim set.</p>
+ * 
+ * <p>The "offline_access" scope is ignored in authentication endpoint validation unless the
+ * response type includes "code".</p>
  */
 public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
 
@@ -45,43 +51,68 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
     @Nonnull private Logger log = LoggerFactory.getLogger(ValidateScope.class);
 
     /** Strategy used to obtain the requested scope value. */
-    @Nonnull private Function<ProfileRequestContext, Scope> scopeLookupStrategy;
-
+    @Nullable private Function<ProfileRequestContext,Scope> requestedScopesLookupStrategy;
+    
     /**
      * Constructor.
      */
     public ValidateScope() {
-        scopeLookupStrategy = new DefaultRequestedScopeLookupFunction();
+        requestedScopesLookupStrategy = new DefaultRequestedScopeLookupFunction();
     }
 
     /**
-     * Set the strategy used to locate the requested scope to use.
+     * Set the strategy used to locate the requested scope to validate.
      * 
      * @param strategy lookup strategy
      */
-    public void setScopeLookupStrategy(@Nonnull final Function<ProfileRequestContext, Scope> strategy) {
+    public void setScopeLookupStrategy(@Nullable final Function<ProfileRequestContext, Scope> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        scopeLookupStrategy = Constraint.isNotNull(strategy, "ScopeLookupStrategy lookup strategy cannot be null");
+        
+        requestedScopesLookupStrategy = strategy;
     }
 
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        // These come from client metadata.
         final Scope registeredScopes = getMetadataContext().getClientInformation().getMetadata().getScope();
         if (registeredScopes == null || registeredScopes.isEmpty()) {
             log.debug("{} No registered scopes for client {}, nothing to do", getLogPrefix(),
                     getMetadataContext().getClientInformation().getID());
             return;
         }
-        final Scope requestedScopes = scopeLookupStrategy.apply(profileRequestContext);
+        
+        // These come from a previous authorization grant (authz code or refresh token).
+        Scope previouslyGrantedScopes = null;
+        if (getOidcResponseContext().getAuthorizationGrantClaimsSet() != null) {
+            previouslyGrantedScopes = getOidcResponseContext().getAuthorizationGrantClaimsSet().getScope();
+        }
+
+        // These come from a request object or parameter. Absent by definition on the UserInfo endpoint.
+        Scope requestedScopes = requestedScopesLookupStrategy != null ?
+                requestedScopesLookupStrategy.apply(profileRequestContext) : null;
+        if (requestedScopes == null) {
+            // With none requested, simply swap requested for previously granted, if any.
+            // Set previous set to null since there's no need to filter against it.
+            requestedScopes = previouslyGrantedScopes;
+            previouslyGrantedScopes = null;
+        }
+        
         for (Iterator<Scope.Value> i = requestedScopes.iterator(); i.hasNext();) {
             final Scope.Value scope = i.next();
             if (!registeredScopes.contains(scope)) {
-                log.warn("{} removing requested scope {} for rp {} as it is not a registered one", getLogPrefix(),
+                log.warn("{} Removing requested but unregistered scope {} for RP {}", getLogPrefix(),
+                        scope.getValue(), getMetadataContext().getClientInformation().getID());
+                i.remove();
+            } else if (previouslyGrantedScopes != null && !previouslyGrantedScopes.contains(scope)) {
+                log.warn("{} Removing requested but previously ungranted scope {} for RP {}", getLogPrefix(),
                         scope.getValue(), getMetadataContext().getClientInformation().getID());
                 i.remove();
             }
         }
+        
         if (requestedScopes.contains(OIDCScopeValue.OFFLINE_ACCESS)) {
             // DefaultRequestResponseTypeLookupFunction returns response type only in authentication end point.
             // It is enough to remove offline_scope in this first validation turn.
@@ -91,6 +122,11 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
                 requestedScopes.remove(OIDCScopeValue.OFFLINE_ACCESS);
             }
         }
-        getOidcResponseContext().setScope(requestedScopes);
+        
+        if (!requestedScopes.isEmpty()) {
+            getOidcResponseContext().setScope(requestedScopes);
+        }
     }
+// Checkstyle: CyclomaticComplexity ON
+    
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
index c2fe1e2c..9b8172b6 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
@@ -28,7 +28,8 @@ import net.shibboleth.idp.profile.logic.AbstractRelyingPartyPredicate;
 
 /**
  * A predicate implementation that checks if attribute consent flag is enabled. The value is fetched from
- * {@link TokenClaimsSet#isConsentEnabled()} via #{@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}
+ * {@link TokenClaimsSet#isConsentEnabled()} via
+ * {@link OIDCAuthenticationResponseContext#getAuthorizationGrantClaimsSet()}
  * under outbound message context. Default value is false, if any of the objects in the chain is null.
  */
 public class AttributeConsentEnabledInTokenClaimsSetPredicate extends AbstractRelyingPartyPredicate {
@@ -39,8 +40,8 @@ public class AttributeConsentEnabledInTokenClaimsSetPredicate extends AbstractRe
         if (outboundMessageCtx != null) {
             final OIDCAuthenticationResponseContext oidcResponseContext = 
                     outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class, false);
-            if (oidcResponseContext != null && oidcResponseContext.getTokenClaimsSet() != null) {
-                return oidcResponseContext.getTokenClaimsSet().isConsentEnabled();
+            if (oidcResponseContext != null && oidcResponseContext.getAuthorizationGrantClaimsSet() != null) {
+                return oidcResponseContext.getAuthorizationGrantClaimsSet().isConsentEnabled();
             }
         }
         return false;
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index daa7c646..33dac13a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -35,17 +35,29 @@
         c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:replayCache-ref="shibboleth.ReplayCache"
         p:revocationCache-ref="shibboleth.RevocationCache" />
-        
-    <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype" />
 
-    <bean id="ValidateRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateRedirectURI"
-        scope="prototype" p:redirectURILookupStrategy-ref="shibboleth.TokenRequestRedirectURILookupStrategy"
-        p:validRedirectURIsLookupStrategy-ref="shibboleth.TokenRequestValidRequestUrisLookupStrategy">
-        <property name="activationCondition">
-            <ref bean="GrantTypeAuthorizationCode" />
-        </property>
+    <!-- Condition signaling that request was NOT for client_credentials grant. -->        
+    <bean id="NotClientCredentialsGrantCondition" parent="shibboleth.Conditions.NOT">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+                p:grantTypes="T(com.nimbusds.oauth2.sdk.GrantType).CLIENT_CREDENTIALS" />
+        </constructor-arg>
     </bean>
 
+    <!-- Condition signaling that request was for authorizaton_code grant. -->        
+    <bean id="AuthorizationCodeGrantCondition"
+        class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.RequestedGrantTypesCondition"
+        p:grantTypes="T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE" />
+    
+    <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype"
+        p:activationCondition-ref="NotClientCredentialsGrantCondition" />
+
+    <bean id="ValidateRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateRedirectURI"
+        scope="prototype"
+        p:activationCondition-ref="AuthorizationCodeGrantCondition"
+        p:redirectURILookupStrategy-ref="shibboleth.TokenRequestRedirectURILookupStrategy"
+        p:validRedirectURIsLookupStrategy-ref="shibboleth.TokenRequestValidRequestUrisLookupStrategy" />
+
     <bean id="shibboleth.TokenRequestRedirectURILookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestRedirectURILookupFunction"
         scope="prototype" />
@@ -188,14 +200,6 @@
         </property>
     </bean>
 
-    <bean id="GrantTypeAuthorizationCode" parent="shibboleth.Conditions.Expression">
-        <constructor-arg>
-            <value>
-                #profileContext.getInboundMessageContext().getMessage().getAuthorizationGrant().getType().equals(T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE)
-            </value>
-        </constructor-arg>
-    </bean>
-
     <bean id="PopulateTokenEndpointJwtSignatureValidationParameters"
             class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
             p:configurationLookupStrategy-ref="shibboleth.oidc.SignatureValidationConfigurationLookup"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
index 758b42b2..f64a95f5 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
@@ -34,13 +34,12 @@
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction"
         scope="prototype" />
 
-    <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
-        p:scopeLookupStrategy-ref="shibboleth.TokenRequestScopeLookupStrategy" />
+    <bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype">
+        <property name="scopeLookupStrategy">
+            <null/>
+        </property>
+    </bean>
     
-    <bean id="shibboleth.TokenRequestScopeLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction"
-        scope="prototype" />
-
     <bean id="SetRequestedClaimsToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRequestedClaimsToResponseContext" scope="prototype"
         p:requestedClaimsLookupStrategy-ref="shibboleth.TokenRequestRequestedClaimsLookupFunction"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 6e0f61fb..677ff1dd 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -18,7 +18,6 @@
 package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.time.Instant;
@@ -33,7 +32,6 @@ import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
-import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.SignedJWT;
@@ -43,8 +41,6 @@ import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
 import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
-import com.nimbusds.oauth2.sdk.auth.Secret;
-import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.pkce.CodeChallenge;
 import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
 import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
@@ -130,33 +126,30 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
     }
     
-    protected void initializeGrantAndRequest(String clientId, Map<String, String> requestParameters) 
-            throws NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException,
-            IOException {
+    protected void initializeGrantAndRequest(final String clientId, final Map<String, String> requestParameters)
+            throws IOException {
         setHttpFormRequest("POST", requestParameters);
         storeMetadata(storageService, clientId, clientSecret);
         setBasicAuth(clientId, clientSecret);
     }
 
     @Test
-    public void testValidGrant() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException {
+    public void testValidGrant() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientId), clientId));
         storeConsent(storageService, "jdoe", clientId, "mail");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
     }
     
     @Test
-    public void testValidLegacyGrant() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidLegacyGrant() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildLegacyAuthorizationCode(clientId), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessToken accessToken = response.getTokens().getAccessToken();
         Assert.assertNotNull(accessToken);
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
@@ -164,21 +157,19 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
     
     @Test
-    public void testValidLegacyConsentGrant() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidLegacyConsentGrant() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildLegacyAuthorizationCode(clientId, "mail"), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessToken accessToken = response.getTokens().getAccessToken();
         Assert.assertNotNull(accessToken);
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
         validateConsentFromAccessToken(response, true);
     }
     
-    protected void validateConsentFromAccessToken(final OIDCTokenResponse response, final boolean value) throws
-        NoSuchAlgorithmException, DataSealerException, ComponentInitializationException,
-        ParseException {
+    protected void validateConsentFromAccessToken(final OIDCTokenResponse response, final boolean value)
+            throws Exception {
         final AccessTokenClaimsSet claims = unwrapAccessToken(response);
         Assert.assertTrue(claims.getClaimsSet().getClaims().containsKey(TokenClaimsSet.KEY_CONSENT_ENABLED));
         Assert.assertEquals(claims.getClaimsSet().getBooleanClaim(TokenClaimsSet.KEY_CONSENT_ENABLED).booleanValue(),
@@ -187,14 +178,13 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantSaml() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException, java.text.ParseException {
+    public void testValidGrantSaml() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdSaml), clientIdSaml));
         setBasicAuth(clientIdSaml, clientSecretSaml);
         storeConsent(storageService, "jdoe", clientIdSaml, "mail");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
         // the email-claim exists in id_token as it's defined to be always included in the SAML metadata
         final JWT idToken = response.getOIDCTokens().getIDToken();
@@ -202,34 +192,32 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         Assert.assertEquals(idToken.getJWTClaimsSet().getClaim("email"), "jdoe at example.org");
     }
 
-    protected String buildAuthorizationCode(String clientId) throws NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException {
+    protected String buildAuthorizationCode(final String clientId) throws Exception {
         return buildAuthorizationCode(clientId, null);
     }
     
-    protected String buildAuthorizationCode(String clientId, String verifier) throws NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    protected String buildAuthorizationCode(final String clientId, final String verifier) throws Exception {
         return buildAuthorizationCode(clientId, verifier, null, null, null);
     }
 
-    protected String buildAuthorizationCode(String clientId, String verifier, JSONObject deliveryClaims,
-            JSONObject deliveryClaimsIDToken, JSONObject deliveryClaimsUserInfo) throws NoSuchAlgorithmException,
-            URISyntaxException, DataSealerException, ComponentInitializationException {
+    protected String buildAuthorizationCode(final String clientId, final String verifier,
+            final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
+            final JSONObject deliveryClaimsUserInfo) throws Exception {
         return ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
                 redirectUri, verifier, deliveryClaims, deliveryClaimsIDToken, deliveryClaimsUserInfo, 
                 "openid profile email").toString();
     }
     
-    protected String buildLegacyAuthorizationCode(String clientId, String... consentedClaims) throws 
-            NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException {
+    protected String buildLegacyAuthorizationCode(final String clientId, final String... consentedClaims)
+            throws Exception {
         final String json = buildJsonForLegacyToken("jdoe", clientId, Scope.parse("openid email"), "ac",
                 consentedClaims);
         return new AuthorizationCode(BaseOIDCResponseActionTest.initializeDataSealer().wrap(json,
                 Instant.now().plusSeconds(30))).getValue();
     }
 
-    protected String buildLegacyRefreshToken(String clientId, String... consentedClaims) throws 
-    NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException {
+    protected String buildLegacyRefreshToken(final String clientId, final String... consentedClaims)
+            throws Exception {
         final String json = buildJsonForLegacyToken("jdoe", clientId, Scope.parse("openid email"), "rf",
                 consentedClaims);
         return new RefreshToken(BaseOIDCResponseActionTest.initializeDataSealer().wrap(json,
@@ -237,26 +225,23 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidSecretJWT() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException, JOSEException {
-        ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+    public void testValidSecretJWT() throws Exception {
+        final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
         final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, JWSAlgorithm.HS256);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
 
     @Test
-    public void testValidSecretJWTNoAlg() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
-        ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+    public void testValidSecretJWTNoAlg() throws Exception {
+        final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
         final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, null);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
     
     @Test
-    public void testValidGrantValidRequestMissingPlainPKCE() throws ParseException, IOException,
-        NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidRequestMissingPlainPKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlain, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlain, plainVerifier()), clientIdPkcePlain));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -265,8 +250,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
     
     @Test
-    public void testValidGrantInvalidUnforcedPlainPKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantInvalidUnforcedPlainPKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlainUnforced, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlainUnforced, plainVerifier()), clientIdPkcePlainUnforced, null,
                 null, codeVerifier + "invalid"));
@@ -276,8 +260,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantInvalidPlainPKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantInvalidPlainPKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlain, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlain, plainVerifier()), clientIdPkcePlain, null, null, 
                 codeVerifier + "invalid"));
@@ -287,30 +270,27 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantValidPlainPKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidPlainPKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlain, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlain, plainVerifier()), clientIdPkcePlain, null, null,
                 codeVerifier));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
 
     @Test
-    public void testValidGrantValidUnforcedPlainPKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidUnforcedPlainPKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlainUnforced, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlainUnforced, plainVerifier()), clientIdPkcePlainUnforced, null,
                 null, codeVerifier));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
 
     @Test
-    public void testValidGrantValidRequestMissingS256PKCE() throws ParseException, IOException,
-        NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidRequestMissingS256PKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkceS256, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkceS256, s256Verifier()), clientIdPkceS256));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
@@ -319,8 +299,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
     
     @Test
-    public void testValidGrantInvalidUnforcedS256PKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantInvalidUnforcedS256PKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkcePlainUnforced, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkcePlainUnforced, s256Verifier()), clientIdPkcePlainUnforced, null, 
                 "S256", codeVerifier + "invalid"));
@@ -330,8 +309,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantInvalidS256PKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantInvalidS256PKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkceS256, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkceS256, s256Verifier()), clientIdPkceS256, null, "S256", 
                 codeVerifier + "invalid"));
@@ -341,37 +319,33 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantValidS256PKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidS256PKCE() throws Exception {
         initializeGrantAndRequest(clientIdPkceS256, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdPkceS256, s256Verifier()), clientIdPkceS256, null, "S256",
                 codeVerifier));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
 
     @Test
-    public void testValidGrantValidUnforcedS256PKCE() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidGrantValidUnforcedS256PKCE() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientId, s256Verifier()), clientId, null, "S256", codeVerifier));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         Assert.assertNotNull(response.getTokens().getAccessToken());
     }
     
     @Test
-    public void testInvalidSecretJWT() throws ParseException, IOException, NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException, JOSEException {
-        ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret + "invalid");
+    public void testInvalidSecretJWT() throws Exception {
+        final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret + "invalid");
         final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, JWSAlgorithm.HS256);
         assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
     }
     
     @Test
-    public void testValidGrantWrappedClaimsUI() throws NoSuchAlgorithmException, URISyntaxException,
-        DataSealerException, ComponentInitializationException, IOException {
+    public void testValidGrantWrappedClaimsUI() throws Exception {
         final String claimName = "name";
         final String claimValue = "John Doe";
         final JSONObject claimsUI = new JSONObject();
@@ -379,7 +353,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientId, null, null, null, claimsUI), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
         Assert.assertNotNull(claimsSet);
         final ClaimsSet dlClaimsSet = claimsSet.getUserinfoDeliveryClaims();
@@ -390,8 +364,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidGrantWrappedClaims() throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
-        ComponentInitializationException, IOException {
+    public void testValidGrantWrappedClaims() throws Exception {
         final String claimName = "name";
         final String claimValue = "John Doe";
         final JSONObject claims = new JSONObject();
@@ -399,7 +372,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientId, null, claims, null, null), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
         Assert.assertNotNull(claimsSet);
         final ClaimsSet dlClaimsSet = claimsSet.getDeliveryClaims();
@@ -410,12 +383,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidLegacyRefreshTokenGrant() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidLegacyRefreshTokenGrant() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
                 buildLegacyRefreshToken(clientId), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessToken accessToken = response.getTokens().getAccessToken();
         Assert.assertNotNull(accessToken);
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
@@ -423,12 +395,11 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
 
     @Test
-    public void testValidLegacyConsentRefreshTokenGrant() throws ParseException, IOException, NoSuchAlgorithmException,
-        URISyntaxException, DataSealerException, ComponentInitializationException {
+    public void testValidLegacyConsentRefreshTokenGrant() throws Exception {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
                 buildLegacyRefreshToken(clientId, "mail"), clientId));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
         final AccessToken accessToken = response.getTokens().getAccessToken();
         Assert.assertNotNull(accessToken);
         Assert.assertNotNull(response.getOIDCTokens().getIDToken());
@@ -448,7 +419,8 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
     
     private String plainVerifier() {
-        return "plain" + CodeChallenge.compute(CodeChallengeMethod.PLAIN, new CodeVerifier(codeVerifier)).getValue();        
+        return "plain" + CodeChallenge.compute(
+                CodeChallengeMethod.PLAIN, new CodeVerifier(codeVerifier)).getValue();        
     }
     
     private String s256Verifier() {
@@ -456,38 +428,37 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     }
     
     protected FlowExecutionResult launchWithJwtAuthentication(final JWTAuthentication authnMethod, final JWSAlgorithm algorithm)
-            throws NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException,
-            IOException {
-        String code = ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
+            throws Exception {
+        final String code = ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
                 redirectUri).toString();
         storeMetadata(storageService, clientId, clientSecret, JWSAlgorithm.HS256,
                 ClientAuthenticationMethod.CLIENT_SECRET_JWT);
-        Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code", code, clientId);
+        final Map<String, String> requestParameters =
+                createRequestParameters(redirectUri, "authorization_code", code, clientId);
         populateClientAssertionParams(requestParameters, authnMethod);
         setHttpFormRequest("POST", requestParameters);
         return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
 
     protected FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt, final JWSAlgorithm algorithm,
-            final ClientAuthenticationMethod method)
-            throws NoSuchAlgorithmException, URISyntaxException, DataSealerException, ComponentInitializationException,
-            IOException {
-        String code = ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
+            final ClientAuthenticationMethod method) throws Exception {
+        final String code = ValidateGrantTest.buildAuthorizationCode(clientId, "https://op.example.org", "jdoe", "mock",
                 redirectUri).toString();
         if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
             storeMetadata(storageService, clientId, clientSecret, algorithm, method);
         } else {
             storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
         }
-        Map<String, String> requestParameters = createRequestParameters(redirectUri, "authorization_code", code, clientId);
+        final Map<String, String> requestParameters =
+                createRequestParameters(redirectUri, "authorization_code", code, clientId);
         populateClientAssertionParams(requestParameters, jwt);
         setHttpFormRequest("POST", requestParameters);
         return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
 
-    protected Map<String, String> createRequestParameters(String redirectUri, String grantType, String code, 
-            String clientId) {
-        Map<String, String> parameters = new HashMap<>();
+    protected Map<String, String> createRequestParameters(final String redirectUri, final String grantType,
+            final String code,  final String clientId) {
+        final Map<String, String> parameters = new HashMap<>();
         addNonNullValue(parameters, "redirect_uri", redirectUri);
         addNonNullValue(parameters, "grant_type", grantType);
         if ("refresh_token".equals(grantType)) {
@@ -499,16 +470,17 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
         return parameters;
     }
     
-    protected Map<String, String> createRequestParameters(String redirectUri, String grantType, String code, 
-            String clientId, String codeChallenge, String codeChallengeMethod, String codeVerifier) {
-        Map<String, String> parameters = createRequestParameters(redirectUri, grantType, code, clientId);
+    protected Map<String, String> createRequestParameters(final String redirectUri, final String grantType,
+            final String code, final String clientId, final String codeChallenge, final String codeChallengeMethod,
+            final String codeVerifier) {
+        final Map<String, String> parameters = createRequestParameters(redirectUri, grantType, code, clientId);
         addNonNullValue(parameters, "code_challenge", codeChallenge);
         addNonNullValue(parameters, "code_challenge_method", codeChallengeMethod);
         addNonNullValue(parameters, "code_verifier", codeVerifier);
         return parameters;
     }
     
-    private void addNonNullValue(Map<String, String> map, String key, String value) {
+    private void addNonNullValue(final Map<String, String> map, final String key, final String value) {
         if (value != null) {
             map.put(key, value);
         }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRequestedClaimsToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRequestedClaimsToResponseContextTest.java
index 84dfbfa2..f13e29aa 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRequestedClaimsToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRequestedClaimsToResponseContextTest.java
@@ -56,7 +56,7 @@ public class SetRequestedClaimsToResponseContextTest extends BaseOIDCResponseAct
                 new Scope()).setACR(new ACR("0")).setClaims(OIDCClaimsRequest.parse("{\"id_token\":{\"email\":{\"essential\":true}},\"userinfo\":{\"name\":{\"essential\":true}}}")).build();
         respCtx.setSubject("subject");
         respCtx.setAuthTime(Instant.now());
-        respCtx.setTokenClaimsSet(claims);
+        respCtx.setAuthorizationGrantClaimsSet(claims);
         respCtx.setAcr("0");
         respCtx.setRedirectURI(new URI("http://example.com"));
         action = new SetRequestedClaimsToResponseContext();
@@ -72,7 +72,7 @@ public class SetRequestedClaimsToResponseContextTest extends BaseOIDCResponseAct
         init();
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
-        final JSONObject claims = (JSONObject) respCtx.getTokenClaimsSet().getClaimsSet().getClaim("claims");
+        final JSONObject claims = (JSONObject) respCtx.getAuthorizationGrantClaimsSet().getClaimsSet().getClaim("claims");
         Assert.assertTrue(isEssential(claims, "id_token", "email"));
         Assert.assertTrue(isEssential(claims, "userinfo", "name"));
     }
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
index 6c997f42..d2fd0c96 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
@@ -18,31 +18,33 @@
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
 import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
 import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
 
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 
 import org.opensaml.storage.ReplayCache;
 import org.opensaml.storage.impl.MemoryStorageService;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
 import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
 import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -62,8 +64,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
 
     URI callback;
 
-    private void init()
-            throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException, DataSealerException {
+    private void init() throws Exception {
         final Instant now = Instant.now();
         acClaims =
                 new AuthorizeCodeClaimsSet.Builder(idGenerator, new ClientID(clientId), "issuer", "userPrin", "subject",
@@ -88,28 +89,27 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         action.initialize();
     }
 
-    public static AuthorizationCode buildAuthorizationCode(String clientId, String issuer, String userPrincipal,
-            String sub, String callbackUrl)
-            throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+            final String userPrincipal, final String sub, final String callbackUrl) throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, null);
     }
 
-    public static AuthorizationCode buildAuthorizationCode(String clientId, String issuer, String userPrincipal,
-            String sub, String callbackUrl, String scope)
-            throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+            final String userPrincipal, final String sub, final String callbackUrl, final String scope)
+                    throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, null, null);
     }
 
-    public static AuthorizationCode buildAuthorizationCode(String clientId, String issuer, String userPrincipal,
-            String sub, String callbackUrl, String codeChallenge, String scope)
-            throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+            final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
+            final String scope) throws Exception {
         return buildAuthorizationCode(clientId, issuer, userPrincipal, sub, callbackUrl, codeChallenge, null, null, null, null);
     }
 
-    public static AuthorizationCode buildAuthorizationCode(String clientId, String issuer, String userPrincipal,
-            String sub, String callbackUrl, String codeChallenge, JSONObject deliveryClaims,
-            JSONObject deliveryClaimsIDToken, JSONObject deliveryClaimsUserInfo, String scope)
-            throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+    static public AuthorizationCode buildAuthorizationCode(final String clientId, final String issuer,
+            final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
+            final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
+            final JSONObject deliveryClaimsUserInfo, final String scope) throws Exception {
         final Instant now = Instant.now();
         final ValidateGrantTest test = new ValidateGrantTest();
         final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder(
@@ -131,77 +131,100 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
         return new AuthorizationCode(acClaims.serialize(test.getDataSealer()));
     }
     
-    
     @Test
-    public void testAuthorizeCodeSuccess()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testAuthorizeCodeSuccess() throws Exception {
         init();
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
-    public void testAuthorizeCodeReplayed()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testAuthorizeCodeReplayed() throws Exception {
         init();
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
     }
 
     @Test
-    public void testRefreshTokenSuccess()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testRefreshTokenSuccess() throws Exception {
         init();
-        TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
         profileRequestCtx.getInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
-    public void testRefreshTokenReplayed()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testRefreshTokenReplayed() throws Exception {
         init();
-        TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
         profileRequestCtx.getInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
         ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
-    public void testMixGrant()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testMixGrant() throws Exception {
         init();
-        TokenRequest req = new TokenRequest(callback, new ClientID(clientId),
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId),
                 new RefreshTokenGrant(new RefreshToken(acClaims.serialize(getDataSealer()))));
         profileRequestCtx.getInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
-    public void testWrongClient()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testWrongClient() throws Exception {
         init();
-        AuthorizationCode code =
+        final AuthorizationCode code =
                 buildAuthorizationCode("clientIdWrong", "issuer", "userPrin", "subject", "http://example.com");
-        TokenRequest req =
+        final TokenRequest req =
                 new TokenRequest(callback, new ClientID(clientId), new AuthorizationCodeGrant(code, callback));
         profileRequestCtx.getInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
     @Test
-    public void testExpired()
-            throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+    public void testExpired() throws Exception {
         init();
         final Instant now = Instant.now();
         rfClaims = new RefreshTokenClaimsSet(acClaims, now, now.minusMillis(10));
-        TokenRequest req = new TokenRequest(callback, new ClientID(clientId),
+        final TokenRequest req = new TokenRequest(callback, new ClientID(clientId),
                 new RefreshTokenGrant(new RefreshToken(rfClaims.serialize(getDataSealer()))));
         profileRequestCtx.getInboundMessageContext().setMessage(req);
         ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
     }
 
+    @Test
+    public void testClientCredentialsSuccess() throws Exception {
+        init();
+        final TokenRequest req = new TokenRequest(callback,
+                new ClientSecretBasic(new ClientID(clientId), new Secret("foo")),
+                new ClientCredentialsGrant());
+        profileRequestCtx.getInboundMessageContext().setMessage(req);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+        final OIDCAuthenticationResponseContext arc =
+                profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
+    }
+    
     @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoRevocationCache() throws NoSuchAlgorithmException, ComponentInitializationException {
+    public void testNoRevocationCache() throws ComponentInitializationException, NoSuchAlgorithmException {
         action = new ValidateGrant(getDataSealer());
         final ReplayCache replayCache = new ReplayCache();
         final MemoryStorageService storageService = new MemoryStorageService();
@@ -213,14 +236,14 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
     }
 
     @Test(expectedExceptions = ComponentInitializationException.class)
-    public void testNoReplayCache() throws NoSuchAlgorithmException, ComponentInitializationException {
+    public void testNoReplayCache() throws ComponentInitializationException, NoSuchAlgorithmException {
         action = new ValidateGrant(getDataSealer());
         action.setRevocationCache(new MockRevocationCache(false, true));
         action.initialize();
     }
 
     @Test(expectedExceptions = ConstraintViolationException.class)
-    public void testNoDataSealer() throws NoSuchAlgorithmException, ComponentInitializationException {
+    public void testNoDataSealer() {
         action = new ValidateGrant(null);
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
index 374a6a00..0ab3d4e3 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScopeTest.java
@@ -19,9 +19,12 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.time.Instant;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
@@ -29,61 +32,202 @@ import org.springframework.webflow.execution.Event;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
 import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest.Method;
 import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
+import com.nimbusds.openid.connect.sdk.UserInfoRequest;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 /** {@link ValidateScope} unit test. */
 public class ValidateScopeTest extends BaseOIDCResponseActionTest {
 
+    /** Action to test. */
     private ValidateScope action;
 
+    /** Client metadata. */
     private OIDCClientMetadata metaData;
 
     @BeforeMethod
     private void init() throws ComponentInitializationException, URISyntaxException {
         action = new ValidateScope();
         action.initialize();
-        OIDCMetadataContext oidcCtx =
+        final OIDCMetadataContext oidcCtx =
                 profileRequestCtx.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class, true);
         metaData = new OIDCClientMetadata();
-        Scope scope = new Scope();
+        final Scope scope = new Scope();
         scope.add(OIDCScopeValue.OPENID);
         scope.add(OIDCScopeValue.EMAIL);
         scope.add(OIDCScopeValue.OFFLINE_ACCESS);
         metaData.setScope(scope);
         metaData.setRedirectionURI(new URI("https://notmatching.org"));
-        OIDCClientInformation information =
+        final OIDCClientInformation information =
                 new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
         oidcCtx.setClientInformation(information);
     }
 
     /**
-     * Test that action filters our non valid scopes.
+     * Test that action filters out non valid scopes on front-channel.
+     * 
+     * @throws ComponentInitializationException
      */
     @Test
-    public void testSuccess() throws ComponentInitializationException {
+    public void testAuthnSuccess() throws ComponentInitializationException {
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
         // input is openid, profile, offline_access and email. profile and offline_access should be filtered out
         // (offline because the request is implicit).
         Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.OPENID));
         Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.EMAIL));
-        Assert.assertTrue(!respCtx.getScope().contains(OIDCScopeValue.OFFLINE_ACCESS));
-        Assert.assertTrue(!respCtx.getScope().contains(OIDCScopeValue.PROFILE));
+        Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.OFFLINE_ACCESS));
+        Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.PROFILE));
     }
 
-    /**
-     * Test that action copes if there are no registered scopes.
-     */
+   /**
+    * Test that action copes if there are no registered scopes.
+    * 
+    * @throws ComponentInitializationException
+    */
     @Test
-    public void testSuccessNoScopes() throws ComponentInitializationException {
+    public void testAuthnNoScopes() throws ComponentInitializationException {
         metaData.setScope(null);
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
         Assert.assertNull(respCtx.getScope());
     }
 
+    /**
+     * Test that action filters out unregistered scopes on back-channel with no prior grant.
+     * 
+     * @throws ComponentInitializationException
+     * @throws URISyntaxException
+     */
+    @Test
+    public void testTokenClientCredentials() throws ComponentInitializationException, URISyntaxException {
+
+        action = new ValidateScope();
+        action.setScopeLookupStrategy(new TokenRequestScopeLookupFunction());
+        action.initialize();
+        
+        final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+                new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+                new ClientCredentialsGrant(),
+                Scope.parse("openid email profile offline_access"));
+        setTokenRequest(req);
+        
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        // input is openid, profile, offline_access and email. profile should be filtered out.
+        Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.OPENID));
+        Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.EMAIL));
+        Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.OFFLINE_ACCESS));
+        Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.PROFILE));
+    }
+    
+   /**
+    * Test that action filters out unregistered scopes on back-channel with prior grant of nothing.
+    * 
+    * @throws ComponentInitializationException
+    * @throws URISyntaxException
+    */
+   @Test
+   public void testTokenNoGrantedScopes() throws ComponentInitializationException, URISyntaxException {
+
+       action = new ValidateScope();
+       action.setScopeLookupStrategy(new TokenRequestScopeLookupFunction());
+       action.initialize();
+       
+       final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+               new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+               new AuthorizationCodeGrant(new AuthorizationCode("foo"), new URI("http://localhost")),
+               Scope.parse("openid email profile offline_access"));
+       setTokenRequest(req);
+       
+       final TokenClaimsSet claims =
+               new AuthorizeCodeClaimsSet.Builder(idGenerator, new ClientID("s6BhdRkqt3"), "issuer", "userPrin",
+                       "subject", Instant.now(), Instant.now(), Instant.now(), new URI("http://localhost"),
+                       new Scope()).setACR(new ACR("0")).build();
+       respCtx.setAuthorizationGrantClaimsSet(claims);
+       
+       final Event event = action.execute(requestCtx);
+       ActionTestingSupport.assertProceedEvent(event);
+       Assert.assertNull(respCtx.getScope());
+   }
+
+   /**
+    * Test that action filters out unregistered scopes on back-channel with prior grants.
+    * 
+    * @throws ComponentInitializationException
+    * @throws URISyntaxException
+    */
+   @Test
+   public void testTokenGrantedScopes() throws ComponentInitializationException, URISyntaxException {
+
+       action = new ValidateScope();
+       action.setScopeLookupStrategy(new TokenRequestScopeLookupFunction());
+       action.initialize();
+       
+       final TokenRequest req = new TokenRequest(new URI("http://localhost"),
+               new ClientSecretBasic(new ClientID("s6BhdRkqt3"), new Secret("foo")),
+               new AuthorizationCodeGrant(new AuthorizationCode("foo"), new URI("http://localhost")),
+               Scope.parse("openid email profile offline_access"));
+       setTokenRequest(req);
+       
+       final TokenClaimsSet claims =
+               new AuthorizeCodeClaimsSet.Builder(idGenerator, new ClientID("s6BhdRkqt3"), "issuer", "userPrin",
+                       "subject", Instant.now(), Instant.now(), Instant.now(), new URI("http://localhost"),
+                       Scope.parse("openid email")).setACR(new ACR("0")).build();
+       respCtx.setAuthorizationGrantClaimsSet(claims);
+       
+       final Event event = action.execute(requestCtx);
+       ActionTestingSupport.assertProceedEvent(event);
+       // input is openid, profile, offline_access and email. profile should be filtered out.
+       Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.OPENID));
+       Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.EMAIL));
+       Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.OFFLINE_ACCESS));
+       Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.PROFILE));
+   }
+
+  /**
+   * Test that action filters out unregistered scopes on UserInfo with prior grants.
+   * 
+   * @throws ComponentInitializationException
+   * @throws URISyntaxException
+   */
+  @Test
+  public void testUserInfoGrantedScopes() throws ComponentInitializationException, URISyntaxException {
+
+      action = new ValidateScope();
+      action.setScopeLookupStrategy(null);
+      action.initialize();
+      
+      final UserInfoRequest req =
+              new UserInfoRequest(new URI("http://localhost"), Method.POST, new BearerAccessToken());
+      setUserInfoRequest(req);
+      
+      final TokenClaimsSet claims =
+              new AuthorizeCodeClaimsSet.Builder(idGenerator, new ClientID("s6BhdRkqt3"), "issuer", "userPrin",
+                      "subject", Instant.now(), Instant.now(), Instant.now(), new URI("http://localhost"),
+                      Scope.parse("openid email")).setACR(new ACR("0")).build();
+      respCtx.setAuthorizationGrantClaimsSet(claims);
+      
+      final Event event = action.execute(requestCtx);
+      ActionTestingSupport.assertProceedEvent(event);
+      // input is openid, profile, offline_access and email. profile should be filtered out.
+      Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.OPENID));
+      Assert.assertTrue(respCtx.getScope().contains(OIDCScopeValue.EMAIL));
+      Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.OFFLINE_ACCESS));
+      Assert.assertFalse(respCtx.getScope().contains(OIDCScopeValue.PROFILE));
+  }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/package-info.java
similarity index 54%
copy from idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
copy to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/package-info.java
index ec39a3cd..32f6334a 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestScopeLookupFunction.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/package-info.java
@@ -15,26 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
-
-import javax.annotation.Nonnull;
-
-import com.nimbusds.oauth2.sdk.Scope;
-
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-
 /**
- * For Token and UserInfo end points.
- * 
- * A function that returns copy of requested scope via a lookup function. This lookup locates scope from token for token
- * request handling. If token claims are not available, null is returned.
+ * Unit tests for profile action implementations related to OIDC.
  */
-public class TokenRequestScopeLookupFunction extends AbstractTokenClaimsLookupFunction<Scope> {
-
-    /** {@inheritDoc} */
-    @Override
-    Scope doLookup(@Nonnull final TokenClaimsSet tokenClaims) {
-        return tokenClaims.getScope();
-    }
-
-}
\ No newline at end of file
+package net.shibboleth.idp.plugin.oidc.op.profile.impl;
\ No newline at end of file

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


More information about the commits mailing list