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

Scott Cantor cantor.2 at osu.edu
Wed Jan 19 22:13:46 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=7fbdafd93b53ab780f9f93ed5c199fe8fa37e51b

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

commit 7fbdafd93b53ab780f9f93ed5c199fe8fa37e51b
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jan 19 17:13:43 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    First working end to end unit tests with fixes to various bits.
---
 .../oidc/op/token/support/TokenClaimsSet.java      |  20 +-
 .../op/oauth2/profile/impl/BuildAccessToken.java   |  38 ++-
 .../impl/SetAccessTokenToResponseContext.java      |   4 +-
 .../op/oauth2/profile/impl/SignAccessToken.java    |   4 +-
 .../impl/FormOutboundTokenResponseMessage.java     |  28 +-
 .../oidc/abstract-api/oidc-abstract-api-beans.xml  |   9 +-
 .../idp/flows/oidc/token/token-beans.xml           |  19 +-
 .../oauth2/profile/impl/BuildAccessTokenTest.java  |   7 +-
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |   6 +-
 .../flow/ClientCredentialsTokenFlowTest.java       | 353 +++++++++++++++++++++
 .../op/profile/flow/IntrospectionFlowTest.java     |   2 +-
 .../oidc/op/profile/flow/RevocationFlowTest.java   |   2 +-
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java |   2 +-
 .../impl/FormOutboundTokenResponseMessageTest.java |  33 +-
 .../src/test/resources/conf/attribute-resolver.xml |  24 ++
 .../src/test/resources/conf/oidc.properties        |   5 +-
 .../src/test/resources/conf/relying-party.xml      |   7 +
 17 files changed, 496 insertions(+), 67 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index 6ee9af7a..ad7f3f40 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -634,37 +634,37 @@ public class TokenClaimsSet {
         @Nonnull @NotEmpty protected String jwtid;
         
         /** Client Id of the rp. */
-        @Nonnull protected ClientID rpId;
+        @Nullable protected ClientID rpId;
 
         /** OP issuer value. */
-        @Nonnull @NotEmpty protected String iss;
+        @Nullable @NotEmpty protected String iss;
 
         /** User Principal of the authenticated user. */
-        @Nonnull @NotEmpty protected String principal;
+        @Nullable @NotEmpty protected String principal;
 
         /** Subject claim value of the authenticated user. */
-        @Nonnull @NotEmpty protected String sub;
+        @Nullable @NotEmpty protected String sub;
 
         /** Authentication context class reference value of the authentication. */
-        @Nonnull protected ACR acr;
+        @Nullable protected ACR acr;
 
         /** Issue time of the claims set. */
-        @Nonnull protected Instant iat;
+        @Nullable protected Instant iat;
 
         /** Expiration time of the claims set. */
-        @Nonnull protected Instant exp;
+        @Nullable protected Instant exp;
 
         /** Not Before time of the claims set. */
         @Nullable protected Instant nbt;
         
         /** Authentication time of the user. */
-        @Nonnull protected Instant authTime;
+        @Nullable protected Instant authTime;
 
         /** Validated redirect URI of the authentication request. */
-        @Nonnull protected URI redirect;
+        @Nullable protected URI redirect;
 
         /** Scope of the token request. */
-        @Nonnull protected Scope reqScope;
+        @Nullable protected Scope reqScope;
         
         /** Audience of token request. */
         @Nonnull @NonnullElements protected List<String> audience;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index baca3a62..1a54f307 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -303,6 +303,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         audienceAttribute = StringSupport.trimOrNull(id);
     }
 
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -321,6 +322,11 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         final String tokenType = accessTokenTypeLookupStrategy.apply(profileRequestContext);
         jwtTokenType = tokenType != null && "JWT".equals(tokenType);
         
+        if (!jwtTokenType && dataSealer == null) {
+            log.error("{} DataSealer required for opaque access tokens", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
+            return false;
+        }
         
         idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
         if (idGenerator == null) {
@@ -353,6 +359,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
         
         return true;
     }
+// Checkstyle: CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
@@ -366,7 +373,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             return;
         }
         
-        final OIDCAuthenticationResponseContext ctx = getOidcResponseContext();
+        final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
 
         final Collection<String> audience = getAudience();
         if (audience == null || audience.isEmpty()) {
@@ -374,14 +381,10 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
             return;
         }
-        ctx.getAudience().addAll(audience);
+        responseCtx.getAudience().addAll(audience);
         log.debug("{} Building access token with audience: {}", getLogPrefix(), audience);
 
-        final Scope scope = getScope(ctx.getScope());
-        if (scope == null) {
-            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
-            return;
-        }
+        final Scope scope = getScope(responseCtx.getScope());
         log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
 
         final Instant now = Instant.now();
@@ -391,16 +394,16 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
                 .setJWTID(idGenerator)
                 .setClientID(clientID)
                 .setIssuer(issuer)
-                .setSubject(ctx.getSubject())
+                .setSubject(responseCtx.getSubject())
                 .setIssuedAt(now)
                 .setExpiresAt(dateExp)
-                .setACR(ctx.getAcr())
-                .setAuthenticationTime(ctx.getAuthTime())
+                .setACR(responseCtx.getAcr())
+                .setAuthenticationTime(responseCtx.getAuthTime())
                 .setScope(scope)
                 .setAudience(audience);
         
-        if (ctx.getAccessTokenClaimSet() != null) {
-            builder.setCustomClaims(ctx.getAccessTokenClaimSet().toJSONObject());
+        if (responseCtx.getAccessTokenClaimSet() != null) {
+            builder.setCustomClaims(responseCtx.getAccessTokenClaimSet().toJSONObject());
         }
         
         final AccessTokenClaimsSet claimsSet = builder.build();
@@ -428,7 +431,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
      * 
      * @return derived {@link Scope} to use or null
      */
-    @Nullable private Scope getScope(@Nullable final Scope validatedScope) {
+    @Nonnull private Scope getScope(@Nullable final Scope validatedScope) {
         
         if (scopeAttribute != null) {
             final IdPAttribute source = (useUnfilteredAttributes ? attributeCtx.getUnfilteredIdPAttributes()
@@ -441,15 +444,10 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
                             .map(StringAttributeValue::getValue)
                             .collect(Collectors.toUnmodifiableList()));
             }
-            
-            log.warn("{} No source attribute {} available to produce scope claim", getLogPrefix(), scopeAttribute);
-            return null;
         }
         
-        if (validatedScope != null) {
-            log.debug("{} Using originally requested/validated scope", getLogPrefix());
-        }
-        return validatedScope;
+        log.debug("{} Using originally requested/validated scope", getLogPrefix());
+        return validatedScope != null ? validatedScope : new Scope();
     }
     
     @Nullable @NonnullElements private Collection<String> getAudience() {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
index a18660b8..a1ba2a4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAccessTokenToResponseContext.java
@@ -24,7 +24,7 @@ import javax.annotation.Nullable;
 
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -59,7 +59,7 @@ public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction
         // PRC -> inbound message context -> OIDC response context -> ATC
         accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
-                        new InboundMessageContextLookup()));
+                        new OutboundMessageContextLookup()));
     }
 
     /**
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java
index ddbebd8a..3f2c0a23 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SignAccessToken.java
@@ -27,7 +27,7 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.nimbusds.jwt.JWTClaimsSet;
@@ -67,7 +67,7 @@ public class SignAccessToken extends AbstractSignJWTAction {
         // PRC -> inbound message context -> OIDC response context -> ATC
         accessTokenContextLookupStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
                 new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
-                        new InboundMessageContextLookup()));
+                        new OutboundMessageContextLookup()));
     }
 
     /**
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
index 6cdff686..7be57cc0 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessage.java
@@ -26,14 +26,18 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
 import com.nimbusds.oauth2.sdk.TokenResponse;
 import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.Tokens;
 import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
 
 /**
  * Action that forms outbound message based on token request and response context. Formed message is set to
  * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * 
+ * <p>This is both OIDC and OAuth aware, based on the presence of an ID token in the context.</p>
  */
 public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseAction {
 
@@ -43,7 +47,7 @@ public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseA
     /** access token for response. */
     @Nullable private AccessToken accessToken;
 
-    /** id token for response. */
+    /** ID token for response. */
     @Nullable private JWT idToken;
 
     /** {@inheritDoc} */
@@ -51,29 +55,33 @@ public class FormOutboundTokenResponseMessage extends AbstractOIDCTokenResponseA
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
         if (!super.doPreExecute(profileRequestContext)) {
-            log.error("{} pre-execute failed", getLogPrefix());
             return false;
         }
+        
         accessToken = getOidcResponseContext().getAccessToken();
         if (accessToken == null) {
-            log.error("{} unable to provide access token (required)", getLogPrefix());
+            log.error("{} unable to provide required access token", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
+        
         idToken = getOidcResponseContext().getProcessedToken();
-        if (idToken == null) {
-            log.error("{} unable to provide id token (required)", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
         return true;
     }
 
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final TokenResponse resp =
-                new OIDCTokenResponse(new OIDCTokens(idToken, accessToken, getOidcResponseContext().getRefreshToken()));
+        
+        final TokenResponse resp;
+        if (idToken != null) {
+            resp = new OIDCTokenResponse(
+                    new OIDCTokens(idToken, accessToken, getOidcResponseContext().getRefreshToken()));
+        } else {
+            // No refresh tokens likely here, but just in case.
+            resp = new AccessTokenResponse(new Tokens(accessToken, getOidcResponseContext().getRefreshToken()));
+        }
         profileRequestContext.getOutboundMessageContext().setMessage(resp);
     }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
index 78c7327f..24e4a783 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-beans.xml
@@ -31,12 +31,13 @@
             value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
         <entry key="#{T(net.shibboleth.idp.authn.AuthnEventIds).NO_POTENTIAL_FLOW}"
             value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_CLIENT}" />
-
         <entry key="#{T(net.shibboleth.idp.profile.IdPEventIds).INVALID_PROFILE_CONFIG}"
-            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />            
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
         <entry key="#{T(org.opensaml.profile.action.EventIds).ACCESS_DENIED}"
-            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />            
-
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).ACCESS_DENIED}" />
+            
+        <entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_GRANT_TYPE}"
+            value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).UNAUTHORIZED_CLIENT}" />
         <entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_GRANT}"
             value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_GRANT}" />
         <entry key="#{T(net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds).INVALID_REDIRECT_URI}"
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 984ca005..193c182b 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
@@ -66,14 +66,14 @@
     <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" />
+                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" />
+        p:grantTypes="#{T(com.nimbusds.oauth2.sdk.GrantType).AUTHORIZATION_CODE}" />
 
     <!-- Traditional third-party grant handling. -->
 
@@ -268,18 +268,25 @@
         p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
 
     <bean id="shibboleth.AccessTokenClaimsSetLookupStrategy"
-        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction" />
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction"
+        p:autoCreate="true" />
 
     <bean id="BuildAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
-        p:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
         p:useUnfilteredAttributes="%{idp.oauth.accessToken.useUnfilteredAttributes:true}"
         p:scopeAttribute="#{'%{idp.oauth.accessToken.scopeAttribute:scope}'.trim()}"
         p:audienceAttribute="#{'%{idp.oauth.accessToken.audienceAttribute:audience}'.trim()}" />
         
     <bean id="SignAccessToken"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype" />
-
+            class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype">
+        <property name="securityParametersLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose"
+                c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    </bean>
+    
     <bean id="SetOAuthAccessTokenToResponseContext"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
         scope="prototype" />
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index 4faef1cf..b345a871 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -121,6 +121,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
         action.setClientIDLookupStrategy(FunctionSupport.constant(null));
         action.setScopeAttribute(null);
         action.setAudienceAttribute(null);
+        action.setDataSealer(getDataSealer());
         action.initialize();
         
         final Event event = action.execute(requestCtx);
@@ -194,7 +195,9 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
         attributeCtx.setUnfilteredIdPAttributes(Collections.singletonList(new IdPAttribute("foo")));
         
         final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.ACCESS_DENIED);
+        ActionTestingSupport.assertProceedEvent(event);
+        verifyClaims(respCtx.getSubcontext(AccessTokenContext.class), new Scope(),
+                Collections.singletonList("https://rp.example.org"));
     }
     
     /**
@@ -376,7 +379,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
     }
 
     /**
-     * Verify opaque access token's claims.
+     * Verify access token's claims.
      * 
      * @param ctx access token context
      * @param scope scope to check for
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index c159f32d..5f4937fd 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -23,9 +23,9 @@ import java.net.URISyntaxException;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
@@ -186,8 +186,8 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
             final ClientAuthenticationMethod tokenEndpointMethod, final JWSAlgorithm userInfoSigAlg,
             final RSAPublicKey publicKey, final String... redirectUri) throws IOException {
         final OIDCClientMetadata metadata = new OIDCClientMetadata();
-        metadata.setGrantTypes(new HashSet<GrantType>(Arrays.asList(GrantType.AUTHORIZATION_CODE,
-                GrantType.REFRESH_TOKEN)));
+        metadata.setGrantTypes(new HashSet<GrantType>(List.of(GrantType.AUTHORIZATION_CODE,
+                GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS)));
         final HashSet<URI> uris = new HashSet<>();
         for (final String uri : redirectUri) {
             try {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
new file mode 100644
index 00000000..c12233cb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/ClientCredentialsTokenFlowTest.java
@@ -0,0 +1,353 @@
+/*
+ * 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.profile.flow;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+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.pkce.CodeChallenge;
+import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod;
+import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.minidev.json.JSONObject;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+/**
+ * Unit tests for the token flow when using the client_credentials grant type.
+ */
+public class ClientCredentialsTokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
+    
+    public static final String FLOW_ID = "oidc/token";
+    
+    private final Scope scope = Scope.parse("profile email");
+    
+    private final String resource = "https://rp.example.org";
+    
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+    
+    public ClientCredentialsTokenFlowTest() {
+        super(FLOW_ID);
+    }
+    
+    @AfterMethod
+    public void removeMetadata() throws IOException {
+        removeMetadata(storageService, clientId);
+    }
+
+    @Test
+    public void testUntrustedClient() throws IOException, ParseException {
+        setHttpFormRequest("POST", createRequestParameters(clientId + "2", scope, resource));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        // This is invalid_request because Nimbus bails out too early if client creds are missing.
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidClientSecret() throws ParseException, IOException {
+        setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret + "2");
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_CLIENT_CODE);
+    }
+
+    @Test
+    public void testInvalidGrantType() throws ParseException, IOException {
+        setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+        setBasicAuth(clientIdSaml, clientSecretSaml);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
+    }
+
+    @Test
+    public void testNoScopes() throws Exception {
+        setHttpFormRequest("POST", createRequestParameters(clientId, scope, resource));
+        storeMetadata(storageService, clientId, clientSecret, null);
+        setBasicAuth(clientId, clientSecret);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getBearerAccessToken());
+        Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
+        Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
+        verifyClaims(null, response.getTokens().getBearerAccessToken(), new Scope(),
+                Collections.singletonList(resource));
+    }
+
+    @Test
+    public void testNoScopesJWT() throws Exception {
+        setHttpFormRequest("POST", createRequestParameters(clientId + "JWT", scope, resource));
+        storeMetadata(storageService, clientId + "JWT", clientSecret, null);
+        setBasicAuth(clientId + "JWT", clientSecret);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getBearerAccessToken());
+        Assert.assertEquals(response.getTokens().getBearerAccessToken().getLifetime(), 600);
+        Assert.assertNull(response.getTokens().getBearerAccessToken().getScope());
+        verifyClaims("JWT", response.getTokens().getBearerAccessToken(), new Scope(),
+                Collections.singletonList(resource));
+    }
+    
+    protected void initializeGrantAndRequest(final String clientId, final Map<String, String> requestParameters)
+            throws IOException {
+        setHttpFormRequest("POST", requestParameters);
+        storeMetadata(storageService, clientId, clientSecret, scope);
+        setBasicAuth(clientId, clientSecret);
+    }
+    
+    /*
+    @Test
+    public void testValidGrant() throws Exception {
+        initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+    }
+
+    @Test
+    public void testValidGrantWithRequestedScope() throws Exception {
+        final Map<String,String> params = createRequestParameters(clientId, scope, resource);
+        params.put("scope", "openid profile");
+        initializeGrantAndRequest(clientId, params);
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        
+        final ValidateGrantTest test = new ValidateGrantTest();
+        final AccessTokenClaimsSet token =
+                AccessTokenClaimsSet.parse(response.getTokens().getAccessToken().getValue(), test.getDataSealer());
+        Assert.assertTrue(token.getScope().contains("openid"));
+        Assert.assertTrue(token.getScope().contains("profile"));
+        Assert.assertFalse(token.getScope().contains("email"));
+    }
+
+    @Test
+    public void testValidSecretJWT() throws Exception {
+        final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, JWSAlgorithm.HS256);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+    }
+
+    @Test
+    public void testValidSecretJWTNoAlg() throws Exception {
+        final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(clientAuth, null);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+    }
+    
+    @Test
+    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 Exception {
+        final String claimName = "name";
+        final String claimValue = "John Doe";
+        final JSONObject claimsUI = new JSONObject();
+        claimsUI.put(claimName, claimValue);
+        initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+        Assert.assertNotNull(claimsSet);
+        final ClaimsSet dlClaimsSet = claimsSet.getUserinfoDeliveryClaims();
+        Assert.assertNotNull(dlClaimsSet);
+        Assert.assertEquals(dlClaimsSet.getClaim(claimName), claimValue);
+        Assert.assertNull(claimsSet.getDeliveryClaims().getClaim(claimName));
+        Assert.assertNull(claimsSet.getIDTokenDeliveryClaims());
+    }
+
+    @Test
+    public void testValidGrantWrappedClaims() throws Exception {
+        final String claimName = "name";
+        final String claimValue = "John Doe";
+        final JSONObject claims = new JSONObject();
+        claims.put(claimName, claimValue);
+        initializeGrantAndRequest(clientId, createRequestParameters(clientId, scope, resource));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        final AccessTokenClaimsSet claimsSet = unwrapAccessToken(response);
+        Assert.assertNotNull(claimsSet);
+        final ClaimsSet dlClaimsSet = claimsSet.getDeliveryClaims();
+        Assert.assertNotNull(dlClaimsSet);
+        Assert.assertEquals(dlClaimsSet.getClaim(claimName), claimValue);
+        Assert.assertNull(claimsSet.getUserinfoDeliveryClaims().getClaim(claimName));
+        Assert.assertNull(claimsSet.getIDTokenDeliveryClaims());
+    }
+    */
+    
+    private AccessTokenClaimsSet unwrapAccessToken(final AccessTokenResponse tokenResponse) {
+        final AccessToken accessToken = tokenResponse.getTokens().getAccessToken();
+        Assert.assertNotNull(accessToken);
+        try {
+            return AccessTokenClaimsSet.parse(accessToken.getValue(), 
+                    BaseOIDCResponseActionTest.initializeDataSealer());
+        } catch (final NoSuchAlgorithmException | java.text.ParseException | DataSealerException
+                | ComponentInitializationException e) {
+            return null;
+        }
+    }
+    
+    protected FlowExecutionResult launchWithJwtAuthentication(final JWTAuthentication authnMethod, final JWSAlgorithm algorithm)
+            throws Exception {
+        storeMetadata(storageService, clientId, clientSecret, scope, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        final Map<String, String> requestParameters = createRequestParameters(clientId, scope, resource);
+        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 Exception {
+        if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(method)) {
+            storeMetadata(storageService, clientId, clientSecret, scope, algorithm, method);
+        } else {
+            storeMetadata(storageService, clientId, null, scope, algorithm, method, null, rsaPublicKey);
+        }
+        final Map<String, String> requestParameters = createRequestParameters(clientId, scope, resource);
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+    }
+
+    protected Map<String, String> createRequestParameters(final String clientId, final Scope scope, final String resource) {
+        final Map<String, String> parameters = new HashMap<>();
+        addNonNullValue(parameters, "grant_type", GrantType.CLIENT_CREDENTIALS.getValue());
+        if (scope != null) {
+            addNonNullValue(parameters, "scope", scope.toString());
+        }
+        if (resource != null) {
+            addNonNullValue(parameters, "resource", resource);
+        }
+        return parameters;
+    }
+    
+    private void addNonNullValue(final Map<String, String> map, final String key, final String value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+
+    protected Pair<String, String> getErrorDetaisForJWTValidation() {
+        return new Pair<>("invalid_client", "Client authentication failed");
+    }
+
+   /**
+    * Verify access token's claims.
+    * 
+    * @param type token type/format
+    * @param token access token
+    * @param s scope to check for
+    * @param audiences audiences to check for
+    * 
+    * @throws ComponentInitializationException 
+    * @throws DataSealerException 
+    * @throws ParseException 
+    * @throws NoSuchAlgorithmException 
+    */
+   private void verifyClaims(@Nullable final String type, @Nonnull final AccessToken token, @Nonnull final Scope s,
+           @Nonnull @NonnullElements final Collection<String> audiences)
+           throws NoSuchAlgorithmException, ParseException, DataSealerException, ComponentInitializationException {
+       
+       if (type == null) {
+           final AccessTokenClaimsSet at = AccessTokenClaimsSet.parse(token.getValue(),
+                   BaseOIDCResponseActionTest.initializeDataSealer());
+           assertNotNull(at);
+           assertEquals(at.getACR(), null);
+           assertEquals(at.getAudience(), audiences);
+           assertTrue(at.getAuthenticationTime().isBefore(Instant.now()));
+           assertEquals(at.getClientID().getValue(), clientId);
+           assertEquals(at.getExp(), at.getIssuedAt().plusSeconds(600));
+           assertEquals(at.getIssuer(), "https://op.example.org");
+           assertTrue(at.getIssuedAt().isBefore(Instant.now()));
+           assertEquals(at.getScope(), s);
+           assertEquals(at.getSubject(), clientId);
+       } else if ("JWT".equals(type)) {
+           final JWTClaimsSet claims = SignedJWT.parse(token.getValue()).getJWTClaimsSet();
+           assertNotNull(claims);
+           assertEquals(claims.getClaim(TokenClaimsSet.KEY_ACR), null);
+           assertEquals(claims.getAudience(), audiences);
+           assertTrue(claims.getDateClaim(TokenClaimsSet.KEY_AUTH_TIME).toInstant().isBefore(Instant.now()));
+           assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_CLIENTID), clientId + type);
+           assertEquals(claims.getExpirationTime().toInstant(), claims.getIssueTime().toInstant().plusSeconds(600));
+           assertEquals(claims.getIssuer(), "https://op.example.org");
+           assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
+           assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), s.toString());
+           assertEquals(claims.getSubject(), clientId + type);
+       } else {
+           throw new RuntimeException("Bad token format");
+       }
+   }
+   
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index fe06694f..2ca1a8fc 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -81,7 +81,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
         setHttpFormRequest("POST", Collections.singletonMap("token",
                 super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token")));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+        assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
     }
     
     @Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
index f2658e05..eb84b277 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
@@ -80,7 +80,7 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
         setHttpFormRequest("POST", Collections.singletonMap("token", super.buildToken(clientId, "sub", 
                 Scope.parse("openid")).toJSONObject().getAsString("access_token")));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+        assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
     }
 
     @Test
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 2e2127d9..812a4f3d 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
@@ -108,7 +108,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     public void testUntrustedClient() throws IOException, ParseException {
         setHttpFormRequest("POST", createRequestParameters(null, "authorization_code", "mockCode", clientId + "2"));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        assertErrorCode(result, OAuth2Error.ACCESS_DENIED_CODE);
+        assertErrorCode(result, OAuth2Error.UNAUTHORIZED_CLIENT_CODE);
     }
     
     @Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
index af3ec28f..acc15a8d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundTokenResponseMessageTest.java
@@ -21,7 +21,6 @@ import java.net.URISyntaxException;
 import java.time.Duration;
 import java.time.Instant;
 
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundTokenResponseMessage;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import org.opensaml.messaging.context.MessageContext;
@@ -34,11 +33,22 @@ import com.nimbusds.jose.JOSEException;
 import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.TokenResponse;
 
+// Checkstyle: ThrowsCount OFF
+
 /** {@link FormOutboundTokenResponseMessage} unit test. */
 public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseActionTest {
 
+    /** Action to test. */
     private FormOutboundTokenResponseMessage action;
 
+    /**
+     * Init method.
+     * 
+     * @throws ComponentInitializationException
+     * @throws URISyntaxException
+     * @throws ParseException
+     * @throws JOSEException
+     */
     @BeforeMethod
     public void init() throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
         action = new FormOutboundTokenResponseMessage();
@@ -50,9 +60,14 @@ public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseAction
 
     /**
      * Test that action is able to form success message.
+     * 
+     * @throws ComponentInitializationException 
+     * @throws URISyntaxException 
+     * @throws ParseException 
+     * @throws JOSEException 
      */
     @Test
-    public void testSuccessMessage()
+    public void testOIDCSuccess()
             throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
         final Event event = action.execute(requestCtx);
         ActionTestingSupport.assertProceedEvent(event);
@@ -61,17 +76,27 @@ public class FormOutboundTokenResponseMessageTest extends BaseOIDCResponseAction
 
     /**
      * Test that action fails if there is no id token.
+     * 
+     * @throws ComponentInitializationException 
+     * @throws URISyntaxException 
+     * @throws ParseException 
+     * @throws JOSEException 
      */
     @Test
-    public void testFailNoIdToken()
+    public void testOAuthSuccess()
             throws ComponentInitializationException, URISyntaxException, ParseException, JOSEException {
         respCtx.setProcessedToken(null);
         final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
+        Assert.assertTrue(((MessageContext) respCtx.getParent()).getMessage() instanceof TokenResponse);
     }
 
     /**
      * Test that action fails if there is no access token.
+     * 
+     * @throws ComponentInitializationException 
+     * @throws URISyntaxException 
+     * @throws ParseException 
+     * @throws JOSEException 
      */
     @Test
     public void testFailNoAccessToken()
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
index 197a3f27..131ab679 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/attribute-resolver.xml
@@ -83,6 +83,30 @@
         <AttributeEncoder xsi:type="oidc:OIDCString" name="sub" />
     </AttributeDefinition>
 
+    <!-- Used to drive client_credentials grant handling. -->
+    
+    <AttributeDefinition id="requestedScope" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
+        <Script>
+          <![CDATA[
+             requestedScope.getValues().addAll(
+               resolutionContext.getSubcontext(
+                 "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getScope()
+               );
+          ]]>
+        </Script>
+    </AttributeDefinition>
+
+    <AttributeDefinition id="requestedAudience" xsi:type="ScriptedAttribute" resolutionPhases="oidc/token">
+        <Script>
+          <![CDATA[
+             requestedAudience.getValues().addAll(
+               resolutionContext.getSubcontext(
+                 "net.shibboleth.idp.plugin.oidc.op.profile.context.OAuthAttributeResolutionContext").getResources()
+               );
+          ]]>
+        </Script>
+    </AttributeDefinition>
+
 
     <!-- ========================================== -->
     <!--      Data Connectors                       -->
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
index 0d00d608..557b62fd 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/test/resources/conf/oidc.properties
@@ -6,4 +6,7 @@ idp.oidc.subject.sourceAttribute = uid
 idp.oidc.subject.salt = isfd07fsddfs70sdf9d99s8
 idp.oidc.discovery.template = src/test/resources/conf/openid-configuration.json
 
-idp.oidc.dynreg.defaultMetadataPolicyFile = src/test/resources/conf/metadata-policy1.json
\ No newline at end of file
+idp.oidc.dynreg.defaultMetadataPolicyFile = src/test/resources/conf/metadata-policy1.json
+
+idp.oauth.accessToken.scopeAttribute = requestedScope
+idp.oauth.accessToken.audienceAttribute = requestedAudience
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
index 5eb74533..b25d62e5 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
@@ -78,6 +78,13 @@
                  </list>
             </property>
         </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdJWT">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Token.MDDriven" p:accessTokenType="JWT" />
+                 </list>
+            </property>
+        </bean>
     </util:list>
 
 </beans>

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


More information about the commits mailing list