[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