[java-oidc-common] branch main updated: JCOMOIDC-184 - Move OAuth2Client authentication flow from OP to oidc-common

Codeberg noreply at shibboleth.net
Fri Sep 18 14:22:00 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
https://codeberg.org/Shibboleth/java-oidc-common/commit/e6cc5f66f447d47434cd95216fcb366d04fbde33

The following commit(s) were added to refs/heads/main by this push:
     new e6cc5f66 JCOMOIDC-184 - Move OAuth2Client authentication flow from OP to oidc-common
e6cc5f66 is described below

commit e6cc5f66f447d47434cd95216fcb366d04fbde33
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 18 16:28:10 2026 +0300

    JCOMOIDC-184 - Move OAuth2Client authentication flow from OP to oidc-common
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-184
    
    - Moved SWF actions. audit extractors and tests from OP's net.shibboleth.idp.plugin.oidc.op.authn package to net.shibboleth.oidc.authn (oidc-common-crypto)
---
 oidc-common-crypto-impl/pom.xml                    |  18 +-
 ...thenticationJWTPayloadClaimsAuditExtractor.java |  99 +++++
 ...tAuthenticationJWTTypeHeaderAuditExtractor.java |  75 ++++
 .../oidc/authn/audit/impl/package-info.java        |  16 +
 .../ExtractClientAuthenticationFromRequest.java    | 176 ++++++++
 .../oidc/authn/impl/JWTCredentialValidator.java    | 302 ++++++++++++++
 .../impl/OIDCClientInfoCredentialValidator.java    | 177 ++++++++
 .../impl/ValidateClientAuthenticationType.java     | 208 ++++++++++
 .../shibboleth/oidc/authn/impl/package-info.java   |  19 +
 ...ExtractClientAuthenticationFromRequestTest.java | 207 ++++++++++
 .../authn/impl/JWTCredentialValidatorTest.java     | 447 +++++++++++++++++++++
 .../OIDCClientInfoCredentialValidatorTest.java     | 160 ++++++++
 .../impl/ValidateClientAuthenticationTypeTest.java | 160 ++++++++
 13 files changed, 2063 insertions(+), 1 deletion(-)

diff --git a/oidc-common-crypto-impl/pom.xml b/oidc-common-crypto-impl/pom.xml
index 1e6d6b32..21e7f626 100644
--- a/oidc-common-crypto-impl/pom.xml
+++ b/oidc-common-crypto-impl/pom.xml
@@ -51,6 +51,11 @@
             <artifactId>idp-authn-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-profile-api</artifactId>
@@ -148,7 +153,18 @@
             <artifactId>idp-testing</artifactId>
             <scope>test</scope>
         </dependency>
-
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-impl</artifactId>
+            <version>${idp.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTPayloadClaimsAuditExtractor.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTPayloadClaimsAuditExtractor.java
new file mode 100644
index 00000000..66499484
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTPayloadClaimsAuditExtractor.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.audit.impl;
+
+import java.text.ParseException;
+import java.util.Date;
+import java.util.Optional;
+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 com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/** {@link Function} that returns the desired claim from the client authentication JWT payload. */
+public class ClientAuthenticationJWTPayloadClaimsAuditExtractor implements Function<ProfileRequestContext, String> {
+
+    /** Strategy that will return {@link OAuth2ClientAuthenticationContext}. */
+    @Nonnull private Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> clientAuthContextLookupStrategy;
+
+    /** The claim whose value is to be extracted. */
+    @Nonnull @NotEmpty private final String key;
+
+    /**
+     * Constructor.
+     *
+     * @param claim Claim whose value is to be extracted
+     */
+    public ClientAuthenticationJWTPayloadClaimsAuditExtractor(
+            @Nonnull @NotEmpty @ParameterName(name = "key") final String claim) {
+        key = Constraint.isNotEmpty(claim, "The claim cannot be empty");
+        // PRC -> AuthenticationContext -> OAuth2ClientAuthenticationContext
+        final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> cacls =
+                new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class).compose(
+                        new ChildContextLookup<>(AuthenticationContext.class));
+        assert cacls != null;
+        clientAuthContextLookupStrategy = cacls;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param claim Claim whose value is to be extracted
+     * @param lookupStrategy Strategy that will return {@link OAuth2ClientAuthenticationContext}.
+     */
+    public ClientAuthenticationJWTPayloadClaimsAuditExtractor(
+            @Nonnull @NotEmpty @ParameterName(name = "key") final String claim,
+            @Nonnull @ParameterName(name = "clientAuthContextLookupStrategy")
+            final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> lookupStrategy) {
+        key = Constraint.isNotEmpty(claim, "key cannot be empty");
+        clientAuthContextLookupStrategy = 
+                Constraint.isNotNull(lookupStrategy, "clientAuthContextLookupStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext input) {
+        final SignedJWT jwt =  Optional.ofNullable(clientAuthContextLookupStrategy.apply(input))
+                .map(ctx -> ctx.getClientAuthentication())
+                .filter(JWTAuthentication.class::isInstance)
+                .map(JWTAuthentication.class::cast)
+                .map(jwtAuthentication -> jwtAuthentication.getClientAssertion())
+                .orElse(null);
+        try {
+            final Object claim = jwt == null ? null : jwt.getJWTClaimsSet().getClaim(key);
+            if (claim instanceof Date date) {
+                return date.toInstant().toString();
+            } else if (claim != null) {
+                return claim.toString();
+            } else {
+                return null;
+            }
+        } catch (final ParseException e) {
+            return null;
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTTypeHeaderAuditExtractor.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTTypeHeaderAuditExtractor.java
new file mode 100644
index 00000000..b9a72abe
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/ClientAuthenticationJWTTypeHeaderAuditExtractor.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.audit.impl;
+
+import java.util.Optional;
+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 com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+
+/** {@link Function} that returns the type header from the client authentication JWT payload. */
+public class ClientAuthenticationJWTTypeHeaderAuditExtractor implements Function<ProfileRequestContext, String> {
+
+    /** Strategy that will return {@link OAuth2ClientAuthenticationContext}. */
+    @Nonnull private Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> clientAuthContextLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public ClientAuthenticationJWTTypeHeaderAuditExtractor() {
+        // PRC -> AuthenticationContext -> OAuth2ClientAuthenticationContext
+        final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> cacls =
+                new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class).compose(
+                        new ChildContextLookup<>(AuthenticationContext.class));
+        assert cacls != null;
+        clientAuthContextLookupStrategy = cacls;
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param lookupStrategy Strategy that will return {@link OAuth2ClientAuthenticationContext}.
+     */
+    public ClientAuthenticationJWTTypeHeaderAuditExtractor(
+            @Nonnull @ParameterName(name = "clientAuthContextLookupStrategy")
+            final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> lookupStrategy) {
+        clientAuthContextLookupStrategy = 
+                Constraint.isNotNull(lookupStrategy, "clientAuthContextLookupStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nullable final ProfileRequestContext input) {
+        return Optional.ofNullable(clientAuthContextLookupStrategy.apply(input))
+                .map(ctx -> ctx.getClientAuthentication())
+                .filter(JWTAuthentication.class::isInstance)
+                .map(JWTAuthentication.class::cast)
+                .map(jwtAuthentication -> jwtAuthentication.getClientAssertion())
+                .map(jwt -> jwt.getHeader().getType())
+                .map(joseType -> joseType.getType())
+                .orElse(null);
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/package-info.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/package-info.java
new file mode 100644
index 00000000..34192964
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/audit/impl/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Package for audit extractors related to client authentication. */
+package net.shibboleth.oidc.authn.audit.impl;
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequest.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequest.java
new file mode 100644
index 00000000..d9c1852a
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.AbstractOptionallyAuthenticatedRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.TLSClientAuthentication;
+
+import net.shibboleth.idp.authn.AbstractExtractionAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.CertificateContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.profile.config.navigate.TokenEndpointAuthMethodLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Extracts OAuth 2 client authentication details from a request and stores them in an
+ * {@link OAuth2ClientAuthenticationContext} beneath the {@link AuthenticationContext} for subsequent
+ * validation.
+ * 
+ * <p>Depending on the form of authentication, additional child contexts may be created to store
+ * extracted credentials, and they may undergo configured transformations. For example, password-based
+ * methods will result in a {@link UsernamePasswordContext}, certificate-based in an {@link CertificateContext},
+ * etc.</p>
+ * 
+ * @pre ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null
+ * @post AuthenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class) ! null
+ *  along with other contexts as appropriate
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ */
+public class ExtractClientAuthenticationFromRequest extends AbstractExtractionAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractClientAuthenticationFromRequest.class);
+
+    /** Lookup strategy for enabled client authentication methods. */
+    @NonnullAfterInit private Function<ProfileRequestContext, Set<ClientAuthenticationMethod>>
+        clientAuthMethodsLookupStrategy;
+
+    /** Message to extract credentials from. */
+    @Nullable private AbstractOptionallyAuthenticatedRequest request;
+
+    /**
+     * Constructor.
+     */
+    public ExtractClientAuthenticationFromRequest() {
+        clientAuthMethodsLookupStrategy = new TokenEndpointAuthMethodLookupFunction();
+    }
+
+    /**
+     * Set the lookup strategy for enabled client authentication methods.
+     *
+     * @param strategy What to set.
+     */
+    public void setClientAuthMethodsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, Set<ClientAuthenticationMethod>> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        clientAuthMethodsLookupStrategy = Constraint.isNotNull(strategy,
+                "Client authentication methods lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        if (profileRequestContext.getInboundMessageContext() != null) {
+            final Object msg = profileRequestContext.ensureInboundMessageContext().getMessage();
+            if (msg instanceof AbstractOptionallyAuthenticatedRequest aoar) {
+                request = aoar;
+                return true;
+            }
+        }
+        
+        log.warn("{} Inbound message missing or of incorrect type", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+        return false;
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        assert request != null;
+        final ClientAuthentication clientAuthentication = request.getClientAuthentication();
+        
+        final OAuth2ClientAuthenticationContext ctx =
+                authenticationContext.ensureSubcontext(OAuth2ClientAuthenticationContext.class);
+        ctx.setClientAuthentication(clientAuthentication);
+
+        if (clientAuthentication == null) {
+            log.debug("{} No OAuth client credentials in request", getLogPrefix());
+            final Set<ClientAuthenticationMethod> methods =
+                    clientAuthMethodsLookupStrategy.apply(profileRequestContext);
+            //  Build event only if 'none' is not enabled in the profile configuration
+            if (methods == null || !methods.contains(ClientAuthenticationMethod.NONE)) {
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            }
+            return;
+        }
+
+        // Note the Nimbus APIs appear to prevent the client ID or secret from being null.
+        
+        if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientAuthentication.getMethod())) {
+            final ClientSecretBasic basic = (ClientSecretBasic) clientAuthentication;
+            if (basic.getClientID() != null && basic.getClientSecret() != null) {
+                final UsernamePasswordContext upContext = new UsernamePasswordContext();
+                upContext.setUsername(applyTransforms(basic.getClientID().getValue()))
+                    .setPassword(basic.getClientSecret().getValue());
+                authenticationContext.addSubcontext(upContext, true);
+            } else {
+                log.warn("{} No OAuth client credentials in basic-auth request?", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            }
+        } else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientAuthentication.getMethod())) {
+            final ClientSecretPost post = (ClientSecretPost) clientAuthentication;
+            if (post.getClientID() != null && post.getClientSecret() != null) {
+                final UsernamePasswordContext upContext = new UsernamePasswordContext();
+                upContext.setUsername(applyTransforms(post.getClientID().getValue()))
+                    .setPassword(post.getClientSecret().getValue());
+                authenticationContext.addSubcontext(upContext, true);
+            } else {
+                log.warn("{} No OAuth client credentials in POST request?", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            }
+        } else if (ClientAuthenticationMethod.TLS_CLIENT_AUTH.equals(clientAuthentication.getMethod()) ||
+                ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH.equals(clientAuthentication.getMethod())) {
+            final TLSClientAuthentication tls = (TLSClientAuthentication) clientAuthentication;
+            if (tls.getClientX509Certificate() != null) {
+                final CertificateContext certContext = new CertificateContext();
+                certContext.setCertificate(tls.getClientX509Certificate());
+                authenticationContext.addSubcontext(certContext, true);
+            }
+        }
+    }
+// Checkstyle: CyclomaticComplexity ON
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidator.java
new file mode 100644
index 00000000..b7f1cb59
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidator.java
@@ -0,0 +1,302 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.text.ParseException;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AbstractCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.ClaimsValidatorLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.ClientAuthenticationJWTTypeLookupFunction;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+/**
+ * A validator that handles authentication via signed JWT.
+ * 
+ * <p>For now, implemented via Nimbus APIs.</p>
+ * 
+ * TODO: there will be additional validation checks added once implemented on the older branch
+ */
+ at ThreadSafeAfterInit
+public class JWTCredentialValidator extends AbstractCredentialValidator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(JWTCredentialValidator.class);
+    
+    /** Strategy that will return {@link OAuth2ClientAuthenticationContext}. */
+    @Nonnull private Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> clientAuthContextLookupStrategy;
+
+    /** Strategy used to locate the {@link SecurityParametersContext} to use for verification. */
+    @Nonnull private Function<ProfileRequestContext,SecurityParametersContext> securityParametersLookupStrategy;
+
+    /** Strategy used to obtain {@link ClaimsValidator}. */
+    @Nonnull private Function<ProfileRequestContext,ClaimsValidator> claimsValidatorLookupStrategy;
+
+    /** Strategy used to fetch required JWT type header value. */
+    @Nonnull private Function<ProfileRequestContext,String> requiredJwtTypeHeaderLookupStrategy;
+
+    /** Whether to save the JWT in the Java Subject's public credentials. */
+    private boolean saveTokenToCredentialSet;
+    
+    /** Constructor. */
+    public JWTCredentialValidator() {
+        // PRC -> AuthenticationContext -> OAuth2ClientAuthenticationContext
+        final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> cacls =
+                new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class).compose(
+                        new ChildContextLookup<>(AuthenticationContext.class));
+        assert cacls != null;
+        clientAuthContextLookupStrategy = cacls;
+        // PRC -> INBOUND -> SPC
+        final Function<ProfileRequestContext,SecurityParametersContext> spls =
+                new ChildContextLookup<>(SecurityParametersContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert spls != null;
+        securityParametersLookupStrategy = spls;
+        
+        claimsValidatorLookupStrategy = new ClaimsValidatorLookupFunction();
+        requiredJwtTypeHeaderLookupStrategy = new ClientAuthenticationJWTTypeLookupFunction();
+    }
+    
+    /**
+     * Set the strategy used to return the {@link OAuth2ClientAuthenticationContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setOAuth2ClientAuthenticationLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OAuth2ClientAuthenticationContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+    
+        clientAuthContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OAuth2ClientAuthenticationContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link SecurityParametersContext} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSecurityParametersLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SecurityParametersContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+
+        securityParametersLookupStrategy =
+                Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate {@link ClaimsValidator} used.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClaimsValidatorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,ClaimsValidator> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        claimsValidatorLookupStrategy =
+                Constraint.isNotNull(strategy, "ClaimsValidator lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set whether to save the JWT in the Java Subject's public credentials.
+     * 
+     * <p>Defaults to true</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setSaveTokenToCredentialSet(final boolean flag) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        
+        saveTokenToCredentialSet = flag;
+    }
+
+    /**
+     * Set the strategy used to fetch required JWT type header value.
+     *
+     * @param strategy lookup strategy
+     *
+     * @since 4.3.0
+     */
+    public void setRequiredJwtTypeHeaderLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+
+        requiredJwtTypeHeaderLookupStrategy =
+                Constraint.isNotNull(strategy, "RequiredJwtTypeHeaderLookupStrategy cannot be null");
+    }
+    
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+        
+        final OAuth2ClientAuthenticationContext clientAuthContext =
+                clientAuthContextLookupStrategy.apply(profileRequestContext);
+        if (clientAuthContext == null) {
+            log.debug("{} No OAuth 2.0 client authentication information found", getLogPrefix());
+            return null;
+        }
+        
+        final ClientAuthentication clientAuth = clientAuthContext.getClientAuthentication();
+        if (clientAuth == null) {
+            log.debug("{} No OAuth 2.0 client authentication information found", getLogPrefix());
+            return null;
+        }
+        if (!ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(clientAuth.getMethod()) &&
+                !ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientAuth.getMethod())) {
+            log.debug("{} OAuth client authentication for '{}' of unsupported type: {}", getLogPrefix(),
+                    clientAuth.getClientID(), clientAuth.getMethod());
+            return null;
+        }
+        
+        if (!(clientAuth instanceof JWTAuthentication)) {
+            log.warn("{} OAuth client authentication object of unexpected type: {}", getLogPrefix(),
+                    clientAuth.getClass().getSimpleName());
+            log.info("{} Login by '{}' failed", getLogPrefix(), clientAuth.getClientID());
+            final LoginException e = new LoginException(AuthnEventIds.INVALID_CREDENTIALS); 
+            if (errorHandler != null) { 
+                errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+            }
+            throw e;
+        }
+
+        final JWTAuthentication jwtAuth = (JWTAuthentication) clientAuth;
+        final SignedJWT clientAssertion = jwtAuth.getClientAssertion();
+        final ClientID clientId = clientAuth.getClientID();
+        assert clientAssertion != null;
+        assert clientId != null;
+        try {
+            validateJWTClaims(profileRequestContext, clientAssertion, clientId);
+        } 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(clientId, clientAssertion);
+    }    
+// 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 String requiredType = requiredJwtTypeHeaderLookupStrategy.apply(profileRequestContext);
+        if (StringSupport.trimOrNull(requiredType) != null) {
+            log.debug("{} Type header is required to be {}", getLogPrefix(), requiredType);
+            final String type = Optional.ofNullable(jwt.getHeader().getType())
+                    .map(joseType -> joseType.getType())
+                    .orElse(null);
+            if (!requiredType.equals(type)) {
+                log.warn("{} JWT validation failed for client '{}': Invalid JWT type header {}",
+                        getLogPrefix(), clientId, type);
+                throw new JWTValidationException("Invalid JWT type header " + type);
+            }
+        }
+
+        final ClaimsValidator validator = claimsValidatorLookupStrategy.apply(profileRequestContext);
+        if (validator == null) {
+            log.warn("{} JWT validation failed for client '{}': No ClaimsValidator found in configuration",
+                    getLogPrefix(), clientId);
+            throw new JWTValidationException("No ClaimsValidator found in configuration");
+        }
+        
+        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;
+        }
+
+        assert claimsSet != null;
+        try {
+            validator.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.
+    *
+    * @param clientId client ID
+    * @param token the token validated
+    * 
+    * @return the decorated subject
+    */
+   @Nonnull protected Subject populateSubject(@Nonnull @NotEmpty final ClientID clientId,
+           @Nonnull final SignedJWT token) {
+      
+       final Subject subject = new Subject();
+       final String clientIdValue = clientId.getValue();
+       if (clientIdValue != null) {
+           subject.getPrincipals().add(new UsernamePrincipal(clientIdValue));
+           if (saveTokenToCredentialSet) {
+               subject.getPublicCredentials().add(token);
+           }
+       }
+      
+       return super.populateSubject(subject);
+   }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidator.java
new file mode 100644
index 00000000..cb2c98f1
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidator.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AbstractUsernamePasswordCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.codec.StringDigester;
+import net.shibboleth.shared.codec.StringDigester.OutputFormat;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.client.ClientMetadata;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+/**
+ * A password validator that authenticates against OIDC client metadata (which may itself be emulated
+ * via SAML metadata).
+ */
+ at ThreadSafeAfterInit
+public class OIDCClientInfoCredentialValidator extends AbstractUsernamePasswordCredentialValidator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCClientInfoCredentialValidator.class);
+    
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext,OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    
+    /** Digester for SHA-1. */
+    @NonnullAfterInit private StringDigester digester;
+
+    /** Constructor. */
+    public OIDCClientInfoCredentialValidator() {
+        final Function<ProfileRequestContext,OIDCMetadataContext> omcls =
+                new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert omcls != null;
+        oidcMetadataContextLookupStrategy = omcls;
+    }
+    
+    /**
+     * Set the strategy used to return the {@link OIDCMetadataContext}.
+     * 
+     * @param strategy The lookup strategy.
+     */
+    public void setOidcMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCMetadataContext> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+    
+        oidcMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        try {
+            digester = new StringDigester("SHA-256", OutputFormat.BASE64);
+        } catch (final NoSuchAlgorithmException e) {
+            throw new ComponentInitializationException("Error creating digester", e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+
+        final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+        
+        if (oidcMetadataContext == null || oidcMetadataContext.getClientInformation() == null ) {
+            log.debug("{} OIDC client metadata is missing", getLogPrefix());
+            return null;
+        }
+
+        final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+        assert clientInformation != null;
+        final ClientMetadata clientMetadata = clientInformation.getMetadata();
+        assert clientMetadata != null;
+        if (ClientAuthenticationMethod.NONE.equals(clientMetadata.getTokenEndpointAuthMethod())) {
+            log.debug("{} OIDC client metadata contains 'none' type for endpoint authentication");
+            final Subject subject = new Subject();
+            final ClientID clientId = clientInformation.getID();
+            assert clientId != null;
+            final String clientIdValue = clientId.getValue();
+            if (clientIdValue != null) {
+                subject.getPrincipals().add(new UsernamePrincipal(applyTransforms(clientIdValue)));
+            }
+            return super.populateSubject(subject);
+        } else if (clientInformation.getSecret() == null) {
+            log.debug("{} OIDC client metadata for '{}' missing client secret", getLogPrefix(),
+                    clientInformation.getID());
+            return null;
+        }
+
+        return super.doValidate(profileRequestContext, authenticationContext, warningHandler, errorHandler);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable protected Subject doValidate(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final UsernamePasswordContext usernamePasswordContext,
+            @Nullable final WarningHandler warningHandler,
+            @Nullable final ErrorHandler errorHandler) throws Exception {
+
+        final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+        if (oidcMetadataContext == null || oidcMetadataContext.getClientInformation() == null ) {
+            log.debug("{} OIDC client metadata is missing", getLogPrefix());
+            return null;
+        }
+
+        final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+        assert clientInformation != null;
+
+        final String username = usernamePasswordContext.getTransformedUsername();
+        log.debug("{} Attempting to authenticate effective client ID '{}' ", getLogPrefix(), username);
+        
+        final String secret = clientInformation.getSecret().getValue();
+        if (secret.startsWith("{SHA2}")) {
+            if (secret.substring(6).equals(digester.apply(usernamePasswordContext.getPassword()))) {
+                log.info("{} Login by '{}' succeeded", getLogPrefix(), username);
+                return populateSubject(new Subject(), usernamePasswordContext);
+            }
+        } else if (secret.equals(usernamePasswordContext.getPassword())) {
+            log.info("{} Login by '{}' succeeded", getLogPrefix(), username);
+            return populateSubject(new Subject(), usernamePasswordContext);
+        }
+        
+        log.info("{} Login by '{}' failed", getLogPrefix(), username);
+        
+        final LoginException e = new LoginException(AuthnEventIds.INVALID_CREDENTIALS); 
+        if (errorHandler != null) { 
+            errorHandler.handleError(profileRequestContext, authenticationContext, e,
+                    AuthnEventIds.INVALID_CREDENTIALS);
+        }
+        throw e;
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationType.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationType.java
new file mode 100644
index 00000000..e83fdb02
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationType.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.profile.config.navigate.TokenEndpointAuthMethodLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validates the client authentication type with the token_endpoint_auth_method stored in the client's metadata
+ * and the profile configuration.
+ * 
+ * <p>In the absence of metadata, the profile configuration is used alone.</p>
+ * 
+ * @pre {@link OIDCMetadataContext} is available
+ * @pre AuthenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class) != null
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#ACCESS_DENIED}
+ */
+public class ValidateClientAuthenticationType extends AbstractAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateClientAuthenticationType.class);
+    
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext,OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+    
+    /** Strategy to obtain enabled token endpoint authentication methods. */
+    @Nonnull private Function<ProfileRequestContext,Set<ClientAuthenticationMethod>> 
+        tokenEndpointAuthMethodsLookupStrategy;
+    
+    /** The attached OIDC metadata context. */
+    @Nullable private OIDCMetadataContext oidcMetadataContext;
+
+    /** The extracted client authentication information. */
+    @Nullable private ClientAuthentication clientAuthentication;
+    
+    /** Enabled client authn methods. */
+    @Nullable @NonnullElements private Set<ClientAuthenticationMethod> enabledMethods;
+    
+    /**
+     * Constructor.
+     */
+    public ValidateClientAuthenticationType() {
+        final Function<ProfileRequestContext,OIDCMetadataContext> omcls =
+                new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                        new InboundMessageContextLookup());
+        assert omcls != null;
+        oidcMetadataContextLookupStrategy = omcls;
+        tokenEndpointAuthMethodsLookupStrategy = new TokenEndpointAuthMethodLookupFunction();
+    }
+        
+    /**
+     * Set the strategy used to return the {@link OIDCMetadataContext}.
+     * 
+     * @param strategy The lookup strategy.
+     */
+    public void setOIDCMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
+        checkSetterPreconditions();
+
+        oidcMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set strategy to obtain enabled token endpoint authentication methods.
+     * @param strategy What to set.
+     */
+    public void setTokenEndpointAuthMethodsLookupStrategy(@Nonnull final Function<ProfileRequestContext, 
+            Set<ClientAuthenticationMethod>> strategy) {
+        checkSetterPreconditions();
+
+        tokenEndpointAuthMethodsLookupStrategy = Constraint.isNotNull(strategy, 
+                "Strategy to obtain enabled token endpoint authentication methods cannot be null");
+        
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+
+        final OAuth2ClientAuthenticationContext oauth2Ctx =
+                authenticationContext.getSubcontext(OAuth2ClientAuthenticationContext.class);
+        if (oauth2Ctx != null) {
+            clientAuthentication = oauth2Ctx.getClientAuthentication();
+        }
+
+        oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+        
+        enabledMethods = tokenEndpointAuthMethodsLookupStrategy.apply(profileRequestContext);
+        if (enabledMethods == null) {
+            enabledMethods = Collections.emptySet();
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        
+        final ClientAuthenticationMethod registeredMethod;
+
+        // Pull the client's registered authn method, or default to client_secret_basic.
+        // If no metadata exists, leave null.
+        if (oidcMetadataContext != null) {
+            final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+            if (clientInformation != null) {
+                final OIDCClientMetadata clientMetadata = clientInformation.getOIDCMetadata();
+                registeredMethod = clientMetadata.getTokenEndpointAuthMethod() != null ? 
+                        clientMetadata.getTokenEndpointAuthMethod() : ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
+            } else {
+                registeredMethod = null;
+            }
+        } else {
+            registeredMethod = null;
+        }
+
+        // Did the client use what it registered and is that still allowed?
+        // The enabledMethods member contains the methods authorized in the configuration as a whole.
+
+        final ClientAuthenticationMethod used =
+                clientAuthentication != null ? clientAuthentication.getMethod() : ClientAuthenticationMethod.NONE;
+
+        if (registeredMethod != null && !registeredMethod.equals(used)) {
+            log.warn("{} Client '{}' registered {} but attempted {}", getLogPrefix(), getClientID(),
+                    registeredMethod, used);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        assert enabledMethods != null;
+        if (!enabledMethods.contains(used)) {
+            log.warn("{} Requested method {} not enabled in profile configuration", getLogPrefix(), used);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+        }
+    }
+
+    /**
+     * Parses the client ID from OIDC metadata or client authentication, if exists.
+     * 
+     * @return client ID, or null it it couldn't be found.
+     */
+    @Nullable private String getClientID() {
+        if (oidcMetadataContext != null) {
+            final OIDCClientInformation clientInformation = oidcMetadataContext.getClientInformation();
+            if (clientInformation != null) {
+                return getClientIDValue(clientInformation.getID());
+            }
+        }
+        if (clientAuthentication != null) {
+            return getClientIDValue(clientAuthentication.getClientID());
+        }
+        return null;
+    }
+
+    /**
+     * Get the client ID value as string if the object is non-null.
+     *     
+     * @param clientId client ID, may be null.
+     * @return the client ID value.
+     */
+    @Nullable private String getClientIDValue(@Nullable final ClientID clientId) {
+        return clientId == null ? null : clientId.getValue();
+    }
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/package-info.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/package-info.java
new file mode 100644
index 00000000..b12b9e0f
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/authn/impl/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * Implementation classes supporting OIDC/OAuth client authentication.
+ */
+package net.shibboleth.oidc.authn.impl;
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequestTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequestTest.java
new file mode 100644
index 00000000..50630306
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ExtractClientAuthenticationFromRequestTest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.authn.impl.ExtractClientAuthenticationFromRequest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+
+/**
+ * Unit tests for {@link ExtractClientAuthenticationFromRequest}.
+ */
+public class ExtractClientAuthenticationFromRequestTest {
+    
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private URI endpointUri;
+    
+    private RSAPrivateKey rsaPrivateKey;
+    
+    private ExtractClientAuthenticationFromRequest action;
+    
+    private RequestContext rc;
+    private ProfileRequestContext prc;
+    
+    @BeforeClass
+    public void initKeys() throws NoSuchAlgorithmException {
+        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        final KeyPair keyPair = keyGen.genKeyPair();
+        rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+    }
+
+    @BeforeMethod
+    public void init() throws URISyntaxException, ComponentInitializationException {
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        endpointUri = new URI("https://mock.example.org/");
+        
+        action = new ExtractClientAuthenticationFromRequest();
+        action.initialize();
+    }
+    
+    protected void initializeRequestCtx(final ClientAuthenticationMethod method)
+            throws JOSEException, ComponentInitializationException {
+        final ClientAuthentication clientAuth;
+        if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+            clientAuth = new ClientSecretBasic(clientId, clientSecret);
+        } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+            clientAuth = new ClientSecretPost(clientId, clientSecret);
+        } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+            clientAuth = new ClientSecretJWT(clientId, endpointUri, JWSAlgorithm.HS256, clientSecret);
+        } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+            clientAuth = new PrivateKeyJWT(clientId, endpointUri, JWSAlgorithm.RS256, (PrivateKey) rsaPrivateKey, null,
+                    null);
+        } else {
+            clientAuth = null;
+        }
+        final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
+        
+        rc = new RequestContextBuilder()
+                .setInboundMessage(new TokenRequest(null, clientAuth, authzGrant, null))
+                .buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(rc);
+        prc.addSubcontext(new AuthenticationContext());
+    }
+
+    @Test
+    public void testNoAuthnContext() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        prc.removeSubcontext(AuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+
+    @Test
+    public void testBasic() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.ensureSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        assert oauth != null;
+        final ClientAuthentication clientAuthentication = oauth.getClientAuthentication();
+        assert clientAuthentication != null;
+        Assert.assertEquals(clientAuthentication.getClientID(), clientId);
+        Assert.assertEquals(clientAuthentication.getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final UsernamePasswordContext up = prc.ensureSubcontext(AuthenticationContext.class)
+                .getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNotNull(up);
+        assert up != null;
+        Assert.assertEquals(up.getUsername(), clientId.getValue());
+        Assert.assertEquals(up.getPassword(), clientSecret.getValue());
+    }
+
+    @Test
+    public void testPost() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.ensureSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        assert oauth != null;
+        final ClientAuthentication clientAuthentication = oauth.getClientAuthentication();
+        assert clientAuthentication != null;
+        Assert.assertEquals(clientAuthentication.getClientID(), clientId);
+        Assert.assertEquals(clientAuthentication.getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final UsernamePasswordContext up = prc.ensureSubcontext(AuthenticationContext.class)
+                .getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNotNull(up);
+        assert up != null;
+        Assert.assertEquals(up.getUsername(), clientId.getValue());
+        Assert.assertEquals(up.getPassword(), clientSecret.getValue());
+    }
+
+    @Test
+    public void testSecretJwt() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+        
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.ensureSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        assert oauth != null;
+        final ClientAuthentication clientAuthentication = oauth.getClientAuthentication();
+        assert clientAuthentication != null;
+        Assert.assertEquals(clientAuthentication.getClientID(), clientId);
+        Assert.assertEquals(clientAuthentication.getMethod(), ClientAuthenticationMethod.CLIENT_SECRET_JWT);
+        final UsernamePasswordContext up = prc.ensureSubcontext(AuthenticationContext.class)
+                .getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNull(up);
+    }
+
+    @Test
+    public void testPrivateKeyJwt() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+
+        final OAuth2ClientAuthenticationContext oauth =
+                prc.ensureSubcontext(AuthenticationContext.class).getSubcontext(OAuth2ClientAuthenticationContext.class);
+        Assert.assertNotNull(oauth);
+        assert oauth != null;
+        final ClientAuthentication clientAuthentication = oauth.getClientAuthentication();
+        assert clientAuthentication != null;
+        Assert.assertEquals(clientAuthentication.getClientID(), clientId);
+        Assert.assertEquals(clientAuthentication.getMethod(), ClientAuthenticationMethod.PRIVATE_KEY_JWT);
+        final UsernamePasswordContext up = prc.ensureSubcontext(AuthenticationContext.class)
+                .getSubcontext(UsernamePasswordContext.class);
+        Assert.assertNull(up);
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidatorTest.java
new file mode 100644
index 00000000..db77930d
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/JWTCredentialValidatorTest.java
@@ -0,0 +1,447 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+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.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+import org.mockito.Mockito;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.ReplayCache;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.opensaml.storage.impl.StorageServiceReplayCache;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+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.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT;
+import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.impl.ValidateCredentials;
+import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.authn.impl.JWTCredentialValidator;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.profile.oauth2.config.impl.AbstractOAuth2ClientAuthenticableProfileConfiguration;
+import net.shibboleth.oidc.profile.oauth2.config.impl.DefaultOAuth2TokenConfiguration;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator;
+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.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.RelyingPartyIdLookupFunction;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.BiFunctionSupport;
+
+/**
+ * Unit tests for {@link JWTCredentialValidator}.
+ */
+public class JWTCredentialValidatorTest extends BaseAuthenticationContextTest {
+    
+    ClientID clientId;
+    Secret clientSecret;
+    
+    URI endpointUri;
+    
+    RSAPrivateKey rsaPrivateKey;
+    RSAPublicKey rsaPublicKey;
+
+    private ClaimsValidator claimsValidator;
+    private JWTCredentialValidator validator;
+    private ValidateCredentials action;
+
+    @SuppressWarnings("unchecked")
+    private Function<ProfileRequestContext, String> typeHeaderLookup = Mockito.mock(Function.class);
+
+    @BeforeClass
+    public void initKeys() throws NoSuchAlgorithmException {
+        final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+        keyGen.initialize(2048);
+        final KeyPair keyPair = keyGen.genKeyPair();
+        rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+        rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+    }
+    
+    @Override
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        super.setUp();
+        
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        try {
+            endpointUri = new URI("http://localhost");
+        } catch (final URISyntaxException e) {
+            throw new ComponentInitializationException(e);
+        }
+
+        final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
+        final MemoryStorageService storageService = new MemoryStorageService();
+        storageService.setId("mockId");
+        storageService.initialize();
+        replayCache.setStorage(storageService);
+        
+        claimsValidator =
+                constructClaimsValidator((HttpServletRequest) src.getExternalContext().getNativeRequest(), replayCache);
+        final DefaultOAuth2TokenConfiguration profile = new DefaultOAuth2TokenConfiguration();
+        profile.setClaimsValidator(claimsValidator);
+        prc.ensureSubcontext(RelyingPartyContext.class).setProfileConfig(profile);
+        
+        validator = new JWTCredentialValidator();
+        validator.setId("test");
+        validator.setSecurityParametersLookupStrategy(new ChildContextLookup<>(SecurityParametersContext.class));
+        validator.setRequiredJwtTypeHeaderLookupStrategy(typeHeaderLookup);
+        validator.initialize();
+        
+        action = new ValidateCredentials();
+        action.setValidators(Collections.singletonList(validator));
+        action.initialize();
+    }
+    
+    protected void completeSetup(final TokenRequest request, final ClientAuthenticationMethod storedMethod,
+            final boolean sameSecret) throws NoSuchAlgorithmException, JOSEException {
+
+        final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setTokenEndpointAuthMethod(storedMethod);
+        
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, clientSecret);
+        oidcContext.setClientInformation(clientInformation);
+        prc.ensureInboundMessageContext().addSubcontext(oidcContext);
+    }
+    
+    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(jwt);
+        } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+            clientAuth = new PrivateKeyJWT(jwt);
+        } else {
+            clientAuth = null;
+        }
+
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.ensureSubcontext(OAuth2ClientAuthenticationContext.class).setClientAuthentication(clientAuth);
+        
+        prc.ensureSubcontext(RelyingPartyContext.class).setRelyingPartyId(clientId.getValue());
+        
+        final AuthorizationGrant authzGrant = new AuthorizationCodeGrant(new AuthorizationCode(), null);
+        completeSetup(new TokenRequest(null, clientAuth, authzGrant, null), method, sameSecret);
+    }
+    
+    protected ClaimsValidator constructClaimsValidator(final HttpServletRequest httpRequest,
+            final ReplayCache replayCache) {
+        final ChainingJWTClaimsValidator claimsValidation = new ChainingJWTClaimsValidator();
+        final ExpiryClaimsValidator expValidator = new ExpiryClaimsValidator();
+        final IssuedAtClaimsValidator iatValidator = new IssuedAtClaimsValidator();
+        iatValidator.setRequiredRule(false);
+        final ExactMatchClaimsValidator issValidator = new ExactMatchClaimsValidator();
+        issValidator.setClaimName("iss");
+        issValidator.setValueToMatchLookupStrategy(
+                BiFunctionSupport.forFunctionOfFirstArg(new RelyingPartyIdLookupFunction()));
+        final ExactMatchClaimsValidator subValidator = new ExactMatchClaimsValidator();
+        subValidator.setClaimName("sub");
+        subValidator.setValueToMatchLookupStrategy(
+                BiFunctionSupport.forFunctionOfFirstArg(new RelyingPartyIdLookupFunction()));
+        final AudienceClaimsValidator audValidator = new AudienceClaimsValidator();
+        audValidator.setAudienceLookupStrategy((prc, claims) -> httpRequest.getRequestURL().toString());
+        final JWTIdentifierClaimsValidator jitValidator = new JWTIdentifierClaimsValidator();
+        assert replayCache != null;
+        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 {
+        return createSecretJWT(claimsSet, clientSecret, null);
+    }
+
+    protected SignedJWT createSecretJWT(final JWTClaimsSet claimsSet, final String clientSecret,
+            final String typeHeader) throws JOSEException {
+        final SignedJWT jwt;
+        if (typeHeader == null) {
+            jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
+        } else {
+            jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.HS256)
+                    .type(new JOSEObjectType(typeHeader)).build(), 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;
+    }
+
+    protected SignedJWT createPrivateKeyJWT(final JWTClaimsSet claimsSet, final String header) throws JOSEException {
+        final SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType(header)).build(), claimsSet);
+        final RSASSASigner signer = new RSASSASigner(rsaPrivateKey);
+        jwt.sign(signer);
+        return jwt;
+    }
+
+    @Test
+    public void testNoClaimsValidator() throws Exception {
+        ((AbstractOAuth2ClientAuthenticableProfileConfiguration) prc.ensureSubcontext(
+                RelyingPartyContext.class).ensureProfileConfig()).setClaimsValidator(null);
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(validClaimsSet()), false, true);
+    }
+    
+    @Test
+    public void testSecretJwt() throws JOSEException, NoSuchAlgorithmException {
+        initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, createSecretJWT(validClaimsSet()), true);
+
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(null);
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+    @Test
+    public void testSecretJwt_enforcedHeader() throws JOSEException, NoSuchAlgorithmException {
+        final String header = "enforcedTypeHeader";
+        initializeTokenRequest(ClientAuthenticationMethod.CLIENT_SECRET_JWT, createSecretJWT(validClaimsSet(),
+                clientSecret.toString(), header), true);
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(header);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+
+    @Test
+    public void testSecretJwt_missingMandatoryHeader() throws Exception {
+        final String header = "enforcedTypeHeader";
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(header);
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(validClaimsSet()), false, true);
+    }
+
+    @Test
+    public void testPrivateKeyJwt() throws JOSEException, NoSuchAlgorithmException {
+        initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT, createPrivateKeyJWT(validClaimsSet()), true);
+        
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(null);
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+    @Test
+    public void testPrivateKeyJwt_enforcedHeader() throws JOSEException, NoSuchAlgorithmException {
+        final String header = "enforcedTypeHeader";
+        initializeTokenRequest(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(validClaimsSet(), header), true);
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(header);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+    @Test
+    public void testPrivateKeyJwt_missingMandatoryHeader() throws Exception {
+        final String header = "enforcedTypeHeader";
+        Mockito.when(typeHeaderLookup.apply(Mockito.any())).thenReturn(header);
+        testFailingJwtAuth(ClientAuthenticationMethod.PRIVATE_KEY_JWT,
+                createPrivateKeyJWT(validClaimsSet()), false, true);
+    }
+
+    @Test
+    public void testInvalidSecretJwt_iatInTheFuture() throws Exception {
+        testFailingJwtAuth(ClientAuthenticationMethod.CLIENT_SECRET_JWT,
+                createSecretJWT(claimsSetWithIatInTheFuture()), false, true);
+    }
+
+    @Test
+    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_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/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidatorTest.java
new file mode 100644
index 00000000..1078dd38
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/OIDCClientInfoCredentialValidatorTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.login.LoginException;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.AuthenticationErrorContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.idp.authn.impl.ValidateCredentials;
+import net.shibboleth.idp.authn.impl.testing.BaseAuthenticationContextTest;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.authn.impl.OIDCClientInfoCredentialValidator;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.codec.StringDigester;
+import net.shibboleth.shared.codec.StringDigester.OutputFormat;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+/** Unit test for {@link OIDCClientInfoCredentialValidator}. */
+public class OIDCClientInfoCredentialValidatorTest extends BaseAuthenticationContextTest {
+
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private OIDCClientInfoCredentialValidator validator;
+    
+    private ValidateCredentials action;
+
+    @BeforeMethod public void setUp() throws ComponentInitializationException {
+        super.setUp();
+
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        
+        validator = new OIDCClientInfoCredentialValidator();
+        validator.setId("test");
+        validator.initialize();
+        
+        action = new ValidateCredentials();
+        action.setValidators(Collections.singletonList(validator));
+        
+        final Map<String,Collection<String>> mappings = new HashMap<>();
+        mappings.put("InvalidPassword", Collections.singleton(AuthnEventIds.INVALID_CREDENTIALS));
+        mappings.put(AuthnEventIds.UNKNOWN_USERNAME, Collections.singleton(AuthnEventIds.UNKNOWN_USERNAME));
+        action.setClassifiedMessages(mappings);
+
+        action.initialize();
+
+        final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        final Secret secret = new Secret("secret1234567890secret1234567890secret1234567890");
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        oidcContext.setClientInformation(clientInformation);
+        prc.ensureInboundMessageContext().addSubcontext(oidcContext);
+    }
+
+    @Test public void testMissingFlow() {
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+
+    @Test public void testMissingUser() {
+        prc.ensureSubcontext(AuthenticationContext.class).setAttemptedFlow(authenticationFlows.get(0));
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testMissingUser2() {
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.ensureSubcontext(UsernamePasswordContext.class);
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+    @Test public void testBadPassword() {
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.ensureSubcontext(UsernamePasswordContext.class).setUsername(clientId.getValue()).setPassword("foo");
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, "InvalidPassword");
+        final AuthenticationErrorContext errorCtx = ac.ensureSubcontext(AuthenticationErrorContext.class);
+        Assert.assertTrue(errorCtx.getExceptions().get(0) instanceof LoginException);
+        Assert.assertFalse(errorCtx.isClassifiedError("UnknownUsername"));
+        Assert.assertTrue(errorCtx.isClassifiedError("InvalidPassword"));
+    }
+
+    @Test public void testAuthorized() {
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.ensureSubcontext(UsernamePasswordContext.class).setUsername(clientId.getValue()).setPassword(clientSecret.getValue());
+
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+    
+    @Test public void testAuthorizedSHA2() throws NoSuchAlgorithmException {
+        final AuthenticationContext ac = prc.ensureSubcontext(AuthenticationContext.class);
+        ac.setAttemptedFlow(authenticationFlows.get(0));
+        ac.ensureSubcontext(UsernamePasswordContext.class).setUsername(clientId.getValue()).setPassword(clientSecret.getValue());
+
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        final Secret secret = new Secret("{SHA2}" + new StringDigester("SHA-256", OutputFormat.BASE64).apply(clientSecret.getValue()));
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        prc.ensureInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class).setClientInformation(clientInformation);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationResult ar = ac.getAuthenticationResult();
+        Assert.assertNotNull(ar);
+        assert ar != null;
+        Assert.assertEquals(ar.getSubject().getPrincipals(UsernamePrincipal.class).iterator()
+                .next().getName(), clientId.getValue());
+    }
+
+}
\ No newline at end of file
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationTypeTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationTypeTest.java
new file mode 100644
index 00000000..a6461038
--- /dev/null
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/authn/impl/ValidateClientAuthenticationTypeTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.oidc.authn.impl;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.Set;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.authn.impl.ValidateClientAuthenticationType;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+
+/**
+ * Unit tests for {@link ValidateClientAuthenticationType}.
+ */
+public class ValidateClientAuthenticationTypeTest {
+    
+    private ClientID clientId;
+    private Secret clientSecret;
+    
+    private ValidateClientAuthenticationType action;
+    
+    private RequestContext rc;
+    private ProfileRequestContext prc;
+    
+    private Set<ClientAuthenticationMethod> enabledMethods;
+    
+    @BeforeMethod
+    public void init() throws ComponentInitializationException {
+        clientId = new ClientID("mockId");
+        clientSecret = new Secret("secret1234567890secret1234567890secret1234567890");
+        enabledMethods = Collections.emptySet();
+        
+        action = new ValidateClientAuthenticationType();
+        action.setTokenEndpointAuthMethodsLookupStrategy(p -> {
+            return enabledMethods;
+            }
+        );
+        action.initialize();
+    }
+    
+    protected void initializeRequestCtx(final ClientAuthenticationMethod method,
+            final ClientAuthenticationMethod storedMethod) throws JOSEException, ComponentInitializationException {
+        final ClientAuthentication clientAuth;
+        if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+            clientAuth = new ClientSecretBasic(clientId, clientSecret);
+        } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+            clientAuth = new ClientSecretPost(clientId, clientSecret);
+        } else {
+            clientAuth = null;
+        }
+        rc = new RequestContextBuilder().setInboundMessage(null).buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(rc);
+        prc.addSubcontext(new AuthenticationContext()).addSubcontext(
+                new OAuth2ClientAuthenticationContext().setClientAuthentication(clientAuth));
+        
+        final OIDCMetadataContext oidcContext = new OIDCMetadataContext();
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setTokenEndpointAuthMethod(storedMethod);
+        // we're not actually validating the secret so it doesn't matter
+        final Secret secret = new Secret("WRONG1234567890secret1234567890secret1234567890");
+        final OIDCClientInformation clientInformation = 
+                new OIDCClientInformation(clientId, new Date(), metadata, secret);
+        oidcContext.setClientInformation(clientInformation);
+        prc.ensureInboundMessageContext().addSubcontext(oidcContext);
+    }
+
+    @Test
+    public void testNoAuthnContext() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
+                ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        prc.removeSubcontext(AuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+    
+    @Test
+    public void testNoneDisabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        prc.ensureSubcontext(AuthenticationContext.class).removeSubcontext(OAuth2ClientAuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, EventIds.ACCESS_DENIED);
+    }
+
+    @Test
+    public void testNoneEnabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.NONE);
+        prc.ensureSubcontext(AuthenticationContext.class).removeSubcontext(OAuth2ClientAuthenticationContext.class);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+    }
+    
+    @Test
+    public void testBasicDisabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.NONE,
+                ClientAuthenticationMethod.NONE);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_POST);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertEvent(e, EventIds.ACCESS_DENIED);
+    }
+    
+    @Test
+    public void testBasicEnabled() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
+                ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+    }
+
+    @Test
+    public void testNoMetadata() throws Exception {
+        initializeRequestCtx(ClientAuthenticationMethod.CLIENT_SECRET_BASIC,
+                ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        prc.ensureInboundMessageContext().removeSubcontext(
+                prc.ensureInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class));
+        enabledMethods = Collections.singleton(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
+        final Event e = action.execute(rc);
+        ActionTestingSupport.assertProceedEvent(e);
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list