[java-idp-plugin-oidc-rp] branch main updated: Add support for JWT decryption and wire up for id_token
Phil Smart
philip.smart at jisc.ac.uk
Wed Jun 1 16:42:47 UTC 2022
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
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=20879d578c7bf4399916b5684de7ad8ae6d0a406
The following commit(s) were added to refs/heads/main by this push:
new 20879d5 Add support for JWT decryption and wire up for id_token
20879d5 is described below
commit 20879d578c7bf4399916b5684de7ad8ae6d0a406
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Jun 1 17:42:41 2022 +0100
Add support for JWT decryption and wire up for id_token
---
.../JWTDecryptionConfigurationLookupFunction.java | 92 ++++++++
.../idp/plugin/authn/oidc/rp/impl/DecryptJWT.java | 176 +++++++++++++++
.../impl/IDTokenInAccessTokenUpdateStrategy.java | 80 +++++++
.../rp/impl/PopulateJWTDecryptionParameters.java | 212 ++++++++++++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 10 +-
.../oidc-relying-party-authn-beans.xml | 44 ++--
.../oidc-relying-party-authn-flow.xml | 1 +
.../idp/service/relying-party/postconfig.xml | 120 ++++++-----
.../authn/oidc/rp/conf/authn/rp-credentials.xml | 18 ++
.../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java | 239 +++++++++++++++++++--
.../test/resources/conf/authn/rp-credentials.xml | 18 ++
11 files changed, 915 insertions(+), 95 deletions(-)
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/JWTDecryptionConfigurationLookupFunction.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/JWTDecryptionConfigurationLookupFunction.java
new file mode 100644
index 0000000..daf4c06
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/JWTDecryptionConfigurationLookupFunction.java
@@ -0,0 +1,92 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+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.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+import net.shibboleth.idp.relyingparty.RelyingPartyConfigurationResolver;
+import net.shibboleth.oidc.profile.config.OIDCSecurityConfiguration;
+import net.shibboleth.oidc.security.DecryptionConfiguration;
+
+/**
+ * A function that returns a {@link DecryptionConfiguration} list for JWE decryption by way
+ * of various lookup strategies.
+ *
+ * <p>
+ * If a specific setting is unavailable, a null value is returned.
+ * </p>
+ */
+public class JWTDecryptionConfigurationLookupFunction
+ extends AbstractRelyingPartyLookupFunction<List<DecryptionConfiguration>> {
+
+ /** A resolver for default security configurations. */
+ @Nullable
+ private RelyingPartyConfigurationResolver rpResolver;
+
+ /**
+ * Set the resolver for default security configurations.
+ *
+ * @param resolver the resolver to use
+ */
+ public void setRelyingPartyConfigurationResolver(@Nullable final RelyingPartyConfigurationResolver resolver) {
+ rpResolver = resolver;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public List<DecryptionConfiguration> apply(@Nullable final ProfileRequestContext input) {
+
+ final List<DecryptionConfiguration> configs = new ArrayList<>();
+
+ final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc != null && pc.getSecurityConfiguration(input) instanceof OIDCSecurityConfiguration
+ && ((OIDCSecurityConfiguration) pc.getSecurityConfiguration(input))
+ .getIdtokenJwtDecryptionConfig() != null) {
+ configs.add(((OIDCSecurityConfiguration) pc.getSecurityConfiguration(input))
+ .getIdtokenJwtDecryptionConfig());
+ }
+ }
+
+ // Check for a per-profile default (relying party independent) config.
+ if (input != null && rpResolver != null) {
+ final SecurityConfiguration defaultConfig =
+ rpResolver.getDefaultSecurityConfiguration(input.getProfileId());
+ if (defaultConfig instanceof OIDCSecurityConfiguration
+ && ((OIDCSecurityConfiguration) defaultConfig)
+ .getIdtokenJwtDecryptionConfig() != null) {
+ configs.add(
+ ((OIDCSecurityConfiguration) defaultConfig).getIdtokenJwtDecryptionConfig());
+ }
+ }
+ // TODO: Support for Global Default configuration?
+ return configs;
+ }
+}
+
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DecryptJWT.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DecryptJWT.java
new file mode 100644
index 0000000..aae31b5
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DecryptJWT.java
@@ -0,0 +1,176 @@
+/*
+ * 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.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.xmlsec.encryption.support.DecryptionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.JWTDecryptionParameters;
+import net.shibboleth.oidc.security.context.JWTSecurityParametersContext;
+import net.shibboleth.oidc.security.impl.JWTDecrypter;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Decrypt the located JWE using the decryption parameters stored in the security context.
+ *
+ */
+public class DecryptJWT extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DecryptJWT.class);
+
+ /** Function that looks up a signed JWT token from the given message context to validate.*/
+ @NonnullAfterInit private Function<ProfileRequestContext, EncryptedJWT> jwtTokenLookupStrategy;
+
+ /**
+ * A consumer that sets the JWT produced from this decryption operation back into the profile request context.
+ */
+ @NonnullAfterInit private BiConsumer<ProfileRequestContext, JWT> jwtUpdateStrategy;
+
+ /** The extracted encrypted JWT that is to be validated.*/
+ @Nullable private EncryptedJWT encryptedJwt;
+
+ /** The decryption object. */
+ @Nullable private JWTDecrypter decrypter;
+
+ /** Strategy used to locate the {@link JWTSecurityParametersContext}. */
+ @Nonnull private Function<ProfileRequestContext, JWTSecurityParametersContext> securityParamsLookupStrategy;
+
+ /** Constructor.*/
+ public DecryptJWT() {
+ securityParamsLookupStrategy =
+ new ChildContextLookup<>(JWTSecurityParametersContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to locate the {@link JWTSecurityParametersContext} associated with a given
+ * {@link ProfileRequestContext}.
+ *
+ * @param strategy strategy used to locate the {@link JWTSecurityParametersContext} associated with a given
+ * {@link ProfileRequestContext}
+ */
+ public void setSecurityParametersContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, JWTSecurityParametersContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ securityParamsLookupStrategy =
+ Constraint.isNotNull(strategy, "SecurityParametersContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to update the profile request context with the JWT produced as a result
+ * of this decryption operation.
+ *
+ * @param strategy the strategy
+ */
+ public void setJwtUpdateStrategy(@Nonnull final BiConsumer<ProfileRequestContext, JWT> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ jwtUpdateStrategy = Constraint.isNotNull(strategy, "JWT update stategy can not be null");
+ }
+
+ /**
+ * Set the strategy used to look up a {@link EncryptedJWT encrypted JWT token}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setJwtTokenLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EncryptedJWT> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ jwtTokenLookupStrategy = Constraint.isNotNull(strategy,
+ "JwtToken lookup strategy cannot be null");
+ }
+
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (jwtTokenLookupStrategy == null) {
+ throw new ComponentInitializationException("JWTTokenLookupStrategy cannot be null");
+ }
+ if (jwtUpdateStrategy == null) {
+ throw new ComponentInitializationException("JWTUpdateStrategy cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ encryptedJwt = jwtTokenLookupStrategy.apply(profileRequestContext);
+ if (encryptedJwt == null) {
+ log.debug("{} Extracted JWT was not an EncryptedJwt, nothing to decrypt",
+ getLogPrefix());
+ return false;
+ }
+
+ final JWTSecurityParametersContext paramsCtx = securityParamsLookupStrategy.apply(profileRequestContext);
+ if (paramsCtx == null || paramsCtx.getDecryptionParameters() == null) {
+ log.debug("{} No security parameter context or decryption parameters", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+ return false;
+ } else {
+ final JWTDecryptionParameters params = paramsCtx.getDecryptionParameters();
+ decrypter = new JWTDecrypter(params);
+ }
+
+
+ return true;
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.debug("{} Decrypting encrypted JWT", getLogPrefix());
+ try {
+ final JWT decryptedJWT = decrypter.decrypt(encryptedJwt);
+ log.debug("{} JWT decrypted successfully", getLogPrefix());
+ jwtUpdateStrategy.accept(profileRequestContext, decryptedJWT);
+ } catch (final DecryptionException e) {
+ log.error("{} Unable to decrypt JWT", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ID_TOKEN);
+ return;
+ }
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/IDTokenInAccessTokenUpdateStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/IDTokenInAccessTokenUpdateStrategy.java
new file mode 100644
index 0000000..8db68b7
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/IDTokenInAccessTokenUpdateStrategy.java
@@ -0,0 +1,80 @@
+/*
+ * 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.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/** Strategy to update the id_token in the {@link AccessTokenResponseContext}.*/
+public class IDTokenInAccessTokenUpdateStrategy implements BiConsumer<ProfileRequestContext, JWT> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IDTokenInAccessTokenUpdateStrategy.class);
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set id_token on. */
+ @Nonnull private final Function<ProfileRequestContext, AccessTokenResponseContext>
+ tokenResponseContextLookupStrategy;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the strategy used look up the {@link AccessTokenResponseContext}.
+ */
+ public IDTokenInAccessTokenUpdateStrategy(@ParameterName(name="accessTokenContextLookupStrategy") final
+ Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+
+ tokenResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "accessTokenContextLookupStrategy can not be null");
+ }
+
+ /** Constructor.*/
+ public IDTokenInAccessTokenUpdateStrategy() {
+ tokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+ @Override
+ public void accept(final ProfileRequestContext profileRequestContext, final JWT idToken) {
+
+ final AccessTokenResponseContext context =
+ tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ if (context != null) {
+ context.setIdToken(idToken);
+ } else {
+ log.warn("Unable to set id_token back onto access token response context");
+ }
+
+ }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/PopulateJWTDecryptionParameters.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/PopulateJWTDecryptionParameters.java
new file mode 100644
index 0000000..e9bf192
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/PopulateJWTDecryptionParameters.java
@@ -0,0 +1,212 @@
+/*
+ * 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.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.xmlsec.EncryptionParametersResolver;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration;
+import net.shibboleth.oidc.security.DecryptionConfiguration;
+import net.shibboleth.oidc.security.JWTDecryptionParameters;
+import net.shibboleth.oidc.security.JWTDecryptionParametersResolver;
+import net.shibboleth.oidc.security.context.JWTSecurityParametersContext;
+import net.shibboleth.oidc.security.criterion.DecryptionConfigurationCriterion;
+import net.shibboleth.oidc.security.criterion.StaticCredentialCriterion;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+public class PopulateJWTDecryptionParameters extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateJWTDecryptionParameters.class);
+
+ /** Strategy used to look up the {@link SecurityParametersContext} to set the parameters for. */
+ @Nonnull
+ private Function<ProfileRequestContext,JWTSecurityParametersContext> securityParametersContextLookupStrategy;
+
+ /** Strategy used to lookup a per-request {@link DecryptionConfiguration} list. */
+ @NonnullAfterInit private Function<ProfileRequestContext,List<DecryptionConfiguration>> configurationLookupStrategy;
+
+ /** Lookup function for relying party context. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+ /** Resolver for parameters to store into context. */
+ @NonnullAfterInit private JWTDecryptionParametersResolver resolver;
+
+ /**
+ * Constructor.
+ */
+ public PopulateJWTDecryptionParameters() {
+ // Create context by default.
+ securityParametersContextLookupStrategy =
+ new ChildContextLookup<>(JWTSecurityParametersContext.class, true).compose(
+ new InboundMessageContextLookup());
+ 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");
+ }
+
+ /**
+ * Set the resolver to use for the parameters to store into the context.
+ *
+ * @param newResolver resolver to use
+ */
+ public void setDecryptionParametersResolver(@Nonnull final JWTDecryptionParametersResolver newResolver) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ resolver = Constraint.isNotNull(newResolver, "DecryptionParametersResolver cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up a per-request {@link DecryptionConfiguration} list.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setConfigurationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, List<DecryptionConfiguration>> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ configurationLookupStrategy = Constraint.isNotNull(strategy,
+ "DecryptionConfiguration lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up the {@link SecurityParametersContext} to set the parameters for.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setSecurityParametersContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, JWTSecurityParametersContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ securityParametersContextLookupStrategy = Constraint.isNotNull(strategy,
+ "SecurityParametersContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (resolver == null) {
+ throw new ComponentInitializationException("DecryptionParametersResolver cannot be null");
+ }
+ if (configurationLookupStrategy == null) {
+ throw new ComponentInitializationException("DecryptionConfiguraitonLookup cannot be null");
+ }
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.debug("{} Resolving JWT DecryptionParameters for request", getLogPrefix());
+
+ final List<DecryptionConfiguration> configs = configurationLookupStrategy.apply(profileRequestContext);
+ if (configs == null || configs.isEmpty()) {
+ log.error("{} No DecryptionConfigurations returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+ return;
+ }
+
+ final JWTSecurityParametersContext paramsCtx =
+ securityParametersContextLookupStrategy.apply(profileRequestContext);
+ if (paramsCtx == null) {
+ log.debug("{} No SecurityParametersContext returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ try {
+
+ final JWTDecryptionParameters params =
+ resolver.resolveSingle(buildCriteriaSet(profileRequestContext, configs));
+ paramsCtx.setDecryptionParameters(params);
+ log.debug("{} {} DecryptionParameters", getLogPrefix(),
+ params != null ? "Resolved" : "Failed to resolve");
+ } catch (final ResolverException e) {
+ log.error("{} Error resolving DecryptionParameters", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+ }
+
+ }
+
+ /**
+ * Build the criteria used as input to the {@link JWTDecryptionParametersResolver}.
+ *
+ * @param profileRequestContext current profile request context
+ * @param configs a list of {@link DecryptionConfiguration}s to add to the criteria set.
+ *
+ * @return the criteria set to use
+ */
+ @Nonnull
+ private CriteriaSet buildCriteriaSet(@Nonnull final ProfileRequestContext profileRequestContext,
+ final List<DecryptionConfiguration> configs) {
+
+ final CriteriaSet criteria = new CriteriaSet();
+ criteria.add(new DecryptionConfigurationCriterion(configs));
+
+ // Build a static credential criteria. Extract the decryption credential from the RP config.
+ final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);
+ if (rpCtx != null && rpCtx.getConfiguration() != null &&
+ rpCtx.getProfileConfig() instanceof OIDCAuthorizationConfiguration) {
+ final OIDCAuthorizationConfiguration profileConfiguration =
+ (OIDCAuthorizationConfiguration) rpCtx.getProfileConfig();
+
+ if (profileConfiguration != null) {
+ criteria.add(new StaticCredentialCriterion(
+ profileConfiguration.getClientCredential(profileRequestContext)));
+ } else {
+ log.warn("{} Profile configuration not available, client credential missing", getLogPrefix());
+ }
+ }
+
+ return criteria;
+
+ }
+
+}
+
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 008cf5d..72d54eb 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
@@ -9,8 +9,14 @@
default-init-method="initialize" default-destroy-method="destroy">
- <!-- System beans needed for extension to function, loaded after global.xml. The default template shows an incomplete
- example authentication flow descriptor which can be removed if not needed -->
+ <!-- System beans needed for extension to function, loaded after global.xml. -->
+
+ <bean id="shibboleth.authn.oidc.rp.JWKCredential" abstract="true"
+ class="net.shibboleth.oidc.security.impl.BasicJWKCredentialFactoryBean" />
+
+ <bean id="shibboleth.authn.oidc.rp.ExpiringJWKCredential" abstract="true"
+ class="net.shibboleth.oidc.security.impl.BasicExpiringJWTSharedSecretCredentialFactoryBean"
+ p:credentialExpiresAt="%{idp.authn.oidc.rp.client.clientSecretExpiresAt:PT0S}" />
<!-- Functions use by the flow and global beans -->
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 62a1059..0d1009a 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
@@ -252,23 +252,42 @@
<!-- ID_TOKEN Decryption -->
-
- <bean id="PopulateIDTokenDecryptionParameters" parent="NestedWebFlowProfileActionAdaptor" scope="prototype">
+
+ <bean id="PopulateIDTokenDecryptionParameters" parent="NestedWebFlowProfileActionAdaptor" scope="prototype">
<constructor-arg>
- <bean class="org.opensaml.profile.action.impl.PopulateDecryptionParameters"
+ <bean class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.PopulateJWTDecryptionParameters"
p:configurationLookupStrategy-ref="shibboleth.authn.oidc.rp.DecryptionConfigurationLookup"
- p:decryptionParametersResolver-ref="shibboleth.DecryptionParametersResolver" />
+ p:decryptionParametersResolver-ref="shibboleth.authn.oidc.rp.IDTokenDecryptionParametersResolver" />
</constructor-arg>
</bean>
-
- <!-- FIXME: this bean already exists in security-system -->
+
+ <bean id="shibboleth.authn.oidc.rp.IDTokenDecryptionParametersResolver"
+ class="net.shibboleth.oidc.security.impl.DefaultJWTDecryptionParametersResolver"/>
+
<bean id="shibboleth.authn.oidc.rp.DecryptionConfigurationLookup" lazy-init="true"
- class="net.shibboleth.idp.profile.config.navigate.DecryptionConfigurationLookupFunction"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.config.navigate.JWTDecryptionConfigurationLookupFunction"
p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
-
- <bean id="shibboleth.DecryptionParametersResolver"
- class="org.opensaml.xmlsec.impl.BasicDecryptionParametersResolver" />
-
+
+ <bean id="IDTokenInAccessTokenUpdateStrategy"
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.IDTokenInAccessTokenUpdateStrategy"/>
+
+ <bean id="DecryptJWT" parent="NestedWebFlowProfileActionAdaptor" scope="prototype">
+ <constructor-arg>
+ <bean class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.DecryptJWT">
+ <property name="jwtTokenLookupStrategy">
+ <bean class="net.shibboleth.idp.profile.context.navigate.SpringExpressionContextLookupFunction"
+ c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+ c:outputType="#{T(com.nimbusds.jwt.EncryptedJWT)}"
+ c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext)).getIdToken()" />
+ </property>
+ <property name="jwtUpdateStrategy">
+ <ref bean="IDTokenInAccessTokenUpdateStrategy"/>
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+
<!-- ID_TOKEN Signature Validation -->
<bean id="PopulateIDTokenSignatureValidationParameters" parent="NestedWebFlowProfileActionAdaptor"
@@ -296,8 +315,7 @@
<list>
<bean
class="net.shibboleth.idp.plugin.authn.oidc.rp.metadata.impl.OIDCProviderMetadataLookupHandler"
- scope="prototype"
- p:copyContextStrategy-ref="OutboundOIDCMetadataContextLookup"
+ scope="prototype" p:copyContextStrategy-ref="OutboundOIDCMetadataContextLookup"
p:providerMetadataResolver-ref="shibboleth.oidc.rp.ProviderMetadataResolver" />
<bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
scope="prototype">
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 5d45e82..367a719 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
@@ -109,6 +109,7 @@
<!-- TODO claim validation will differ per grant_type -->
<action-state id="ValidateToken">
<evaluate expression="PopulateIDTokenDecryptionParameters" />
+ <evaluate expression="DecryptJWT"/>
<evaluate expression="PopulateIDTokenSignatureValidationParameters" />
<evaluate expression="HandleIDTokenValidation" />
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index cc799db..b565388 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -23,7 +23,7 @@
p:alwaysIncludedAttributes="%{idp.authn.oidc.rp.alwaysIncludedAttributes:}" />
<!-- FIXME This will NEED a new ID and possibly class. If not, the OP plugin and RP plugin can not be installed together -->
- <!-- Only load the default client_id and client_secret if discovery is disabled -->
+ <!-- Only load the default client_id and client_secret if discovery is disabled -->
<bean id="OIDC.SSO" parent="AbstractOIDCSSOProfile" lazy-init="true"
class="net.shibboleth.oidc.profile.config.OIDCAuthorizationConfiguration"
p:httpRequestMethod="%{idp.authn.oidc.rp.httpRequestMethod:GET}"
@@ -33,84 +33,86 @@
p:deniedUserInfoAttributes="%{idp.authn.oidc.rp.deniedUserInfoAttributes:}"
p:clientId="#{%{idp.authn.oidc.rp.discoveryRequired:false} == true ? null : '%{idp.authn.oidc.rp.client.clientId:}'}"
p:clientCredential="#{%{idp.authn.oidc.rp.discoveryRequired:false} == true ? {null} : getObject('shibboleth.authn.oidc.rp.DefaultCredential')}"
- p:clientAuthenticationMethod="%{idp.authn.oidc.rp.clientAuthenticationMethod:client_secret_basic}"/>
-
+ p:clientAuthenticationMethod="%{idp.authn.oidc.rp.clientAuthenticationMethod:client_secret_basic}" />
+
<!-- Security Configuration Defaults. These settings establish the default security configurations for signatures and
loads the default credentials used. -->
<bean id="shibboleth.authn.oidc.rp.DefaultSecurityConfiguration"
class="net.shibboleth.oidc.profile.config.OIDCSecurityConfiguration">
- <!-- Add these back were appropriate -->
- <!-- <property name="signatureSigningConfiguration"> <ref bean="#{'%{idp.oidc.signing.config:shibboleth.oidc.SigningConfiguration}'.trim()}"
- /> </property> <property name="encryptionConfiguration"> <ref bean="#{'%{idp.oidc.encryption.config:shibboleth.oidc.EncryptionConfiguration}'.trim()}"
- /> </property> <property name="requestObjectDecryptionConfiguration"> <ref bean="#{'%{idp.oidc.rodecrypt.config:shibboleth.oidc.requestObjectDecryptionConfiguration}'.trim()}"
- /> </property> <property name="requestObjectSignatureValidationConfiguration"> <ref bean="#{'%{idp.oidc.rovalid.config:shibboleth.oidc.requestObjectSignatureValidationConfiguration}'.trim()}"
- /> </property> <property name="tokenEndpointJwtSignatureValidationConfiguration"> <ref bean="#{'%{idp.oidc.rovalid.config:shibboleth.oidc.tokenEndpointJwtSignatureValidationConfiguration}'.trim()}"
- /> </property> -->
- <property name="idTokenJwtSignatureValidationConfig">
- <ref bean="#{'%{idp.oidc.rp.rovalid.config:shibboleth.authn.oidc.rp.IDTokenJwtSignatureValidationConfiguration}'.trim()}"/>
- </property>
+ <property name="idTokenJwtDecryptionConfig">
+ <ref
+ bean="#{'%{dp.authn.oidc.rp.idtoken.decrypt.config:shibboleth.authn.oidc.rp.DefaultDecryptionConfiguration}'.trim()}" />
+ </property>
+ <property name="idTokenJwtSignatureValidationConfig">
+ <ref
+ bean="#{'%{dp.authn.oidc.rp.idtoken.valid.config:shibboleth.authn.oidc.rp.IDTokenJwtSignatureValidationConfiguration}'.trim()}" />
+ </property>
</bean>
+
- <!-- For developers to override per RP config -->
- <bean id="shibboleth.authn.oidc.rp.ExpiringJWKCredential" abstract="true"
- class="net.shibboleth.oidc.security.impl.BasicExpiringJWTStaticCredentialFactoryBean"
- p:credentialExpiresAt="%{idp.authn.oidc.rp.client.clientSecretExpiresAt:PT0S}"/>
-
- <bean id="shibboleth.authn.oidc.rp.DefaultCredential"
+
+ <bean id="shibboleth.authn.oidc.rp.DefaultCredential"
parent="shibboleth.authn.oidc.rp.ExpiringJWKCredential"
- p:secret="%{idp.authn.oidc.rp.client.clientSecret:#{null}}"
- p:keyNames="defaultPropertiesClientSecret"/>
-
-
+ p:secret="%{idp.authn.oidc.rp.client.clientSecret:#{null}}"
+ p:keyNames="defaultPropertiesClientSecret"
+ p:encMethod="%{idp.authn.oidc.rp.client.clientSecret.encMethods:A256GCM}"
+ p:alg="dir" />
+
+ <bean id="shibboleth.authn.oidc.rp.DefaultDecryptionConfiguration"
+ class="net.shibboleth.oidc.security.impl.BasicJWTDecryptionConfiguration"
+ p:KEKCredentialResolver-ref="defaultOIDCRPKeyEncryptionCredentialResolver"
+ p:contentEncryptionKeyCredentialResolver-ref="defaultOIDCRPContentEncryptionKeyCredentialResolver"/>
+
+ <!-- A resolver to public/private keys global to the RP -->
+ <bean id="defaultOIDCRPKeyEncryptionCredentialResolver"
+ class="org.opensaml.security.credential.impl.ChainingCredentialResolver">
+ <constructor-arg>
+ <list>
+ <bean class="org.opensaml.security.credential.impl.StaticCredentialResolver"
+ c:credentials-ref="shibboleth.authn.oidc.rp.DefaultKeyEncryptionCredentials" />
+ </list>
+ </constructor-arg>
+ </bean>
- <!-- Configuration for supported algorithms for token endpoint authentication JWT signature validation. -->
- <!-- TODO This was a parent bean, but as that was not compatible with the new trust engine stuff, I moved to it's own class for now -->
- <bean id="shibboleth.authn.oidc.rp.IDTokenJwtSignatureValidationConfiguration"
+ <!-- A pre-shared Direct Encryption key e.g. a pairwise client_secret from the input criterion -->
+ <bean id="defaultOIDCRPContentEncryptionKeyCredentialResolver"
+ class="org.opensaml.security.credential.impl.ChainingCredentialResolver">
+ <constructor-arg>
+ <list>
+ <bean id="CriterionCredentialResolver"
+ class="net.shibboleth.oidc.security.impl.CriterionCredentialResolver" />
+ </list>
+ </constructor-arg>
+ </bean>
+
+ <!-- Configuration for supported algorithms for token endpoint authentication JWT signature validation. -->
+ <!-- TODO This was a parent bean, but as that was not compatible with the new trust engine stuff, I moved to it's own
+ class for now -->
+ <bean id="shibboleth.authn.oidc.rp.IDTokenJwtSignatureValidationConfiguration"
class="net.shibboleth.oidc.security.impl.BasicSignatureValidationConfiguration"
p:signatureTrustEngine-ref="ExplicitKeySignedJWTTrustEngine">
- <!-- <property name="signatureAlgorithms">
- <list>
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_256" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_384" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_RS_512" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_256" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_384" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_ES_512" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_HS_256" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_HS_384" />
- <util:constant
- static-field="net.shibboleth.oidc.jwa.support.SignatureConstants.ALGO_ID_SIGNATURE_HS_512" />
- </list>
- </property> -->
</bean>
-
- <bean id="defaultSignedJWTCredentialResolver" class="org.opensaml.security.credential.impl.ChainingCredentialResolver">
+
+ <bean id="defaultSignedJWTCredentialResolver"
+ class="org.opensaml.security.credential.impl.ChainingCredentialResolver">
<constructor-arg>
<list>
- <bean id="OIDCProviderMetadataCredentialResolver"
- class="net.shibboleth.oidc.security.impl.ProviderMetadataCredentialResolver"
- p:remoteJwkSetCache-ref="shibboleth.authn.oidc.rp.RemoteJwkSetCache"/>
- <bean id="CriterionCredentialResolver"
- class="net.shibboleth.oidc.security.impl.CriterionCredentialResolver"/>
+ <bean id="OIDCProviderMetadataCredentialResolver"
+ class="net.shibboleth.oidc.security.impl.ProviderMetadataCredentialResolver"
+ p:remoteJwkSetCache-ref="shibboleth.authn.oidc.rp.RemoteJwkSetCache" />
+ <bean id="CriterionCredentialResolver"
+ class="net.shibboleth.oidc.security.impl.CriterionCredentialResolver" />
</list>
</constructor-arg>
</bean>
-
+
<bean id="ExplicitKeySignedJWTTrustEngine"
class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine"
- c:resolver-ref="defaultSignedJWTCredentialResolver"/>
-
-
+ c:resolver-ref="defaultSignedJWTCredentialResolver" />
+
+
</beans>
diff --git a/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/rp-credentials.xml b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/rp-credentials.xml
new file mode 100644
index 0000000..7dc5a6f
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/resources/net/shibboleth/idp/plugin/authn/oidc/rp/conf/authn/rp-credentials.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!-- Your RP's default encryption (really decryption) keys, set via property file. -->
+ <util:list id="shibboleth.authn.oidc.rp.DefaultKeyEncryptionCredentials">
+ <bean parent="shibboleth.authn.oidc.rp.JWKCredential" p:failIfResourceIsNull="false"
+ p:resource="%{idp.authn.oidc.rp.client.enc.key:#{null}}" />
+ </util:list>
+
+</beans>
\ No newline at end of file
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 e5fe1d2..0b1a817 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
@@ -42,7 +42,6 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.security.credential.Credential;
import org.opensaml.security.credential.CredentialResolver;
import org.opensaml.security.credential.UsageType;
-import org.opensaml.xmlsec.DecryptionConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -53,7 +52,6 @@ import org.springframework.webflow.engine.impl.FlowExecutionImpl;
import org.springframework.webflow.execution.FlowExecution;
import org.springframework.webflow.test.MockFlowBuilderContext;
-import com.google.common.base.Enums;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
@@ -108,14 +106,16 @@ import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
import net.shibboleth.oidc.security.credential.BasicExpiringJWKCredential;
import net.shibboleth.oidc.security.credential.BasicJWKCredential;
import net.shibboleth.oidc.security.credential.JWKCredential;
-import net.shibboleth.oidc.security.impl.BasicExpiringJWTStaticCredentialFactoryBean;
+import net.shibboleth.oidc.security.credential.impl.JWKEncryptionCredentialContext;
+import net.shibboleth.oidc.security.impl.BasicJWTDecryptionConfiguration;
import net.shibboleth.oidc.security.impl.BasicSignatureValidationConfiguration;
+import net.shibboleth.oidc.security.impl.CriterionCredentialResolver;
import net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine;
import net.shibboleth.oidc.security.impl.JWSAssemblyUtils;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
-import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.collection.Pair;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
import okhttp3.mockwebserver.MockResponse;
@@ -306,6 +306,9 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
loadBeanDefinitionsFromXmlFile(builderContext,
new ClassPathResource("attribute/filter/attribute-filter-system.xml"), null);
+
+ loadBeanDefinitionsFromXmlFile(builderContext,
+ new ClassPathResource("conf/authn/rp-credentials.xml"), null);
}
/**
@@ -388,6 +391,56 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
return accessTokenSerialized;
}
+ /**
+ * Create an OAuth access token with a runtime constructed id_token. This allows the
+ * expiry to be current. The token is signed using HS256 and encrypted using a key encryption
+ * management mode.
+ *
+ * @return a serialized access token response.
+ *
+ * @throws Exception on error.
+ */
+ private Pair<String, RSAKey> createAccessTokenResponseJSONSignedAndAsymmetricEncrypted() throws Exception {
+ final var header = new JWSHeader.Builder(JWSAlgorithm.HS256)
+ .type(JOSEObjectType.JWT)
+ .build();
+ final var payload = new JWTClaimsSet.Builder()
+ .issuer(OP_ISSUER_ID)
+ .audience(List.of(CLIENT_ID,"demo_rp2"))
+ .subject("jdoe")
+ .claim("nonce", "abadnonce")
+ .claim("azp", CLIENT_ID)
+ .claim("name","jdoe")
+ .expirationTime(Date.from(Instant.now().plusSeconds(120)))
+ .build();
+ payload.getClaims().forEach((k,v) -> log.debug("{}:{}",k,v));
+ final var signedJWT = new SignedJWT(header,payload);
+ signedJWT.sign(new MACSigner(CLIENT_SECRET));
+
+
+ final RSAKey keyRecipient = new RSAKeyGenerator(2048)
+ .keyID("1")
+ .keyUse(KeyUse.ENCRYPTION)
+ .generate();
+
+ final JWEObject jweObject =
+ new JWEObject(new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJWT));
+ jweObject.encrypt(new RSAEncrypter(keyRecipient.toPublicJWK()));
+ final EncryptedJWT jwe = EncryptedJWT.parse(jweObject.serialize());
+
+ final String accessTokenSerialized = "{\n"
+ + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"token_type\": \"Bearer\",\n"
+ + " \"id_token\": \""+jwe.serialize()+"\",\n"
+ + " \"scope\": \"openid\"\n"
+ + "}";
+ log.debug("Access token: \n {}",accessTokenSerialized);
+ return new Pair<String, RSAKey>(accessTokenSerialized, keyRecipient);
+ }
+
/**
* Create a signed UserInfo response JWT.
*
@@ -640,7 +693,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
partyContext.setProfileConfig(partyConfig);
partyConfig.setClientAuthenticationMethod("client_secret_basic");
partyConfig.setClientId(CLIENT_ID);
- partyConfig.setClientCredential(createCredentialFromSharedSecret(CLIENT_SECRET));
+ partyConfig.setClientCredential(createDirectEncryptionCredentialFromSharedSecret(CLIENT_SECRET));
// Set a default security config for the profile config
final OIDCSecurityConfiguration secConfig = new OIDCSecurityConfiguration();
final BasicSignatureValidationConfiguration<SignedJWT> sigValidation =
@@ -866,34 +919,152 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
partyContext.setProfileConfig(partyConfig);
partyConfig.setClientAuthenticationMethod("client_secret_basic");
partyConfig.setClientId(CLIENT_ID);
- partyConfig.setClientCredential(createCredentialFromSharedSecret(CLIENT_SECRET));
+ partyConfig.setClientCredential(createDirectEncryptionCredentialFromSharedSecret(CLIENT_SECRET));
// Set a default security config for the profile config
final OIDCSecurityConfiguration secConfig = new OIDCSecurityConfiguration();
+
+ //Use a mocked signature config
final BasicSignatureValidationConfiguration<SignedJWT> sigValidation =
new BasicSignatureValidationConfiguration<>();
sigValidation.setSignatureTrustEngine(new ExplicitKeySignedJWTTrustEngine(
- new CredentialResolver() {
+ new CriterionCredentialResolver()));
+
+ secConfig.setIdTokenJwtSignatureValidationConfig(sigValidation);
+
+ final var decryptConfig = new BasicJWTDecryptionConfiguration();
+
+ // Use a real resolver.
+ decryptConfig.setContentEncryptionKeyCredentialResolver(new CriterionCredentialResolver());
+
+ secConfig.setIdTokenJwtDecryptionConfig(decryptConfig);
+ partyConfig.setSecurityConfiguration(secConfig);
+
+
+ final RelyingPartyConfiguration rPartyConfig = new RelyingPartyConfiguration();
+ rPartyConfig.setResponderId("http://idp.example.com/");
+ partyContext.setConfiguration(rPartyConfig);
+ nestPrc.addSubcontext(partyContext);
+
+
+ // Setup outbound context
+ final MessageContext outMsgCtx = new MessageContext();
+ outMsgCtx.setMessage(createAuthenticationRequest());
+ outMsgCtx.addSubcontext(createPeerContext());
+ outMsgCtx.addSubcontext(createResponseTypeAndModeContext());
+ nestPrc.setOutboundMessageContext(outMsgCtx);
+ outMsgCtx.getSubcontext(OIDCPeerEntityContext.class).addSubcontext(createOAuth2ClientContext(CLIENT_ID,null));
+
+ // Setup inbound context.
+ final MessageContext inMsgCtx = new MessageContext();
+ inMsgCtx.setMessage(createAuthenticationResponse());
+ nestPrc.setInboundMessageContext(inMsgCtx);
+
+ // Add prc to flow.
+ prc.getSubcontext(AuthenticationContext.class)
+ .addSubcontext(new ExternalAuthenticationContext(new ExternalAuthenticationImpl(false)));
+ flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
+
+
+ updateFlowExecution(flowExecution);
+
+ //set start view and ending event to transition on.
+ externalContext.setEventId("proceed");
+ setCurrentState("AuthRequest");
+ resumeFlow(externalContext);
+
+ mockOPServer.shutdown();
+
+ //assert success conditions
+ assertFlowExecutionEnded();
+ assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+ assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+ assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
+
+
+ }
+
+ /**
+ * Uses symmetric MAC and asymmetric encryption.
+ *
+ * @throws Exception on error.
+ */
+ @Test
+ public void testAuthnFlowFromAuthorizationCallback_Using_SignedAndAsymetricEncryptedIDToken()
+ throws Exception {
+
+ setFlowPath(FLOW);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
+
+ final Map<String,String> mockProperties = Map.of(
+ "idp.service.clientinfo.failFast","false",
+ "idp.entityID", "http://idp.example.com/",
+ "idp.authn.oidc.rp.proxyIssuer",OP_ISSUER_ID,
+ "idp.oidc.rp.redirecturl.allowedOrigins", RP_ALLOWED_ORIGINS);
+
+ setMockProperties(mockProperties);
+
+ final Pair<String, RSAKey> accessTokenAndKey = createAccessTokenResponseJSONSignedAndAsymmetricEncrypted();
+ final MockWebServer mockOPServer = createSimpleServer();
+ // First is token exchange
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(accessTokenAndKey.getFirst()));
+ // Second is userInfo
+ mockOPServer.enqueue(new MockResponse().setResponseCode(200)
+ .setHeader("content-type", "application/json")
+ .setBody(USERINFO_RESPONSE));
+ mockOPServer.start(9918);
+
+
+ final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+ .createFlowExecution(getFlowDefinition());
+ final ProfileRequestContext prc = buildProfileRequestContext("authn/OIDCRelyingParty", false,false);
+ prc.getSubcontext(AuthenticationContext.class).setAuthenticatingAuthority(OP_ISSUER_ID);
+
+ // create a nested PRC under the authentication context
+ final ProfileRequestContext nestPrc = (ProfileRequestContext)
+ prc.getSubcontext(AuthenticationContext.class).addSubcontext(new ProfileRequestContext(), true);
+
+ // Add under nest PRC
+ final RelyingPartyContext partyContext = new RelyingPartyContext();
+ final OIDCAuthorizationConfiguration partyConfig = new OIDCAuthorizationConfiguration();
+ partyContext.setProfileConfig(partyConfig);
+ partyConfig.setClientAuthenticationMethod("client_secret_basic");
+ partyConfig.setClientId(CLIENT_ID);
+ partyConfig.setClientCredential(createDirectEncryptionCredentialFromSharedSecret(CLIENT_SECRET));
+ // Set a default security config for the profile config
+ final OIDCSecurityConfiguration secConfig = new OIDCSecurityConfiguration();
+ final BasicSignatureValidationConfiguration<SignedJWT> sigValidation =
+ new BasicSignatureValidationConfiguration<>();
+ sigValidation.setSignatureTrustEngine(new ExplicitKeySignedJWTTrustEngine(
+ new CriterionCredentialResolver()));
+
+ secConfig.setIdTokenJwtSignatureValidationConfig(sigValidation);
+
+ final var decryptConfig = new BasicJWTDecryptionConfiguration();
+ decryptConfig.setKEKCredentialResolver(new CredentialResolver() {
@Override
public Credential resolveSingle(final CriteriaSet criteria) throws ResolverException {
final BasicJWKCredential jwkCredential = new BasicJWKCredential();
- jwkCredential.setAlgorithm(JWSAlgorithm.HS256);
- jwkCredential.setKid("secret_key");
- jwkCredential.setSecretKey(new SecretKeySpec(
- JWSAssemblyUtils.getSecretBytes(CLIENT_SECRET), "NONE"));
+ jwkCredential.setAlgorithm(JWEAlgorithm.RSA_OAEP_256);
+ jwkCredential.setKid(accessTokenAndKey.getSecond().getKeyID());
+ try {
+ jwkCredential.setPrivateKey(accessTokenAndKey.getSecond().toPrivateKey());
+ jwkCredential.setPublicKey(accessTokenAndKey.getSecond().toPublicKey());
+ } catch (final JOSEException e) {
+ fail();
+ }
return jwkCredential;
- }
-
+ }
@Override
public Iterable<Credential> resolve(final CriteriaSet criteria) throws ResolverException {
return List.of(resolveSingle(criteria));
}
- }));
-
- secConfig.setIdTokenJwtSignatureValidationConfig(sigValidation);
- final var decryptConfig = new OIDCDecryptionConfiguration();
- secConfig.setRequestObjectDecryptionConfiguration(null)
-
+ });
+
+ secConfig.setIdTokenJwtDecryptionConfig(decryptConfig);
partyConfig.setSecurityConfiguration(secConfig);
@@ -1027,17 +1198,43 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
}
/**
- * Create a basic {@link JWKCredential} from the given shared secret.
+ * Create a direct encryption {@link JWKCredential} from the given shared secret.
*
* @param secret the secret to convert to a {@link JWKCredential}.
*
* @return the credential
*/
- private JWKCredential createCredentialFromSharedSecret(final String secret) {
+ private JWKCredential createDirectEncryptionCredentialFromSharedSecret(final String secret) {
final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
jwkCredential.setSecretKey(new SecretKeySpec(JWSAssemblyUtils.getSecretBytes(secret), "NONE"));
jwkCredential.setCredentialExpiresAt(Duration.ZERO);
jwkCredential.setUsageType(UsageType.UNSPECIFIED);
+ jwkCredential.getCredentialContextSet().add(
+ new JWKEncryptionCredentialContext(EncryptionMethod.A256GCM));
+ jwkCredential.setKid("mockKey");
+ jwkCredential.getKeyNames().add("mockKey");
+ jwkCredential.setAlgorithm(JWEAlgorithm.DIR);
+ return jwkCredential;
+ }
+
+ /**
+ * Create a direct encryption {@link JWKCredential} from the given shared secret.
+ *
+ * @param secret the secret to convert to a {@link JWKCredential}.
+ *
+ * @return the credential
+ * @throws JOSEException
+ */
+ private JWKCredential createKeyEncryptionCredential(final RSAKey secret) throws JOSEException {
+ final BasicExpiringJWKCredential jwkCredential = new BasicExpiringJWKCredential();
+ jwkCredential.setPrivateKey(secret.toPrivateKey());
+ jwkCredential.setPublicKey(secret.toPublicKey());
+ jwkCredential.setCredentialExpiresAt(Duration.ZERO);
+ jwkCredential.setUsageType(UsageType.ENCRYPTION);
+
+ jwkCredential.setKid(secret.getKeyID());
+ jwkCredential.getKeyNames().add("mockKey");
+ jwkCredential.setAlgorithm(secret.getAlgorithm());
return jwkCredential;
}
diff --git a/idp-oidc-rp-impl/src/test/resources/conf/authn/rp-credentials.xml b/idp-oidc-rp-impl/src/test/resources/conf/authn/rp-credentials.xml
new file mode 100644
index 0000000..7dc5a6f
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/resources/conf/authn/rp-credentials.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!-- Your RP's default encryption (really decryption) keys, set via property file. -->
+ <util:list id="shibboleth.authn.oidc.rp.DefaultKeyEncryptionCredentials">
+ <bean parent="shibboleth.authn.oidc.rp.JWKCredential" p:failIfResourceIsNull="false"
+ p:resource="%{idp.authn.oidc.rp.client.enc.key:#{null}}" />
+ </util:list>
+
+</beans>
\ 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