[java-idp-plugin-oidc-rp] 01/01: JOIDCRP-29 - Support client_secret_jwt and private_key_jwt client authentication
Phil Smart
philip.smart at jisc.ac.uk
Tue Jun 20 14:22:45 UTC 2023
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch dev/JOIDCRP-29
in repository java-idp-plugin-oidc-rp.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=94e4e6a81904d2500aa6807180c3183b515868e8
commit 94e4e6a81904d2500aa6807180c3183b515868e8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Jun 19 17:18:41 2023 +0100
JOIDCRP-29 - Support client_secret_jwt and private_key_jwt client
authentication
- Cleanup security context location in the context tree
- Support client authentication using a JWT bearer token
https://shibboleth.atlassian.net/browse/JOIDCRP-29
---
...viderMetadataStringListValueLookupFunction.java | 87 ++++
...earerTokenForClientAuthenticationPredicate.java | 69 +++
...nitializeOAuth2ClientAuthenticationContext.java | 181 +-------
...izeOAuth2ClientAuthenticationMethodHandler.java | 504 +++++++++++++++++++++
...tAuthenticationConfigurationLookupFunction.java | 211 +++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 47 ++
.../oidc-relying-party-authn-beans.xml | 49 +-
.../oidc-relying-party-authn-flow.xml | 2 +-
.../authn/oidc/rp/conf/authn/oidc-rp.properties | 3 +-
.../authn/oidc/rp/impl/AbstractOIDCTest.java | 18 +-
...Auth2ClientAuthenticationMethodHandlerTest.java | 257 +++++++++++
.../OIDCRPFlowFromAuthenticationResponseTest.java | 128 ++++++
.../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java | 2 +-
...henticationConfigurationLookupFunctionTest.java | 166 +++++++
.../resources/metadata/test-provider-standard.json | 8 +-
15 files changed, 1556 insertions(+), 176 deletions(-)
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/ProviderMetadataStringListValueLookupFunction.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/ProviderMetadataStringListValueLookupFunction.java
new file mode 100644
index 0000000..2e3ae53
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/ProviderMetadataStringListValueLookupFunction.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.config.navigate;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Fetches the value for the configured key as List of {@link String}s. May be {@code null} if the value is not found
+ * or the given {@link OIDCProviderMetadata} is null.
+ */
+public class ProviderMetadataStringListValueLookupFunction implements Function<OIDCProviderMetadata, List<String>> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProviderMetadataStringListValueLookupFunction.class);
+
+ /** The key for which to fetch the value for. */
+ @Nonnull private final String keyName;
+
+ /**
+ * Constructor.
+ *
+ * @param name The key for which to fetch the value for.
+ */
+ public ProviderMetadataStringListValueLookupFunction(@ParameterName(name = "keyName") @Nonnull final String name) {
+ keyName = Constraint.isNotEmpty(name, "The key name cannot be empty");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public List<String> apply(@Nullable final OIDCProviderMetadata metadata) {
+ if (metadata == null) {
+ log.trace("No provider metadata available");
+ return null;
+ }
+ final Object value = metadata.toJSONObject().get(keyName);
+ if (value == null) {
+ log.trace("No value found for the key {}", keyName);
+ return null;
+ }
+ if (value instanceof String) {
+ return List.of((String)value);
+ }
+ if (value instanceof List) {
+ final List<?> valueAsList = (List<?>)value;
+ return Collections.unmodifiableList(valueAsList.stream()
+ .filter(Objects::nonNull)
+ .filter(String.class::isInstance)
+ .map(String.class::cast)
+ .filter(Predicate.not(String::isEmpty))
+ .collect(Collectors.toList()));
+ }
+
+ return null;
+ }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java
new file mode 100644
index 0000000..307de51
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/context/logic/JWTBearerTokenForClientAuthenticationPredicate.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.messaging.context.logic;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+
+import net.shibboleth.idp.profile.config.ProfileConfiguration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.profile.logic.messaging.AbstractRelyingPartyPredicate;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+
+/** A predicate that determines if the client authentication method chosen is a JWT type.*/
+public class JWTBearerTokenForClientAuthenticationPredicate extends AbstractRelyingPartyPredicate {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(JWTBearerTokenForClientAuthenticationPredicate.class);
+
+ @Override
+ public boolean test(@Nullable final MessageContext input) {
+
+ final ParentProfileRequestContextLookup<MessageContext> lookup = new ParentProfileRequestContextLookup<>();
+ final ProfileRequestContext prc = lookup.apply(input);
+
+ final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc instanceof OIDCAuthenticationRelyingPartyProfileConfiguration) {
+ final String authMethod =
+ ((OIDCAuthenticationRelyingPartyProfileConfiguration) pc).getTokenEndpointAuthMethod(prc);
+ final ClientAuthenticationMethod method = new ClientAuthenticationMethod(authMethod);
+ if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT) ||
+ method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+
+ return true;
+ } else {
+ log.trace("Configured client authentication method '{}' does not require a signed JWT bearer token"
+ , authMethod);
+ }
+ }
+ }
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationContext.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationContext.java
index 96d541c..2fa6e52 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationContext.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationContext.java
@@ -17,49 +17,32 @@
package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
-import java.nio.charset.StandardCharsets;
import java.util.function.Function;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.crypto.SecretKey;
+import org.opensaml.messaging.context.MessageContext;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-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 net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.RelyingPartyContext;
import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
-import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
-import net.shibboleth.oidc.security.credential.ClientSecretCredential;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
- * An {@link AbstractProfileAction action} that resolves the Client Authentication method for the chosen
- * upstream OpenID Provider (issuer) from the profile configuration.
+ * An {@link AbstractMessageHandler action} that initializes an {@link OAuth2ClientAuthenticationContext} for later use.
*
* @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
* @event {@link EventIds#INVALID_PROFILE_CTX}
- * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CONFIG}
- * @post Add the {@link ClientAuthenticationMethod} to the {@link OAuth2ClientAuthenticationContext}
+ * @post create a {@link OAuth2ClientAuthenticationContext}
*/
-public class InitializeOAuth2ClientAuthenticationContext extends AbstractProfileAction {
+public class InitializeOAuth2ClientAuthenticationContext extends AbstractMessageHandler {
/** Class logger. */
@Nonnull
@@ -69,17 +52,8 @@ public class InitializeOAuth2ClientAuthenticationContext extends AbstractProfile
* The strategy used to lookup or create the {@link OAuth2ClientAuthenticationContext}
* for storing the client authentication.
*/
- @Nonnull private Function<ProfileRequestContext, OAuth2ClientAuthenticationContext>
+ @Nonnull private Function<MessageContext, OAuth2ClientAuthenticationContext>
oauth2ClientAuthenticationContextLookupStrategy;
-
- /** The stashed OAuth2 client authentication context.*/
- @Nullable private OAuth2ClientAuthenticationContext oauth2ClientAuthenticationContext;
-
- /** Lookup function for relying party context. */
- @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
-
- /** Applicable stashed profile configuration. */
- @Nullable private OIDCAuthenticationRelyingPartyProfileConfiguration profileConfiguration;
/** Constructor.*/
@@ -88,27 +62,9 @@ public class InitializeOAuth2ClientAuthenticationContext extends AbstractProfile
// Default under the OIDC Peer Entity Context, create is true
oauth2ClientAuthenticationContextLookupStrategy =
new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class, true).compose(
- new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
- new OutboundMessageContextLookup()));
-
- relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
-
- }
-
- /**
- * Set lookup strategy for relying party context.
- *
- * @param strategy lookup strategy
- */
- public void setRelyingPartyContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- relyingPartyContextLookupStrategy =
- Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+ new ChildContextLookup<>(OIDCPeerEntityContext.class));
}
-
-
+
/**
* Set the strategy to lookup the {@link OAuth2ClientAuthenticationContext}
* from the {@link ProfileRequestContext}.
@@ -116,7 +72,7 @@ public class InitializeOAuth2ClientAuthenticationContext extends AbstractProfile
* @param strgy the strategy.
*/
public void setOAuth2ClientAuthenticationContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, OAuth2ClientAuthenticationContext> strgy) {
+ @Nonnull final Function<MessageContext, OAuth2ClientAuthenticationContext> strgy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
@@ -124,125 +80,20 @@ public class InitializeOAuth2ClientAuthenticationContext extends AbstractProfile
"OAuth2 client authentication context lookup strategy cannot be null");
}
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-
- if (!super.doPreExecute(profileRequestContext)) {
- return false;
- }
-
- oauth2ClientAuthenticationContext =
- oauth2ClientAuthenticationContextLookupStrategy.apply(profileRequestContext);
- if (oauth2ClientAuthenticationContext == null) {
- log.error("{} No OAuth2 client authentication context found or created", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
-
- final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
- if (rpCtx != null && rpCtx.getConfiguration() != null &&
- rpCtx.getProfileConfig() instanceof OIDCAuthenticationRelyingPartyProfileConfiguration) {
- profileConfiguration = (OIDCAuthenticationRelyingPartyProfileConfiguration) rpCtx.getProfileConfig();
- }
- if (profileConfiguration == null) {
- log.error("{} Profile configuration not found", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
- return false;
- }
-
- return true;
-
- }
@Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- super.doExecute(profileRequestContext);
-
- final String clientAuthMethod =
- profileConfiguration.getTokenEndpointAuthMethod(profileRequestContext);
- if (clientAuthMethod.isEmpty()) {
- log.error("{} No client authentication method found from profile configuration", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CONFIG);
- return;
- }
-
- final String clientId =
- profileConfiguration.getClientId(profileRequestContext);
- if (clientId == null) {
- log.error("{} No client_id found from profile configuration", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CONFIG);
- return;
- }
+ protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
- final ClientSecretCredential clientCredential =
- profileConfiguration.getClientCredential(profileRequestContext);
- if (clientCredential == null) {
- log.error("{} No client secret credential found from profile configuration", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CONFIG);
- return;
- }
-
- final ClientAuthentication clientAuthentication =
- constructClientAuthentication(clientId, clientAuthMethod, clientCredential);
+ final OAuth2ClientAuthenticationContext context =
+ oauth2ClientAuthenticationContextLookupStrategy.apply(messageContext);
- if (clientAuthentication == null) {
- log.error("{} No client authentication could be constructed", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CONFIG);
- return;
+ if (context == null) {
+ throw new MessageHandlerException("No OAuth2 client authentication context found or created");
}
- oauth2ClientAuthenticationContext.setClientAuthentication(clientAuthentication);
- log.debug("{} Initialized OAuth2 Client Authentication Context: Found client authentication mode "
- + "'{}' for client '{}'",getLogPrefix(), clientAuthentication.getMethod(),
- clientAuthentication.getClientID());
- }
-
- /**
- * Construct the client authentication from the given client authentication record.
- *
- * @param clientId the client_id
- * @param tokenEndpointAuthMethod the token endpoint authentication method
- * @param clientCredential the client credential
- *
- * @return the constructed client authentication
- */
- @Nullable protected ClientAuthentication constructClientAuthentication(
- @Nonnull final String clientId, @Nonnull final String tokenEndpointAuthMethod,
- @Nonnull final ClientSecretCredential clientCredential) {
-
- // TODO support JWT types
- final Secret secret = new Secret(clientCredential.getSecret());
-
- // TODO redundent check for now, as the secret can not expire. Add back?
- if (secret.expired()) {
- log.warn("{} Client secret has expired for client '{}'", getLogPrefix(), clientId);
- return null;
- }
-
- final ClientAuthenticationMethod method = new ClientAuthenticationMethod(tokenEndpointAuthMethod);
- if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
- return new ClientSecretBasic(new ClientID(clientId), secret);
- } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
- return new ClientSecretPost(new ClientID(clientId), secret);
- }
- log.warn("{}: Client authentication method '{}' not supported for client '{}'", getLogPrefix(),
- tokenEndpointAuthMethod, clientId);
- return null;
-
+ log.debug("{} Initialized OAuth2 Client Authentication Context",getLogPrefix());
}
- /**
- * Convert the encoded byte array representing the secret into a UTF-8 String.
- *
- * @param key the key to convert
- * @return the UTF-8 encoded string value of the secret.
- */
- @Nullable private String convertSecretKeyToString(@Nullable final SecretKey key) {
- if (key == null || key.getEncoded() == null) {
- return null;
- }
- return new String(key.getEncoded(),StandardCharsets.UTF_8);
- }
}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java
new file mode 100644
index 0000000..ace2306
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandler.java
@@ -0,0 +1,504 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.security.interfaces.ECPrivateKey;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSObject.State;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.auth.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 com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.credential.ClientSecretCredential;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An {@link AbstractMessageHandler action} that resolves the Client Authentication method for the chosen
+ * upstream OpenID Provider (issuer) from the profile configuration, and adds it to the
+ * {@link OAuth2ClientAuthenticationContext}.
+ *
+ * <p>If a JWT client authentication type, the security parameters context is used create a signed JWT client assertion.
+ * </p>
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CONFIG}
+ * @post Add the {@link ClientAuthenticationMethod} to the {@link OAuth2ClientAuthenticationContext}
+ */
+public class InitializeOAuth2ClientAuthenticationMethodHandler extends AbstractMessageHandler {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(InitializeOAuth2ClientAuthenticationMethodHandler.class);
+
+ /** Lookup function for parent ProfileRequestContext. */
+ @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+ = new ParentProfileRequestContextLookup<>();
+
+ /**
+ * The strategy used to lookup or create the {@link OAuth2ClientAuthenticationContext}
+ * for storing the client authentication.
+ */
+ @Nonnull private Function<MessageContext, OAuth2ClientAuthenticationContext>
+ oauth2ClientAuthenticationContextLookupStrategy;
+
+ /** The stashed OAuth2 client authentication context.*/
+ @Nullable private OAuth2ClientAuthenticationContext oauth2ClientAuthenticationContext;
+
+ /** Lookup strategy to locate the OP metadata to use.*/
+ @Nonnull private Function<MessageContext, OIDCProviderMetadataContext> providerMetadataLookupStrategy;
+
+ /** Lookup function for relying party context. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Applicable stashed profile configuration. */
+ @Nullable private OIDCAuthenticationRelyingPartyProfileConfiguration profileConfiguration;
+
+ /** Strategy used to look up the {@link SecurityParametersContext} to set the parameters for. */
+ @Nonnull private Function<MessageContext, SecurityParametersContext> securityParametersContextLookupStrategy;
+
+ /** The offset to add to the 'exp' claim time for JWT client authentication methods. Default is 30 seconds.*/
+ @Nonnull private Duration jwtBearerExpiryOffset;
+
+ /**
+ * Stashed security parameters context for JWT Bearer Token client authentication methods. Can be {@code null} if
+ * those client authentication methods are not used.
+ */
+ @Nullable private SecurityParametersContext jwtBearerClientAuthSecurityParameters;
+
+ /** The stashed provider metadata.*/
+ @Nullable private OIDCProviderMetadata providerMetadata;
+
+ /** The stashed client_secret to use if required.*/
+ @Nullable private ClientSecretCredential clientCredential;
+
+ /** The stashed client authentication method to use.*/
+ @Nullable private String clientAuthMethod;
+
+ /** The stashed client identifier for this request.*/
+ @Nullable private String clientId;
+
+
+ /** Constructor.*/
+ public InitializeOAuth2ClientAuthenticationMethodHandler() {
+ // Default under the OIDC Peer Entity Context, create is true
+ oauth2ClientAuthenticationContextLookupStrategy =
+ new ChildContextLookup<>(OAuth2ClientAuthenticationContext.class, true).compose(
+ new ChildContextLookup<>(OIDCPeerEntityContext.class));
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ securityParametersContextLookupStrategy = new ChildContextLookup<>(SecurityParametersContext.class);
+ providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
+ new ChildContextLookup<>(OIDCPeerEntityContext.class));
+ jwtBearerExpiryOffset = Duration.ofSeconds(30);
+ }
+
+ /**
+ * Set the JWT expiry time offset for appropriate client authentication methods.
+ *
+ * @param expiry the JWT 'exp' claim offset
+ */
+ public void setJwtBearerExpiryOffset(@Nonnull final Duration expiry) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ jwtBearerExpiryOffset = Constraint.isNotNull(expiry, "jwtBearerExpiryOffset can not be null");
+ }
+
+ /**
+ * Set the lookup strategy to locate the OpenID providers metadata.
+ *
+ * @param strategy the strategy.
+ */
+ public void setProviderMetadataLookupStrategy(
+ @Nonnull final Function<MessageContext, OIDCProviderMetadataContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ providerMetadataLookupStrategy =
+ Constraint.isNotNull(strategy,"Provider metadata lookup strategy can not be null");
+ }
+
+ /**
+ * Set the strategy used to look up the {@link SecurityParametersContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSecurityParametersContextLookupStrategy(
+ @Nonnull final Function<MessageContext, SecurityParametersContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ securityParametersContextLookupStrategy =
+ Constraint.isNotNull(strategy, "JWTSecurityParametersContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set lookup strategy for relying party context.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ relyingPartyContextLookupStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+
+ /**
+ * Set the strategy to lookup the {@link OAuth2ClientAuthenticationContext}
+ * from the {@link MessageContext}.
+ *
+ * @param strgy the strategy.
+ */
+ public void setOAuth2ClientAuthenticationContextLookupStrategy(
+ @Nonnull final Function<MessageContext, OAuth2ClientAuthenticationContext> strgy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+ oauth2ClientAuthenticationContextLookupStrategy = Constraint.isNotNull(strgy,
+ "OAuth2 client authentication context lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+
+ if (!super.doPreInvoke(messageContext)) {
+ return false;
+ }
+
+ oauth2ClientAuthenticationContext =
+ oauth2ClientAuthenticationContextLookupStrategy.apply(messageContext);
+ if (oauth2ClientAuthenticationContext == null) {
+ log.error("{} No OAuth2 client authentication context found or created", getLogPrefix());
+ throw new MessageHandlerException("No OAuth2 client authentication context found or created");
+ }
+
+ final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.compose(PRC_LOOKUP).apply(messageContext);
+ if (rpCtx != null && rpCtx.getConfiguration() != null &&
+ rpCtx.getProfileConfig() instanceof OIDCAuthenticationRelyingPartyProfileConfiguration) {
+ profileConfiguration = (OIDCAuthenticationRelyingPartyProfileConfiguration) rpCtx.getProfileConfig();
+ }
+ if (profileConfiguration == null) {
+ log.error("{} Profile configuration not found", getLogPrefix());
+ throw new MessageHandlerException("No OAuth2 client authentication context found or created");
+ }
+
+ // Can be null
+ jwtBearerClientAuthSecurityParameters = securityParametersContextLookupStrategy.apply(messageContext);
+
+ final OIDCProviderMetadataContext providerCtx = providerMetadataLookupStrategy.apply(messageContext);
+ if (providerCtx == null || providerCtx.getProviderInformation() == null) {
+ log.error("{} Provider metadata not found", getLogPrefix());
+ throw new MessageHandlerException("Provider metadata not found");
+ }
+ providerMetadata = providerCtx.getProviderInformation();
+
+ clientAuthMethod = profileConfiguration.getTokenEndpointAuthMethod(PRC_LOOKUP.apply(messageContext));
+ if (clientAuthMethod.isEmpty()) {
+ throw new MessageHandlerException("No client authentication method found from profile configuration");
+ }
+
+ clientId = profileConfiguration.getClientId(PRC_LOOKUP.apply(messageContext));
+ if (clientId == null) {
+ throw new MessageHandlerException("No client_id found from profile configuration");
+ }
+
+ clientCredential = profileConfiguration.getClientCredential(PRC_LOOKUP.apply(messageContext));
+
+ return true;
+
+ }
+
+ @Override
+ protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
+
+ final ClientAuthenticationMethod method = new ClientAuthenticationMethod(clientAuthMethod);
+ ClientAuthentication clientAuthentication = null;
+
+ if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) ||
+ method.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST)) {
+
+ if (clientCredential == null) {
+ throw new MessageHandlerException("No client secret credential found from profile configuration, "
+ + "can not construct client authenticaton");
+ }
+ final Secret secret = new Secret(clientCredential.getSecret());
+ // TODO redundent check for now, as the secret can not expire. Add back?
+ if (secret.expired()) {
+ log.warn("{} Client secret has expired for client '{}'", getLogPrefix(), clientId);
+ throw new MessageHandlerException("Client secret has expired");
+ }
+ if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)) {
+ clientAuthentication = new ClientSecretBasic(new ClientID(clientId), secret);
+ } else {
+ clientAuthentication = new ClientSecretPost(new ClientID(clientId), secret);
+ }
+
+ } else if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+ verifySuitableClientSecretJWTSecurityContext();
+ clientAuthentication = new ClientSecretJWT(buildClientAuthenticationJwt());
+ } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+ verifySuitablePrivateKetJWTSecurityContext();
+ clientAuthentication = new PrivateKeyJWT(buildClientAuthenticationJwt());
+ } else {
+ log.warn("{}: Client authentication method '{}' not supported for client '{}'", getLogPrefix(),
+ method, clientId);
+ }
+
+ if (clientAuthentication == null) {
+ throw new MessageHandlerException("Client authentication could be constructed");
+ }
+
+ oauth2ClientAuthenticationContext.setClientAuthentication(clientAuthentication);
+ log.debug("{} Initialized OAuth2 Client Authentication Context: Found client authentication mode "
+ + "'{}' for client '{}'",getLogPrefix(), clientAuthentication.getMethod(),
+ clientAuthentication.getClientID());
+
+ }
+
+ /**
+ * Check the populated security context is using the correct algorithm family for client_secret_jwt client
+ * authentication.
+ *
+ * @throws MessageHandlerException if the wrong algorithm family is specified in the security context
+ */
+ private void verifySuitableClientSecretJWTSecurityContext() throws MessageHandlerException {
+ if (jwtBearerClientAuthSecurityParameters == null ||
+ jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters() == null) {
+ throw new MessageHandlerException("Missing security parameters needed to build client_secret_jwt");
+ }
+ if (jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters().getSigningCredential() == null) {
+ throw new MessageHandlerException("Missing credential needed to build client_secret_jwt");
+ }
+ final Algorithm jwsAlgorithm =
+ resolveAlgorithm(jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters());
+ if (!JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
+ throw new MessageHandlerException("Trying to construct client_secret_jwt using the wrong algorithm: "
+ + jwsAlgorithm);
+ }
+ }
+
+
+ /**
+ * Check the populated security context is using the correct algorithm family for private_key_jwt client
+ * authentication.
+ *
+ * @throws MessageHandlerException if the wrong algorithm family is specified in the security context
+ */
+ private void verifySuitablePrivateKetJWTSecurityContext() throws MessageHandlerException {
+ if (jwtBearerClientAuthSecurityParameters == null ||
+ jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters() == null) {
+ throw new MessageHandlerException("Missing security parameters needed to private_key_jwt");
+ }
+ if (jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters().getSigningCredential() == null) {
+ throw new MessageHandlerException("Missing credential needed to build private_key_jwt");
+ }
+ final Algorithm jwsAlgorithm =
+ resolveAlgorithm(jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters());
+ if (!JWSAlgorithm.Family.SIGNATURE.contains(jwsAlgorithm)) {
+ throw new MessageHandlerException("Trying to construct private_key_jwt using the wrong algorithm: "
+ + jwsAlgorithm);
+ }
+ }
+
+ /**
+ * Build the claim values required for a client authentication bearer JWT.
+ *
+ * @param clientId the client identifier
+ *
+ * @return the constructed JWT claims set
+ */
+ private JWTClaimsSet buildClientAuthenticationJwtClaims() {
+ return new JWTClaimsSet.Builder()
+ .subject(clientId)
+ .issuer(clientId)
+ .audience(providerMetadata.getTokenEndpointURI().toString())
+ .jwtID(OIDCProxySupport.generateNonce(32))
+ .issueTime(Date.from(Instant.now()))
+ .expirationTime(Date.from(Instant.now().plus(jwtBearerExpiryOffset)))
+ .build();
+ }
+
+ /**
+ * Build a signed JWT bearer token for client authentication using the populated security parameters. Relies on
+ * the correct alg and credential existing in the security context ahead of time for the correct SignedJWT to be
+ * returned e.g. for either client_secret_jwt or private_key_jwt.
+ *
+ * @return a signed JWT bearer token, or {@code null} if there was an error during construction
+ */
+ @Nullable private SignedJWT buildClientAuthenticationJwt() {
+ if (jwtBearerClientAuthSecurityParameters == null ||
+ jwtBearerClientAuthSecurityParameters.getSignatureSigningParameters() == null) {
+ log.error("{} Requested client_secret_jwt client authentication, but signing parameters could not "
+ + "be found",getLogPrefix());
+ return null;
+ }
+ final JWTClaimsSet claims = buildClientAuthenticationJwtClaims();
+ final SignedJWT signed = signClaims(claims, jwtBearerClientAuthSecurityParameters);
+ if (signed == null) {
+ log.trace("Could not construct client_secret_jwt client authentication");
+ return null;
+ }
+ return signed;
+ }
+
+ /**
+ * Sign the given JWT claims set using the signing parameters from the context.
+ *
+ * @param jwtClaimSetToSign the claims to sign
+ * @param secContext the security context to determine the signing algorithm and keys
+ *
+ * @return a signed JWT or {@code null} if an error occurs.
+ */
+ @Nullable private SignedJWT signClaims(@Nonnull final JWTClaimsSet jwtClaimSetToSign,
+ @Nonnull final SecurityParametersContext secContext) {
+ try {
+ SignedJWT jwt = null;
+ final Credential credential = secContext.getSignatureSigningParameters().getSigningCredential();
+ final Algorithm jwsAlgorithm = resolveAlgorithm(secContext.getSignatureSigningParameters());
+ final JWSSigner signer = getSigner(jwsAlgorithm, credential);
+ final JWSHeader.Builder headerBuilder = new JWSHeader.Builder(new JWSAlgorithm(jwsAlgorithm.getName()))
+ .keyID(CredentialConversionUtil.resolveKid(credential));
+ headerBuilder.type(JOSEObjectType.JWT);
+ jwt = new SignedJWT(headerBuilder.build(), jwtClaimSetToSign);
+ jwt.sign(signer);
+ if (log.isDebugEnabled() && !log.isTraceEnabled()) {
+ log.debug("{} Signed JWT Bearer Token for client authentication using kid '{}'", getLogPrefix(),
+ CredentialConversionUtil.resolveKid(credential));
+ } else if (log.isTraceEnabled()) {
+ log.trace("{} Signed JWT Bearer Token for client authentication using kid '{}': {}", getLogPrefix(),
+ CredentialConversionUtil.resolveKid(credential),jwt.serialize());
+ }
+
+ if (jwt.getState() != State.SIGNED) {
+ // Should not really happen, as JOSEException should be thrown
+ log.error("{} JWT Bearer Token for client authentication was not signed", getLogPrefix());
+ return null;
+ }
+ return jwt;
+
+ } catch (final JOSEException e) {
+ log.error("{} Error signing claims set: {}", getLogPrefix(), e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Returns correct implementation of signer based on algorithm type.
+ *
+ * @param jwsAlgorithm JWS algorithm
+ * @param credential the credential to use
+ * @return signer for algorithm and private key
+ * @throws JOSEException if algorithm cannot be supported
+ */
+ private JWSSigner getSigner(final Algorithm jwsAlgorithm, final Credential credential) throws JOSEException {
+ if (JWSAlgorithm.Family.EC.contains(jwsAlgorithm)) {
+ return new ECDSASigner((ECPrivateKey) credential.getPrivateKey());
+ }
+ if (JWSAlgorithm.Family.RSA.contains(jwsAlgorithm)) {
+ return new RSASSASigner(credential.getPrivateKey());
+ }
+ if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
+ return new MACSigner(credential.getSecretKey());
+ }
+ throw new JOSEException("Unsupported algorithm " + jwsAlgorithm.getName());
+ }
+
+ /**
+ * Resolves JWS algorithm from signature signing parameters.
+ *
+ * @param params the signature signing parameters
+ * @return JWS algorithm
+ */
+ protected JWSAlgorithm resolveAlgorithm(@Nonnull final SignatureSigningParameters params) {
+
+ final JWSAlgorithm algorithm = new JWSAlgorithm(params.getSignatureAlgorithm());
+ final Credential credential = params.getSigningCredential();
+ if (credential instanceof JWKCredential && !algorithm.equals(((JWKCredential) credential).getAlgorithm())) {
+ log.debug("{} Signature signing algorithm {} differs from JWK algorithm '{}'", getLogPrefix(),
+ algorithm.getName(), ((JWKCredential) credential).getAlgorithm() != null ?
+ ((JWKCredential) credential).getAlgorithm() : "not specified");
+ }
+ log.trace("{} Algorithm resolved {}", getLogPrefix(), algorithm.getName());
+ return algorithm;
+ }
+
+ /**
+ * Convert the encoded byte array representing the secret into a UTF-8 String.
+ *
+ * @param key the key to convert
+ * @return the UTF-8 encoded string value of the secret.
+ */
+ @Nullable private String convertSecretKeyToString(@Nullable final SecretKey key) {
+ if (key == null || key.getEncoded() == null) {
+ return null;
+ }
+ return new String(key.getEncoded(),StandardCharsets.UTF_8);
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunction.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunction.java
new file mode 100644
index 0000000..cb1fb0b
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunction.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.security.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.Algorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+
+import net.shibboleth.idp.profile.config.ProfileConfiguration;
+import net.shibboleth.idp.profile.config.SecurityConfiguration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.relyingparty.RelyingPartyConfigurationResolver;
+import net.shibboleth.oidc.profile.config.JSONSecurityConfiguration;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.security.jose.SignatureSigningConfiguration;
+import net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningConfiguration;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A function that returns a {@link SignatureSigningConfiguration} list for signing client authentication JWTs.
+ *
+ * <p>The configuration list is taken from the active security configuration, but the algorithms are filtered to only
+ * allow those that are compatible with the client authentication type chosen. For example, the HMAC family
+ * of algorithms if the client_secret_jwt method is used.</p>
+ */
+public class ClientAuthenticationConfigurationLookupFunction
+ implements ContextDataLookupFunction<MessageContext, List<SignatureSigningConfiguration>> {
+
+ /** Lookup function for parent ProfileRequestContext. */
+ @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP =
+ new ParentProfileRequestContextLookup<>();
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ClientAuthenticationConfigurationLookupFunction.class);
+
+ /**
+ * Strategy used to locate the {@link RelyingPartyContext} associated with a
+ * given {@link ProfileRequestContext}.
+ */
+ @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** A resolver for default security configurations. */
+ @Nullable private RelyingPartyConfigurationResolver rpResolver;
+
+ /** Constructor. */
+ public ClientAuthenticationConfigurationLookupFunction() {
+ relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+ }
+
+ /**
+ * Set the resolver for default security configurations.
+ *
+ * @param resolver
+ * the resolver to use
+ */
+ public void setRelyingPartyConfigurationResolver(@Nullable final RelyingPartyConfigurationResolver resolver) {
+ rpResolver = resolver;
+ }
+
+ /**
+ * Set the strategy used to locate the {@link RelyingPartyContext} associated
+ * with a given {@link ProfileRequestContext}.
+ *
+ * @param strategy
+ * lookup strategy
+ */
+ public void setRelyingPartyContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+ relyingPartyContextLookupStrategy = Constraint.isNotNull(strategy,
+ "RelyingPartyContext lookup strategy cannot be null");
+ }
+
+ @Override
+ @Nullable
+ public List<SignatureSigningConfiguration> apply(@Nullable final MessageContext input) {
+
+ final List<SignatureSigningConfiguration> configs = new ArrayList<>();
+ String tokenEndpointAuthMethod = null;
+ final RelyingPartyContext rpc = relyingPartyContextLookupStrategy.apply(PRC_LOOKUP.apply(input));
+
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc != null && pc.getSecurityConfiguration(PRC_LOOKUP.apply(input)) instanceof JSONSecurityConfiguration
+ && ((JSONSecurityConfiguration) pc.getSecurityConfiguration(PRC_LOOKUP.apply(input)))
+ .getJwtSignatureSigningConfiguration() != null) {
+ configs.add(((JSONSecurityConfiguration) pc.getSecurityConfiguration(PRC_LOOKUP.apply(input)))
+ .getJwtSignatureSigningConfiguration());
+ }
+ if (pc instanceof OIDCAuthenticationRelyingPartyProfileConfiguration) {
+ tokenEndpointAuthMethod = ((OIDCAuthenticationRelyingPartyProfileConfiguration) pc)
+ .getTokenEndpointAuthMethod(PRC_LOOKUP.apply(input));
+ }
+ }
+
+ // Check for a per-profile default (relying party independent) config.
+ if (input != null && rpResolver != null) {
+ final SecurityConfiguration defaultConfig = rpResolver
+ .getDefaultSecurityConfiguration(PRC_LOOKUP.apply(input).getProfileId());
+ if (defaultConfig instanceof JSONSecurityConfiguration
+ && ((JSONSecurityConfiguration) defaultConfig).getJwtSignatureSigningConfiguration() != null) {
+ configs.add(((JSONSecurityConfiguration) defaultConfig).getJwtSignatureSigningConfiguration());
+ }
+ }
+
+ if (tokenEndpointAuthMethod == null) {
+ log.trace("Token endpoint client authentication method can not be found");
+ return Collections.emptyList();
+ }
+
+ final ClientAuthenticationMethod method = new ClientAuthenticationMethod(tokenEndpointAuthMethod);
+ final List<SignatureSigningConfiguration> configsFiltered = new ArrayList<>();
+
+ // Filter algorithms based on the type of JWT client authentication method chosen.
+ if (method.equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
+ for (final SignatureSigningConfiguration config : configs) {
+ final List<String> filteredForMethodAlgs = filterAlgorithmsAgainstFamily(JWSAlgorithm.Family.HMAC_SHA,
+ config.getSignatureAlgorithms());
+ configsFiltered.add(createSignatureSigningConfiguration(config, filteredForMethodAlgs));
+ }
+ } else if (method.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) {
+ for (final SignatureSigningConfiguration config : configs) {
+ final List<String> filteredForMethodAlgs = filterAlgorithmsAgainstFamily(JWSAlgorithm.Family.SIGNATURE,
+ config.getSignatureAlgorithms());
+ configsFiltered.add(createSignatureSigningConfiguration(config, filteredForMethodAlgs));
+ }
+ }
+
+ return configsFiltered;
+ }
+
+ /**
+ * Filter out any algorithms in the list that are not from the given algorithm family.
+ *
+ * @param algFamily the algorithm family to filter on
+ * @param algorithms the algorithms to filter
+ *
+ * @return a filtered list of algorithms
+ */
+ @Nonnull @NonnullElements @NotLive @Unmodifiable private List<String> filterAlgorithmsAgainstFamily(
+ @Nonnull final JWSAlgorithm.Family algFamily, @Nullable final List<String> algorithms) {
+
+ if (algorithms == null) {
+ return Collections.emptyList();
+ }
+
+ final List<String> filtered = algorithms.stream()
+ .filter(Objects::nonNull).filter(Predicate.not(String::isEmpty)).map(JWSAlgorithm::parse)
+ .filter(algFamily::contains).map(Algorithm::getName).collect(Collectors.toList());
+ return Collections.unmodifiableList(filtered);
+ }
+
+ /**
+ * Create a copy of the signature signing configuration given, but replace the algorithms with those input.
+ *
+ * @param signingConfig the signature signing configuration to copy
+ * @param algorithms the algorithms to add into the copied configuration
+ *
+ * @return a copied signature signing configuration with the algorithms input
+ */
+ @Nonnull private BasicSignatureSigningConfiguration createSignatureSigningConfiguration(
+ @Nonnull final SignatureSigningConfiguration signingConfig, @Nonnull final List<String> algorithms) {
+ final BasicSignatureSigningConfiguration config = new BasicSignatureSigningConfiguration();
+ config.setExcludedAlgorithms(signingConfig.getExcludedAlgorithms());
+ config.setIncludedAlgorithms(signingConfig.getIncludedAlgorithms());
+ config.setIncludeExcludePrecedence(signingConfig.getIncludeExcludePrecedence());
+ config.setSigningCredentials(signingConfig.getSigningCredentials());
+ config.setIncludeMerge(signingConfig.isIncludeMerge());
+ config.setExcludeMerge(signingConfig.isExcludeMerge());
+ config.setSignatureAlgorithms(algorithms);
+ return config;
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index e55efe6..314efaf 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -25,6 +25,16 @@
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
+ <bean id="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext) }"
+ c:createContext="true"/>
+
+ <bean id="shibboleth.ChildLookupOrCreate.SecurityParametersContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.oidc.security.jose.context.SecurityParametersContext) }"
+ c:createContext="true" />
+
<bean id="shibboleth.ChildLookup.AccessTokenResponseContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext) }" />
@@ -38,6 +48,15 @@
</constructor-arg>
</bean>
+ <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromMessageContext" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.OIDCPeerEntityContext" />
+ </constructor-arg>
+ </bean>
+
<bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromInbound" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
<ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
@@ -46,6 +65,24 @@
<ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromInbound" />
</constructor-arg>
</bean>
+
+ <bean id="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.OIDCPeerEntityContext" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookupOrCreate.SecurityParametersContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer" />
+ </constructor-arg>
+ </bean>
<bean id="shibboleth.ChildLookup.ProviderMetadataFromProviderContext"
class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
@@ -127,6 +164,16 @@
<ref bean="shibboleth.MessageContextLookup.Outbound" />
</constructor-arg>
</bean>
+
+ <bean id="shibboleth.ChildLookupOrCreate.SecurityParametersContextInAccessTokenResponseContext"
+ parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookupOrCreate.SecurityParametersContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.AccessTokenResponseContext" />
+ </constructor-arg>
+ </bean>
<bean id="shibboleth.MessageLookup.AuthenticationResponse"
class="org.opensaml.messaging.context.navigate.MessageLookup"
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
index 05042c1..4d7b50e 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
@@ -370,9 +370,52 @@
<!-- CODE flow beans -->
- <bean id="InitializeOAuth2ClientAuthenticationContext" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.InitializeOAuth2ClientAuthenticationContext"
- p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+ <bean id="InitializeOAuth2ClientAuthenticationContextHandler" parent="NestedWebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="OUTBOUND">
+ <constructor-arg>
+ <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+ <property name="handlers">
+ <list>
+ <bean id="InitializeOAuth2ClientAuthenticationContext" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.InitializeOAuth2ClientAuthenticationContext"
+ p:oAuth2ClientAuthenticationContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer"/>
+ <bean id="PopulateJWTClientAuthenticationSignatureSigningParameters" scope="prototype"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParametersHandler"
+ p:noResultIsError="true"
+ p:securityParametersContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext"
+ p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromMessageContext">
+ <!-- Is only required for client_secret_jwt and private_key_jwt -->
+ <property name="activationCondition">
+ <bean id="JWTBearerTokenForClientAuthenticationCondition"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.context.logic.JWTBearerTokenForClientAuthenticationPredicate"/>
+ </property>
+ <property name="configurationLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.authn.oidc.rp.security.impl.ClientAuthenticationConfigurationLookupFunction"
+ scope="prototype" />
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.RelyingPartySigningParametersResolver"
+ scope="prototype">
+ <property name="providerMetadataAlgorithmLookupStrategy">
+ <bean
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.config.navigate.ProviderMetadataStringListValueLookupFunction"
+ c:keyName="token_endpoint_auth_signing_alg_values_supported" />
+ </property>
+ </bean>
+ </property>
+ </bean>
+ <bean id="InitializeOAuth2ClientAuthenticationMethodHandler" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.InitializeOAuth2ClientAuthenticationMethodHandler"
+ p:securityParametersContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext"
+ p:jwtBearerExpiryOffset="%{idp.authn.oidc.rp.client.authenticationMethod.jwt.expiryOffset:PT30S}"/>
+ </list>
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+ </property>
+ </bean>
<bean id="ExchangeCodeForAccessToken" scope="prototype"
class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ExchangeCodeForAccessToken"
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
index 5b3c8be..bbdbca8 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
@@ -111,7 +111,7 @@
</decision-state>
<action-state id="AuthorizationCodeFlow">
- <evaluate expression="InitializeOAuth2ClientAuthenticationContext" />
+ <evaluate expression="InitializeOAuth2ClientAuthenticationContextHandler" />
<evaluate expression="ExchangeCodeForAccessToken" />
<evaluate expression="ValidateOAuthAccessTokenResponse" />
<evaluate expression="PopulateIDTokenDecryptionParameters" />
diff --git a/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
index 662dd20..ef03dfd 100644
--- a/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
+++ b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/oidc-rp.properties
@@ -13,8 +13,9 @@ idp.authn.oidc.rp.client.redirecturl.allowedOrigins = https://localhost:8443
## Override the default response_mode for the given response_type
#idp.authn.oidc.rp.client.responseMode = query
-## Client authentication method. Currently client_secret_basic and client_secret_post
+## Client authentication method.
#idp.authn.oidc.rp.client.authenticationMethod = client_secret_basic
+#idp.authn.oidc.rp.client.authenticationMethod.jwt.expiryOffset = PT30S
## Comma seperated list of additional scopes e.g. profile or email. The openid scope is added by default
#idp.authn.oidc.rp.client.scopes =
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
index f39d4e8..07e1c12 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
import java.net.URI;
import java.time.Duration;
import java.util.Map;
-import java.util.Set;
import javax.annotation.Nonnull;
@@ -59,10 +58,10 @@ public abstract class AbstractOIDCTest {
private static final String REDIRECT_URI_OVERRIDE = "https://localhost/callback";
/** The client_id.*/
- private static final String CLIENT_ID = "demo_rp";
+ protected static final String CLIENT_ID = "demo_rp";
/** The client_secret.*/
- private static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
+ protected static final String CLIENT_SECRET = "Xp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$";
/** Provider configuration.*/
protected final String GOOD_PROVIDER_CONFIGURATION_INFO =
@@ -286,7 +285,7 @@ public abstract class AbstractOIDCTest {
partyConfig = new DefaultOIDCAuthorizationConfiguration();
partyContext.setProfileConfig(partyConfig);
partyConfig.setClientCredential(TestCredentialHelper.createClientSecretCredential(CLIENT_SECRET));
- partyConfig.setTokenEndpointAuthMethods(Set.of("client_secret_basic"));
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
partyConfig.setClientId(CLIENT_ID);
partyConfig.setRedirectUriOverride(REDIRECT_URI_OVERRIDE);
final RelyingPartyConfiguration rPartyConfig = new RelyingPartyConfiguration();
@@ -319,5 +318,16 @@ public abstract class AbstractOIDCTest {
return new JSONObject(tokenResponseAsMap);
}
+ /**
+ * Return the Profile configuration from the relying party context.
+ *
+ * @param prc the prc
+ * @return the relying party configuration
+ */
+ protected DefaultOIDCAuthorizationConfiguration getRelyingPartyProfileConfig(final ProfileRequestContext prc) {
+ return (DefaultOIDCAuthorizationConfiguration) prc.getSubcontext(RelyingPartyContext.class)
+ .getProfileConfig();
+ }
+
}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java
new file mode 100644
index 0000000..a98ab78
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/InitializeOAuth2ClientAuthenticationMethodHandlerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.impl;
+
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.security.credential.BasicCredential;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+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 net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+
+/** Tests for {@link InitializeOAuth2ClientAuthenticationMethodHandler}.*/
+public class InitializeOAuth2ClientAuthenticationMethodHandlerTest extends AbstractOIDCTest {
+
+ private InitializeOAuth2ClientAuthenticationMethodHandler handler;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ handler = new InitializeOAuth2ClientAuthenticationMethodHandler();
+
+ }
+
+ @Test
+ public void testInitialiseClientSecretBasic_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ final var context = prc.getOutboundMessageContext().getSubcontext(OIDCPeerEntityContext.class)
+ .getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretBasic);
+ final var basicClientAuth = (ClientSecretBasic) context.getClientAuthentication();
+ assertEquals(basicClientAuth.getClientSecret().getValue(),CLIENT_SECRET);
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretBasic_NoCredential() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_basic");
+ partyConfig.setClientCredential(null);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseUnsportedClientAuthenticationMethod() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("unsupported");
+ partyConfig.setClientCredential(null);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ }
+
+ @Test
+ public void testInitialiseClientSecretPost_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_post");
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ final var context = prc.getOutboundMessageContext().getSubcontext(OIDCPeerEntityContext.class)
+ .getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretPost);
+ final var basicClientAuth = (ClientSecretPost) context.getClientAuthentication();
+ assertEquals(basicClientAuth.getClientSecret().getValue(),CLIENT_SECRET);
+ }
+
+ @Test
+ public void testInitialiseClientSecretJWT_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ final var context = prc.getOutboundMessageContext().getSubcontext(OIDCPeerEntityContext.class)
+ .getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof ClientSecretJWT);
+ final var clientSecretJwt = (ClientSecretJWT) context.getClientAuthentication();
+ assertNotNull(clientSecretJwt);
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(clientSecretJwt.getClientAssertion());
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), CLIENT_ID);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(clientSecretJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://www.certification.openid.net/test/a/test_rp_proxy/token");
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(clientSecretJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_NoSecurityParams() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ final var context = prc.getOutboundMessageContext().getSubcontext(OIDCPeerEntityContext.class)
+ .getSubcontext(OAuth2ClientAuthenticationContext.class);
+ }
+
+ @Test
+ public void testInitialisePrivateKeyJWT_Success() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+ final RSAKey key = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ secParams.setSigningCredential(new BasicCredential(key.toPublicKey(), key.toPrivateKey()));
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ final var context = prc.getOutboundMessageContext().getSubcontext(OIDCPeerEntityContext.class)
+ .getSubcontext(OAuth2ClientAuthenticationContext.class);
+ assertNotNull(context);
+ assertNotNull(context.getClientAuthentication());
+ assertTrue(context.getClientAuthentication() instanceof PrivateKeyJWT);
+ final var privateKeyJwt = (PrivateKeyJWT) context.getClientAuthentication();
+ assertNotNull(privateKeyJwt);
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet());
+ assertNotNull(privateKeyJwt.getClientAssertion());
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getClientID().toString(), CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getSubject().toString(), CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getIssuer().toString(), CLIENT_ID);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().size(),1);
+ assertEquals(privateKeyJwt.getJWTAuthenticationClaimsSet().getAudience().get(0).toString(),
+ "https://www.certification.openid.net/test/a/test_rp_proxy/token");
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getJWTID());
+ assertNotNull(privateKeyJwt.getJWTAuthenticationClaimsSet().getExpirationTime());
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialisePrivateKeyJWT_WrongAlgorithm() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+ final RSAKey key = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ secParams.setSigningCredential(new BasicCredential(key.toPublicKey(), key.toPrivateKey()));
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_WrongAlgorithm() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+ secParams.setSigningCredential(new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential());
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialisePrivateKeyJWT_NoCredential() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("RS256");
+ secContext.setSignatureSigningParameters(secParams);
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+ }
+
+ @Test(expectedExceptions = MessageHandlerException.class)
+ public void testInitialiseClientSecretJWT_NoCredential() throws Exception {
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final SecurityParametersContext secContext = new SecurityParametersContext();
+ final SignatureSigningParameters secParams = new SignatureSigningParameters();
+ secParams.setSignatureAlgorithm("HS256");
+ secContext.setSignatureSigningParameters(secParams);
+
+ prc.getOutboundMessageContext().addSubcontext(secContext);
+
+ handler.initialize();
+ handler.invoke(prc.getOutboundMessageContext());
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
index b853ab3..c1fdfd1 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowFromAuthenticationResponseTest.java
@@ -58,6 +58,7 @@ import net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredent
import net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine;
import net.shibboleth.oidc.security.impl.support.TestCredentialHelper;
import net.shibboleth.oidc.security.jose.impl.BasicDecryptionConfiguration;
+import net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningConfiguration;
import net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationConfiguration;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
@@ -154,6 +155,133 @@ public class OIDCRPFlowFromAuthenticationResponseTest extends OIDCRPFlowTest {
assertEndUserClaimsVerified(nestedPrc);
}
+ /**
+ * Test the flow from the external authorization request to the end of the flow.
+ * Using client_secret_jwt to client authentication.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void test_IDTokenHS256_PlainUserInfo_ClientSecretJWTClientAuth() throws Exception {
+
+ basicSetup();
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ final var accessTokenResp = TestTokenHelper.createAccessTokenResponseJSON(
+ Map.of("iss", OP_ISSUER_ID, "azp", CLIENT_ID, "aud", List.of(CLIENT_ID)),
+ JWSAlgorithm.HS256, null, null, new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential(),
+ null);
+ final var userInfoResp =
+ TestTokenHelper.createPlainUserInfoResponseString(Map.of("iss", OP_ISSUER_ID, "aud", List.of(CLIENT_ID)));
+ // First is token exchange
+ queueMockServerResponse(mockOPServer, 200, accessTokenResp, "application/json");
+ // Second is plain userInfo
+ queueMockServerResponse(mockOPServer, 200, userInfoResp, "application/json");
+ mockOPServer.start(9918);
+
+ final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ .createFlowExecution(getFlowDefinition());
+
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
+
+ final DefaultOIDCAuthorizationConfiguration partyConfig = getRelyingPartyProfileConfig(prc);
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+
+ final JSONSecurityConfiguration secConfig = (JSONSecurityConfiguration)
+ partyConfig.getSecurityConfiguration(prc);
+ final BasicSignatureSigningConfiguration signingConfig = new BasicSignatureSigningConfiguration();
+ // Add more than one type of algorithm and credential, and make sure the resolver and signer picks the correctly
+ // NOTE, the client_secret credential will be added by the resolver, so you do not need to add it here.
+ // Add unused key here
+ final RSAKey encKey = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ signingConfig.setSigningCredentials(List.of(new BasicCredential(encKey.toPublicKey(), encKey.toPrivateKey())));
+ signingConfig.setSignatureAlgorithms(List.of("RS256","HS256"));
+ secConfig.setJwtSignatureSigningConfiguration(signingConfig);
+
+ updateFlowExecution(flowExecution);
+
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthnRequest");
+ resumeFlow(externalContext);
+
+ mockOPServer.shutdown();
+
+ // Assert test conditions
+
+ final var nestedPrc = assertStandardEndFlowSuccessConditions(prc);
+
+ assertIdTokenSignatureAndClaimsVerified(nestedPrc);
+ assertPlainJSONObjectUserInfoToken(nestedPrc);
+ assertEndUserClaimsVerified(nestedPrc);
+ }
+
+ /**
+ * Test the flow from the external authorization request to the end of the flow.
+ * Using private_key_jwt to client authentication.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void test_IDTokenHS256_PlainUserInfo_PrivateKeyJWTClientAuth() throws Exception {
+
+ basicSetup();
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ final var accessTokenResp = TestTokenHelper.createAccessTokenResponseJSON(
+ Map.of("iss", OP_ISSUER_ID, "azp", CLIENT_ID, "aud", List.of(CLIENT_ID)),
+ JWSAlgorithm.HS256, null, null, new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential(),
+ null);
+ final var userInfoResp =
+ TestTokenHelper.createPlainUserInfoResponseString(Map.of("iss", OP_ISSUER_ID, "aud", List.of(CLIENT_ID)));
+ // First is token exchange
+ queueMockServerResponse(mockOPServer, 200, accessTokenResp, "application/json");
+ // Second is plain userInfo
+ queueMockServerResponse(mockOPServer, 200, userInfoResp, "application/json");
+ mockOPServer.start(9918);
+
+ final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ .createFlowExecution(getFlowDefinition());
+
+ final ProfileRequestContext prc = populateBasicContextTreeFromAuthnResponse(flowExecution);
+
+ final DefaultOIDCAuthorizationConfiguration partyConfig = getRelyingPartyProfileConfig(prc);
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+
+ // Create signature signing config for client_authentication JWT method
+ final RSAKey encKey = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ final JSONSecurityConfiguration secConfig = (JSONSecurityConfiguration)
+ partyConfig.getSecurityConfiguration(prc);
+ final BasicSignatureSigningConfiguration signingConfig = new BasicSignatureSigningConfiguration();
+ signingConfig.setSigningCredentials(List.of(new BasicCredential(encKey.toPublicKey(), encKey.toPrivateKey())));
+ // Ensure it chooses the RS algorithm here.
+ signingConfig.setSignatureAlgorithms(List.of("RS256","HS256"));
+ secConfig.setJwtSignatureSigningConfiguration(signingConfig);
+
+ updateFlowExecution(flowExecution);
+
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthnRequest");
+ resumeFlow(externalContext);
+
+ mockOPServer.shutdown();
+
+ // Assert test conditions
+
+ final var nestedPrc = assertStandardEndFlowSuccessConditions(prc);
+
+ assertIdTokenSignatureAndClaimsVerified(nestedPrc);
+ assertPlainJSONObjectUserInfoToken(nestedPrc);
+ assertEndUserClaimsVerified(nestedPrc);
+ }
+
@Test
public void test_IDTokenHS256_UserInfoHS256() throws Exception {
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
index dbde6c2..4041162 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
@@ -155,7 +155,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
* mock server that is started. This OP supports the use of the request object.
*/
protected static final ClassPathResource GOOD_PROVIDER_CONFIGURATION_INFO_USE_REQUEST_OBJECT_ENCRYPT =
- new ClassPathResource("/metadata/test-provider-requestobject-encrypt.json");;
+ new ClassPathResource("/metadata/test-provider-requestobject-encrypt.json");
/**
* Example of good provider metadata. Only supports RS256 signature alg for request object.
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunctionTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunctionTest.java
new file mode 100644
index 0000000..49ffc22
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/security/impl/ClientAuthenticationConfigurationLookupFunctionTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.oidc.rp.security.impl;
+
+import static org.junit.Assert.assertNotNull;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.opensaml.security.credential.BasicCredential;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.impl.AbstractOIDCTest;
+import net.shibboleth.oidc.profile.config.JSONSecurityConfiguration;
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.oidc.security.credential.DefaultClientSecretCredential;
+import net.shibboleth.oidc.security.jose.impl.BasicSignatureSigningConfiguration;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+/** Tests for {@link ClientAuthenticationConfigurationLookupFunction}.*/
+public class ClientAuthenticationConfigurationLookupFunctionTest extends AbstractOIDCTest {
+
+ private ClientAuthenticationConfigurationLookupFunction function;
+
+ private DefaultOIDCAuthorizationConfiguration partyConfig;
+
+ private BasicSignatureSigningConfiguration signingConfig;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ function = new ClientAuthenticationConfigurationLookupFunction();
+ partyConfig = getRelyingPartyProfileConfig(prc);
+
+ // Create signature signing config for client_authentication JWT method
+ final RSAKey encKey = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+ final JSONSecurityConfiguration secConfig = new JSONSecurityConfiguration();
+ signingConfig = new BasicSignatureSigningConfiguration();
+ signingConfig.setSigningCredentials(List.of(new BasicCredential(encKey.toPublicKey(), encKey.toPrivateKey()),
+ new DefaultClientSecretCredential(CLIENT_SECRET).toSigningCredential()));
+ secConfig.setJwtSignatureSigningConfiguration(signingConfig);
+ partyConfig.setSecurityConfiguration(secConfig);
+ }
+
+ @Test
+ public void testRS256_Success() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+ signingConfig.setSignatureAlgorithms(List.of("RS256","HS256"));
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertNotNull(sigConfigs.get(0).getSigningCredentials());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 1);
+ assertEquals(sigConfigs.get(0).getSigningCredentials().size(), 2);
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().get(0),"RS256");
+ }
+
+ @Test
+ public void testRS256_NotSpecified() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("private_key_jwt");
+ signingConfig.setSignatureAlgorithms(List.of("HS256"));
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 0);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 0);
+ }
+
+ @Test
+ public void testHS256_Success() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ signingConfig.setSignatureAlgorithms(List.of("RS256","HS256"));
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSigningCredentials());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 1);
+ assertEquals(sigConfigs.get(0).getSigningCredentials().size(), 2);
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().get(0),"HS256");
+ }
+
+ @Test
+ public void testHS256_NotSpecified() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ signingConfig.setSignatureAlgorithms(List.of("RS256"));
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSigningCredentials());
+ assertEquals(sigConfigs.get(0).getSigningCredentials().size(), 2);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 0);
+ }
+
+ @Test
+ public void testNoTokenAuthenticationMethodSpecified() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethodLookupStrategy(FunctionSupport.constant(null));
+ signingConfig.setSignatureAlgorithms(List.of("RS256"));
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 0);
+ }
+
+ @Test
+ public void testNoAlgorithmsSpecified() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ signingConfig.setSignatureAlgorithms(Collections.emptyList());
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 0);
+ assertNotNull(sigConfigs.get(0).getSigningCredentials());
+ assertEquals(sigConfigs.get(0).getSigningCredentials().size(), 2);
+ }
+
+ @Test
+ public void testNullAlgorithmsSpecified() {
+ // Ensure it chooses the RS algorithm here.
+ partyConfig.setTokenEndpointAuthMethod("client_secret_jwt");
+ signingConfig.setSignatureAlgorithms(null);
+ final var sigConfigs = function.apply(prc.getOutboundMessageContext());
+ assertNotNull(sigConfigs);
+ assertEquals(sigConfigs.size(), 1);
+ assertNotNull(sigConfigs.get(0).getSignatureAlgorithms());
+ assertEquals(sigConfigs.get(0).getSignatureAlgorithms().size(), 0);
+ assertNotNull(sigConfigs.get(0).getSigningCredentials());
+ assertEquals(sigConfigs.get(0).getSigningCredentials().size(), 2);
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/resources/metadata/test-provider-standard.json b/idp-oidc-rp-impl/src/test/resources/metadata/test-provider-standard.json
index 41404ac..ff71502 100644
--- a/idp-oidc-rp-impl/src/test/resources/metadata/test-provider-standard.json
+++ b/idp-oidc-rp-impl/src/test/resources/metadata/test-provider-standard.json
@@ -29,7 +29,9 @@
],
"token_endpoint_auth_methods_supported":[
"client_secret_post",
- "client_secret_basic"
+ "client_secret_basic",
+ "private_key_jwt",
+ "client_secret_jwt"
],
"claims_supported":[
"aud",
@@ -54,5 +56,9 @@
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:jwt-bearer"
+ ],
+ "token_endpoint_auth_signing_alg_values_supported":[
+ "HS256",
+ "RS256"
]
}
\ 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