[java-idp-oidc] branch main updated: JOIDC-65 JWT client authentication support is incomplete

Scott Cantor cantor.2 at osu.edu
Thu Dec 30 20:27:22 UTC 2021


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=10603b0153101ffb4defbfceb75cbdc55d9afeb0

The following commit(s) were added to refs/heads/main by this push:
     new 10603b01 JOIDC-65 JWT client authentication support is incomplete
10603b01 is described below

commit 10603b0153101ffb4defbfceb75cbdc55d9afeb0
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Dec 30 15:27:20 2021 -0500

    JOIDC-65 JWT client authentication support is incomplete
    
    https://shibboleth.atlassian.net/browse/JOIDC-65
    
    Consolidated patch redone for new login flow.
---
 .../oidc/op/authn/impl/JWTCredentialValidator.java |  67 ++++-
 ...entIDFromOIDCMetadataContextLookupFunction.java |  78 +++++
 .../authn/OAuth2Client/OAuth2Client-beans.xml      |  53 +++-
 .../op/authn/impl/JWTCredentialValidatorTest.java  | 208 +++++++++++--
 .../AbstractOidcClientAuthenticationFlowTest.java  | 321 +++++++++++++++++++++
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |  25 +-
 .../op/profile/flow/IntrospectionFlowTest.java     |  36 ++-
 .../oidc/op/profile/flow/RevocationFlowTest.java   |  30 +-
 .../plugin/oidc/op/profile/flow/TokenFlowTest.java |  44 +--
 9 files changed, 817 insertions(+), 45 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
index dbfc8aec..47fe1e68 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidator.java
@@ -17,6 +17,7 @@
 
 package net.shibboleth.idp.plugin.oidc.op.authn.impl;
 
+import java.text.ParseException;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -30,7 +31,10 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
 import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.jwt.claims.JWTClaimsValidation;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
 import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -43,6 +47,7 @@ import org.opensaml.xmlsec.context.SecurityParametersContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
@@ -68,6 +73,9 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
     /** Strategy used to locate the {@link SecurityParametersContext} to use for verification. */
     @Nonnull private Function<ProfileRequestContext,SecurityParametersContext> securityParametersLookupStrategy;
 
+    /** The claims validation to be applied for validating the incoming JWTs. */
+    @NonnullAfterInit private JWTClaimsValidation claimsValidation;
+    
     /** Whether to save the JWT in the Java Subject's public credentials. */
     private boolean saveTokenToCredentialSet;
     
@@ -107,6 +115,16 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
                 Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
     }
 
+    /**
+     * Set the claims validator used for validating the incoming JWTs.
+     * 
+     * @param validation claims validator
+     */
+    public void setClaimsValidation(@Nonnull final JWTClaimsValidation validation) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        claimsValidation = Constraint.isNotNull(validation, "JWTClaimsValidation cannot be null");
+    }
     
     /**
      * Set whether to save the JWT in the Java Subject's public credentials.
@@ -126,8 +144,12 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();
 
+        if (claimsValidation == null) {
+            throw new ComponentInitializationException("ClaimsValidator cannot be null");
+        }
     }
     
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
     @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
@@ -176,11 +198,54 @@ public class JWTCredentialValidator extends AbstractCredentialValidator {
             throw e;
         }
 
+        try {
+            validateJWTClaims(profileRequestContext, jwtAuth.getClientAssertion(), clientAuth.getClientID());
+        } catch (final Exception e) {
+            log.info("{} Login by '{}' failed", getLogPrefix(), clientAuth.getClientID());
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+            }
+            throw e;
+        }
+        
         log.info("{} Login by '{}' succeeded", getLogPrefix(), clientAuth.getClientID());
         
         return populateSubject(clientAuth.getClientID(), jwtAuth.getClientAssertion());
-    }
+    }    
+// Checkstyle: CyclomaticComplexity ON
 
+    /**
+     * Validates the contents of the given JWT against the requirements set in the OIDC core specification section 9.
+     * 
+     * @param jwt JWT to be validated
+     * @param clientId client ID from which the JWT is coming from
+     * @param profileRequestContext profile request context
+     * 
+     * @throws ParseException if unable to parse the claim set
+     * @throws JWTValidationException if the claims fail to validate
+     */
+    protected void validateJWTClaims(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final SignedJWT jwt, @Nonnull final ClientID clientId)
+                    throws ParseException, JWTValidationException {
+        
+        final JWTClaimsSet claimsSet;
+        
+        try {
+            claimsSet = jwt.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.warn("{} Could not parse the JWT from client '{}' into claims set", getLogPrefix(), clientId);
+            throw e;
+        }
+        
+        try {
+            claimsValidation.validate(claimsSet, profileRequestContext);
+        } catch (final JWTValidationException e) {
+            log.warn("{} JWT validation failed for client '{}': {}", getLogPrefix(), clientId, e.getMessage());
+            throw e;
+        }
+    } 
+    
    /**
     * Builds a subject with "standard" content from the validation.
     *
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java
new file mode 100644
index 00000000..f06e2db2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/ClientIDFromOIDCMetadataContextLookupFunction.java
@@ -0,0 +1,78 @@
+/*
+ * 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.logic;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A function that returns client_id via client information stored in {@link OIDCMetadataContext}.
+ */
+public class ClientIDFromOIDCMetadataContextLookupFunction
+        implements BiFunction<ProfileRequestContext, JWTClaimsSet, String> {
+
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public ClientIDFromOIDCMetadataContextLookupFunction() {
+        oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                new InboundMessageContextLookup());
+    }
+
+    /**
+     * Set the strategy used to return the {@link OIDCMetadataContext}.
+     * 
+     * @param strategy The lookup strategy.
+     */
+    public void setOIDCMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
+        oidcMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nonnull final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
+        final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(prc);
+        if (oidcMetadataContext == null) {
+            return null;
+        }
+        final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+        if (clientInformation == null || clientInformation.getID() == null) {
+            return null;
+        }
+        return clientInformation.getID().toString();
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
index 3ebbed75..b046a4a8 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OAuth2Client/OAuth2Client-beans.xml
@@ -60,7 +60,58 @@
         <bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.OIDCClientInfoCredentialValidator"
             p:id="oauth2-clientinfo" />
         <bean class="net.shibboleth.idp.plugin.oidc.op.authn.impl.JWTCredentialValidator"
-            p:id="oauth2-jwt" />
+            p:id="oauth2-jwt"
+            p:claimsValidation-ref="#{'%{idp.auth.OAuth2Client.JWTValidation:DefaultJWTClaimsValidation}'.trim()}" />
+    </util:list>
+    
+    <!-- JWT validation wiring. -->
+
+    <bean id="DefaultJWTClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidation"
+        p:claimValidators-ref="ClaimsValidators" />
+    
+    <bean id="ClientIDFromOIDCMetadataContextLookupFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <bean id="ExpiryClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="IssuedAtClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+        p:messageLifetime="%{idp.policy.messageLifetime:PT1M}"
+        p:requiredRule="false" />
+
+    <bean id="IssuerClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="iss" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <bean id="SubjectClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="sub" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <bean id="AudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+        <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.getRequestURL().toString()"
+                p:customObject-ref="shibboleth.HttpServletRequest" />
+        </property>
+    </bean>
+
+    <bean id="JWTIdentifierClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+        p:replayCache-ref="shibboleth.ReplayCache" />
+
+    <util:list id="ClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="IssuedAtClaimsValidator" />
+        <ref bean="IssuerClaimsValidator" />
+        <ref bean="SubjectClaimsValidator" />
+        <ref bean="AudienceClaimsValidator" />
+        <ref bean="JWTIdentifierClaimsValidator" />
     </util:list>
     
     <!-- Validator parent beans -->
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
index 6a6416fa..541617e5 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/authn/impl/JWTCredentialValidatorTest.java
@@ -24,12 +24,17 @@ import java.security.KeyPairGenerator;
 import java.security.NoSuchAlgorithmException;
 import java.security.interfaces.RSAPrivateKey;
 import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 
 import javax.crypto.spec.SecretKeySpec;
+import javax.servlet.http.HttpServletRequest;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.storage.ReplayCache;
+import org.opensaml.storage.impl.MemoryStorageService;
 import org.opensaml.xmlsec.context.SecurityParametersContext;
 import org.springframework.webflow.execution.Event;
 import org.testng.Assert;
@@ -39,7 +44,12 @@ import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
 import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
 import com.nimbusds.oauth2.sdk.AuthorizationGrant;
@@ -60,9 +70,17 @@ import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
 import net.shibboleth.idp.plugin.oidc.op.authn.context.OAuth2ClientAuthenticationContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.ClientIDFromOIDCMetadataContextLookupFunction;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.jwt.claims.JWTClaimsValidation;
 import net.shibboleth.oidc.security.credential.BasicJWKCredential;
 import net.shibboleth.oidc.security.impl.OIDCSignatureValidationParameters;
+import net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidation;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
 /**
@@ -91,16 +109,28 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
     }
     
     @BeforeMethod
-    public void setUo() throws URISyntaxException, ComponentInitializationException {
+    public void setUp() throws ComponentInitializationException {
         super.setUp();
         
         clientId = new ClientID("mockId");
         clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
-        endpointUri = new URI("https://mock.example.org/");
+        try {
+            endpointUri = new URI("http://localhost");
+        } catch (final URISyntaxException e) {
+            throw new ComponentInitializationException(e);
+        }
 
+        ReplayCache replayCache = new ReplayCache();
+        MemoryStorageService storageService = new MemoryStorageService();
+        storageService.setId("mockId");
+        storageService.initialize();
+        replayCache.setStorage(storageService);
+        
         validator = new JWTCredentialValidator();
         validator.setId("test");
         validator.setSecurityParametersLookupStrategy(new ChildContextLookup<>(SecurityParametersContext.class));
+        validator.setClaimsValidation(
+                constructClaimsValidation((HttpServletRequest) src.getExternalContext().getNativeRequest(), replayCache));
         validator.initialize();
         
         action = new ValidateCredentials();
@@ -137,7 +167,8 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
             if (sameSecret) {
                 credential.setSecretKey(new SecretKeySpec(clientSecret.getValueBytes(), "NONE"));
             } else {
-                credential.setSecretKey(new SecretKeySpec("secret1234567890secret1234567890secretWRONG".getBytes(), "NONE"));
+                credential.setSecretKey(
+                        new SecretKeySpec("secret1234567890secret1234567890secretWRONG".getBytes(), "NONE"));
             }
         }
         
@@ -154,14 +185,14 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
         prc.getInboundMessageContext().addSubcontext(oidcContext);
     }
     
-    protected void initializeTokenRequest(final ClientAuthenticationMethod method, final boolean success)
-            throws JOSEException, NoSuchAlgorithmException {
+    protected void initializeTokenRequest(final ClientAuthenticationMethod method, final SignedJWT jwt,
+            final boolean sameSecret) throws JOSEException, NoSuchAlgorithmException {
 
         final ClientAuthentication clientAuth;
         if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
-            clientAuth = new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret);
+            clientAuth = new ClientSecretJWT(jwt);
         } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
-            clientAuth = new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, rsaPrivateKey, null, null);
+            clientAuth = new PrivateKeyJWT(jwt);
         } else {
             clientAuth = null;
         }
@@ -171,12 +202,107 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
         ac.getSubcontext(OAuth2ClientAuthenticationContext.class, true).setClientAuthentication(clientAuth);
         
         final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
-        completeSetup(new TokenRequest(null, clientAuth, authzGrant), method, success);
+        completeSetup(new TokenRequest(null, clientAuth, authzGrant), method, sameSecret);
+    }
+    
+    protected JWTClaimsValidation constructClaimsValidation(final HttpServletRequest httpRequest,
+            final ReplayCache replayCache) {
+        final ChainingJWTClaimsValidation claimsValidation = new ChainingJWTClaimsValidation();
+        final ExpiryClaimsValidator expValidator = new ExpiryClaimsValidator();
+        final IssuedAtClaimsValidator iatValidator = new IssuedAtClaimsValidator();
+        iatValidator.setRequiredRule(false);
+        final ExactMatchClaimsValidator issValidator = new ExactMatchClaimsValidator();
+        issValidator.setClaimName("iss");
+        issValidator.setValueToMatchLookupStrategy(new ClientIDFromOIDCMetadataContextLookupFunction());
+        final ExactMatchClaimsValidator subValidator = new ExactMatchClaimsValidator();
+        subValidator.setClaimName("sub");
+        subValidator.setValueToMatchLookupStrategy(new ClientIDFromOIDCMetadataContextLookupFunction());
+        final AudienceClaimsValidator audValidator = new AudienceClaimsValidator();
+        audValidator.setAudienceLookupStrategy((prc, claims) -> httpRequest.getRequestURL().toString());
+        final JWTIdentifierClaimsValidator jitValidator = new JWTIdentifierClaimsValidator();
+        jitValidator.setReplayCache(replayCache);
+        claimsValidation.setClaimValidators(List.of(expValidator, iatValidator, issValidator, subValidator,
+                audValidator, jitValidator));
+        return claimsValidation;
+    }
+    
+    protected void testFailingJwtAuth(final ClientAuthenticationMethod method, final SignedJWT jwt,
+            final boolean replay, final boolean sameSecret) throws Exception {
+        initializeTokenRequest(method, jwt, sameSecret);
+        
+        Event event = action.execute(src);
+        if (replay) {
+            ActionTestingSupport.assertProceedEvent(event);
+            event = action.execute(src);
+        }
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+    protected JWTClaimsSet claimsSetWithIatInTheFuture() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId.toString())
+                .issuer(clientId.toString())
+                .audience(endpointUri.toString())
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .issueTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetWithExpInThePast() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId.toString())
+                .issuer(clientId.toString())
+                .audience(endpointUri.toString())
+                .expirationTime(Date.from(Instant.now().minusSeconds(600)))
+                .issueTime(Date.from(Instant.now()))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetWithoutJit() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId.toString())
+                .issuer(clientId.toString())
+                .audience(endpointUri.toString())
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .issueTime(Date.from(Instant.now()))
+                .build();
+    }
+
+    protected JWTClaimsSet validClaimsSet() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId.toString())
+                .issuer(clientId.toString())
+                .audience(endpointUri.toString())
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .issueTime(Date.from(Instant.now()))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+        return createSecretJWT(claimsSet, clientSecret.getValue());
+    }
+
+    protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet, final String clientSecret)
+            throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
+        final MACSigner signer = new MACSigner(clientSecret);
+        jwt.sign(signer);
+        return jwt;
+    }
+    
+    protected SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
+        final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+        jwt.sign(signer);
+        return jwt;
     }
     
     @Test
     public void testSecretJwt() throws JOSEException, NoSuchAlgorithmException {
-        initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, true);
+        initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, createSecretJWT(validClaimsSet()), true);
 
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
@@ -189,7 +315,7 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
 
     @Test
     public void testPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
-        initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, true);
+        initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, createPrivateKeyJWT(validClaimsSet()), true);
         
         final Event event = action.execute(src);
         ActionTestingSupport.assertProceedEvent(event);
@@ -201,19 +327,63 @@ public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
     }
 
     @Test
-    public void testFailingSecretJwt() throws JOSEException, NoSuchAlgorithmException {
-        initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, false);
+    public void testInvalidSecretJwt_signature() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(validClaimsSet()), false, false);
+    }
 
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    @Test
+    public void testInvalidSecretJwt_iatInTheFuture() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(claimsSetWithIatInTheFuture()), false, true);
     }
 
     @Test
-    public void testFailingPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
-        initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, false);
-        
-        final Event event = action.execute(src);
-        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_CREDENTIALS);
+    public void testInvalidSecretJwt_expInThePast() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(claimsSetWithExpInThePast()), false, true);
+    }
+    
+    @Test
+    public void testInvalidSecretJwt_withoutJit() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(claimsSetWithoutJit()), false, true);
+    }
+    
+    @Test
+    public void testInvalidSecretJwt_jitReplayDetected() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(validClaimsSet()), true, true);
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJwt_signature() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(validClaimsSet()), false, false);
     }
 
+    @Test
+    public void testInvalidPrivateKeyJwt_iatInTheFuture() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(claimsSetWithIatInTheFuture()), false, true);
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJwt_expInThePast() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(claimsSetWithExpInThePast()), false, true);
+    }
+    
+    @Test
+    public void testInvalidPrivateKeyJwt_withoutJit() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(claimsSetWithoutJit()), false, true);
+    }
+    
+    @Test
+    public void testInvalidPrivateKeyJwt_jitReplayDetected() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(validClaimsSet()), true, true);
+    }
+    
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
new file mode 100644
index 00000000..d497e21a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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 java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+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.PrivateKeyJWT;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.utilities.java.support.collection.Pair;
+
+/**
+ * Base unit test class for flows involving JWT based authentication (client_secret_jwt
+ * or private_key_jwt).
+ */
+public abstract class AbstractOidcClientAuthenticationFlowTest extends AbstractOidcApiFlowTest {
+    
+    String clientId = "mockClientId";
+    String clientSecret = "1234567890mockClientSecretmockClientSecretmockClientSecret";
+    String clientIdSaml = "mockSamlClientId";
+    String clientSecretSaml = "mockClientSecretmockClientSecretmockClientSecret";
+
+    String jwtAud = "http://localhost";
+    
+    RSAPrivateKey rsaPrivateKey;
+    RSAPublicKey rsaPublicKey;
+    
+    public AbstractOidcClientAuthenticationFlowTest(final String flowId) {
+        super(flowId);
+    }
+
+    @BeforeClass
+    public void initKeys() throws NoSuchAlgorithmException {
+        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        final KeyPair keyPair = keyGen.genKeyPair();
+        rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+        rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+    }
+    
+    protected void populateClientAssertionParams(final Map<String, String> requestParameters, 
+            final SignedJWT jwt) {
+        requestParameters.put("client_assertion", jwt.serialize());
+        requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+    }
+
+    protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet, final String clientSecret)
+            throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
+        final MACSigner signer = new MACSigner(clientSecret);
+        jwt.sign(signer);
+        return jwt;
+    }
+    
+    protected SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet) throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
+        final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+        jwt.sign(signer);
+        return jwt;
+    }
+    
+    @Test
+    public void testInvalidSecretJWT_missingSub() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetMissingSub(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidSecretJWT_missingIss() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetMissingIss(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidSecretJWT_missingAud() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetMissingAud(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidSecretJWT_missingExp() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetMissingExp(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidSecretJWT_expiredExp() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetExpiredExp(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+
+    @Test
+    public void testInvalidSecretJWT_issuedInTheFuture() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetIssuedInTheFuture(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+
+    @Test
+    public void testInvalidSecretJWT_missingJti() throws Exception {
+        final SignedJWT jwt = createSecretJWT(claimsSetMissingJti(), clientSecret);
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.HS256,
+                ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_missingSub() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingSub());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_missingIss() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingIss());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_missingAud() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingAud());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_missingExp() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingExp());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, "invalid_request");
+        assertErrorDescriptionContains(result, "UnableToDecode");
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_expiredExp() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetExpiredExp());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_issuedInTheFuture() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetIssuedInTheFuture());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+
+    @Test
+    public void testInvalidPrivateKeyJWT_missingJti() throws Exception {
+        final SignedJWT jwt = createPrivateKeyJWT(claimsSetMissingJti());
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS256,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        assertErrorCode(result, getErrorDetaisForJWTValidation().getFirst());
+        assertErrorDescriptionContains(result, getErrorDetaisForJWTValidation().getSecond());
+    }
+    
+    protected JWTClaimsSet claimsSetMissingSub() {
+        return new JWTClaimsSet.Builder()
+                .issuer(clientId)
+                .audience(jwtAud)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetMissingIss() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .audience(jwtAud)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetMissingAud() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetMissingExp() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .audience(jwtAud)
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetExpiredExp() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .audience(jwtAud)
+                .expirationTime(Date.from(Instant.now().minusSeconds(600)))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetIssuedInTheFuture() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .audience(jwtAud)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .issueTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID("mockId")
+                .build();
+    }
+    
+    protected JWTClaimsSet claimsSetMissingJti() {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .audience(jwtAud)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .build();
+    }
+
+    protected ClientSecretJWT buildSecretJwtAuth(String secret) throws JOSEException, URISyntaxException {
+        return new ClientSecretJWT(new ClientID(clientId), new URI(jwtAud),
+                JWSAlgorithm.HS256, new Secret(secret));
+    }
+    
+    protected PrivateKeyJWT buildPrivateKeyJwtAuth() throws JOSEException, URISyntaxException {
+        return new PrivateKeyJWT(new ClientID(clientId), new URI(jwtAud),
+                JWSAlgorithm.RS256, rsaPrivateKey, null, null);   
+    }
+    
+    protected void populateClientAssertionParams(final Map<String, String> requestParameters, 
+            final JWTAuthentication clientAuth) {
+        requestParameters.put("client_assertion", clientAuth.getClientAssertion().serialize());
+        requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+    }
+
+    
+    protected abstract FlowExecutionResult launchWithJwtAuthentication(final SignedJWT jwt,
+            final JWSAlgorithm algorithm, final ClientAuthenticationMethod method) throws Exception;
+
+    /**
+     * Get the pair of error code and error description for the error produced via event
+     * {@link EventIds.ACCESS_DENIED}. This is abstract due to the fact that each endpoint
+     * may have its own mappings.
+     * 
+     * @return The pair of error code and error description.
+     */
+    protected abstract Pair<String, String> getErrorDetaisForJWTValidation();
+}
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 1accfa5d..e0ebd7d6 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
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.security.interfaces.RSAPublicKey;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Date;
@@ -41,6 +42,8 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 
 import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.oauth2.sdk.ErrorResponse;
 import com.nimbusds.oauth2.sdk.GrantType;
 import com.nimbusds.oauth2.sdk.Response;
@@ -124,6 +127,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
     protected void assertErrorDescriptionContains(final FlowExecutionResult result, final String errorDescription) {
         final ErrorResponse errorResponse = parseErrorResponse(result);
         Assert.assertNotNull(errorResponse.getErrorObject().getDescription());
+        System.out.println("Error " + errorResponse.getErrorObject().getDescription());
         Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription));
     }
     
@@ -151,20 +155,27 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
     protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
             final String... redirectUri) throws IOException {
         storeMetadata(storageService, clientId, secret, null, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null,
-                redirectUri);
+                null, redirectUri);
     }
 
     protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
             final JWSAlgorithm tokenEndpointSigAlg, final ClientAuthenticationMethod tokenEndpointMethod,
             final String... redirectUri)
             throws IOException {
-        storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, null, redirectUri);
+        storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, null, null, redirectUri);
     }
 
     protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
             final JWSAlgorithm tokenEndpointSigAlg, final ClientAuthenticationMethod tokenEndpointMethod,
             final JWSAlgorithm userInfoSigAlg, final String... redirectUri)
             throws IOException {
+        storeMetadata(storageService, clientId, secret, tokenEndpointSigAlg, tokenEndpointMethod, userInfoSigAlg, null, redirectUri);
+    }
+    
+    protected void storeMetadata(final StorageService storageService, final String clientId, final String secret,
+            final JWSAlgorithm tokenEndpointSigAlg, 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)));
@@ -184,8 +195,16 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
         metadata.setTokenEndpointAuthJWSAlg(tokenEndpointSigAlg);
         metadata.setTokenEndpointAuthMethod(tokenEndpointMethod);
         metadata.setUserInfoJWSAlg(userInfoSigAlg);
-        final OIDCClientInformation information = new OIDCClientInformation(new ClientID(clientId), new Date(),
+        final OIDCClientInformation information;
+        if (publicKey == null) {
+            information = new OIDCClientInformation(new ClientID(clientId), new Date(),
                 metadata, new Secret(secret));
+        } else {
+            RSAKey rsaKey = new RSAKey.Builder(publicKey).build();
+            JWKSet jwkSet = new JWKSet(rsaKey);
+            metadata.setJWKSet(jwkSet);
+            information = new OIDCClientInformation(new ClientID(clientId), metadata);
+        }
         storageService.create(BaseStorageServiceClientInformationComponent.CONTEXT_NAME, clientId, 
                 information.toJSONObject().toJSONString(), System.currentTimeMillis() + (60 * 60 * 1000));
         
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 0959c0f1..b897750c 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
@@ -20,7 +20,11 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
 import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,18 +34,22 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 
+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 OAuth2 introspection flow.
  */
-public class IntrospectionFlowTest extends AbstractOidcApiFlowTest {
+public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowTest {
 
     public static final String FLOW_ID = "oauth2/introspection";
 
@@ -163,4 +171,30 @@ public class IntrospectionFlowTest extends AbstractOidcApiFlowTest {
         TokenIntrospectionErrorResponse resp = (TokenIntrospectionErrorResponse) parseErrorResponse(result);
         Assert.assertEquals(resp.getErrorObject().getCode(), OAuth2Error.INVALID_CLIENT_CODE);
     }
+    
+    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, algorithm, method);
+        } else {
+            storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
+        }
+        final String accessToken =  super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token");
+        Map<String, String> requestParameters = createRequestParameters(accessToken, clientId);
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+    }
+    
+    protected Map<String, String> createRequestParameters(final String token, final String clientId) {
+        final Map<String, String> result = new HashMap<>();
+        result.put("token", token);
+        result.put("client_id", clientId);
+        return result;
+    }
+
+    protected Pair<String, String> getErrorDetaisForJWTValidation() {
+        return new Pair<>("invalid_client", "Client authentication failed");
+    }
+
 }
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 7174eccb..f458edb7 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
@@ -21,6 +21,8 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,19 +32,23 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.ParseException;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
 
 import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.OAuth2RevocationSuccessResponse;
+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 OAuth2 revocation flow.
  */
-public class RevocationFlowTest extends AbstractOidcApiFlowTest {
+public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest {
 
     public static final String FLOW_ID = "oauth2/revocation";
     
@@ -86,7 +92,7 @@ public class RevocationFlowTest extends AbstractOidcApiFlowTest {
         parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
     }
 
-   @Test
+    @Test
     public void testSuccessWithSamlMetadata() throws IOException, NoSuchAlgorithmException, URISyntaxException,
         DataSealerException, ComponentInitializationException {
         setBasicAuth(clientIdSaml, clientSecretSaml);
@@ -117,6 +123,26 @@ public class RevocationFlowTest extends AbstractOidcApiFlowTest {
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
     }
+    
+    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, algorithm, method);
+        } else {
+            storeMetadata(storageService, clientId, null, algorithm, method, null, rsaPublicKey);
+        }
+        final String accessToken =  super.buildToken(clientId, "sub", Scope.parse("openid")).toJSONObject().getAsString("access_token");
+        Map<String, String> requestParameters = new HashMap<>();
+        requestParameters.put("token", accessToken);
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+    }
+    
+    protected Pair<String, String> getErrorDetaisForJWTValidation() {
+        return new Pair<>("invalid_client", "Client authentication failed");
+    }
+
 
     @Test
     public void testFailedAuthentication() throws IOException, NoSuchAlgorithmException, URISyntaxException,
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 3e953e2f..6e0f61fb 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.URI;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
@@ -37,6 +36,7 @@ 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;
 import com.nimbusds.oauth2.sdk.AuthorizationCode;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.Scope;
@@ -58,25 +58,22 @@ 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.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.
  */
-public class TokenFlowTest extends AbstractOidcFlowTest {
+public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
     
     public static final String FLOW_ID = "oidc/token";
     
     String redirectUri = "https://example.org/cb";
-    String clientId = "mockClientId";
-    String clientIdSaml = "mockSamlClientId";
     String clientIdPkcePlain = "mockClientIdPKCEPlain";
     String clientIdPkcePlainUnforced = "mockClientIdPKCEPlainUnforced";
     String clientIdPkceS256 = "mockClientIdPKCES256";
-    String clientSecret = "mockClientSecretmockClientSecretmockClientSecret";
     String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
-
     
     @Autowired
     @Qualifier("shibboleth.StorageService")
@@ -194,7 +191,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
         DataSealerException, ComponentInitializationException, java.text.ParseException {
         initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "authorization_code",
                 buildAuthorizationCode(clientIdSaml), clientIdSaml));
-        setBasicAuth(clientIdSaml, clientSecret);
+        setBasicAuth(clientIdSaml, clientSecretSaml);
         storeConsent(storageService, "jdoe", clientIdSaml, "mail");
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
@@ -444,7 +441,7 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
         try {
             return AccessTokenClaimsSet.parse(accessToken.getValue(), 
                     BaseOIDCResponseActionTest.initializeDataSealer());
-        } catch (NoSuchAlgorithmException | java.text.ParseException | DataSealerException
+        } catch (final NoSuchAlgorithmException | java.text.ParseException | DataSealerException
                 | ComponentInitializationException e) {
             return null;
         }
@@ -470,16 +467,22 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
         setHttpFormRequest("POST", requestParameters);
         return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
-    
-    protected ClientSecretJWT buildSecretJwtAuth(String secret) throws JOSEException, URISyntaxException {
-        return new ClientSecretJWT(new ClientID(clientId), new URI("https://op.example.org"),
-                JWSAlgorithm.HS256, new Secret(secret));
-    }
-    
-    protected void populateClientAssertionParams(final Map<String, String> requestParameters, 
-            final JWTAuthentication clientAuth) {
-        requestParameters.put("client_assertion", clientAuth.getClientAssertion().serialize());
-        requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+
+    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",
+                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);
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(FLOW_ID, null, externalContext);
     }
 
     protected Map<String, String> createRequestParameters(String redirectUri, String grantType, String code, 
@@ -510,4 +513,9 @@ public class TokenFlowTest extends AbstractOidcFlowTest {
             map.put(key, value);
         }
     }
+
+    protected Pair<String, String> getErrorDetaisForJWTValidation() {
+        return new Pair<>("invalid_client", "Client authentication failed");
+    }
+
 }

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


More information about the commits mailing list