[java-idp-oidc] branch main updated: JOIDC-161 - Support unregistered clients at authz endpoint
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Sep 1 10:10:14 UTC 2023
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=f1bf41e9669dfb1b6bfe762edabe9f6f03cefde1
The following commit(s) were added to refs/heads/main by this push:
new f1bf41e9 JOIDC-161 - Support unregistered clients at authz endpoint
f1bf41e9 is described below
commit f1bf41e9669dfb1b6bfe762edabe9f6f03cefde1
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 1 13:10:01 2023 +0300
JOIDC-161 - Support unregistered clients at authz endpoint
https://shibboleth.atlassian.net/browse/JOIDC-161
The authorize-flow can now be used as UnverifiedRelyingParty, when unregisteredClientPolicy (or its lookupStrategy) is set to the OIDC.SSO configuration.
The default policy (shibboleth.oidc.DefaultUnregisteredPolicyLookupStrategy) may be exploited, then the policy is set in the file set via idp.oidc.DefaultUnregisteredPolicyFile -property.
The policy file needs to contain policies for the following claims:
- scope (name -operator)
- redirect_uri (one_of and regexp -operators)
- response_type (one_of -operator)
- client_id (one_of and regexp -operators)
---
.../impl/ValidateClientIDAgainstPolicy.java | 158 +++++++++++++++
.../oauth2/profile/impl/ValidateRedirectURI.java | 61 ++++++
.../oauth2/profile/impl/ValidateResponseType.java | 29 ++-
...tboundAuthenticationResponseMessageContext.java | 44 +++--
.../logic/DefaultAllowedScopeLookupFunction.java | 70 +++++++
...registeredClientAllowedScopeLookupFunction.java | 77 ++++++++
.../DefaultUnregisteredClientPolicyValidator.java | 213 +++++++++++++++++++++
...egisteredClientResponseTypesLookupFunction.java | 85 ++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 65 ++++++-
.../idp/flows/oidc/authorize/authorize-beans.xml | 11 +-
.../idp/flows/oidc/authorize/authorize-flow.xml | 1 +
.../profile/impl/ValidateRedirectURITest.java | 63 ++++--
.../profile/impl/ValidateResponseTypeTest.java | 52 ++++-
.../oidc/op/profile/flow/AuthorizeFlowTest.java | 52 +++++
...steredClientAllowedScopeLookupFunctionTest.java | 79 ++++++++
...faultUnregisteredClientPolicyValidatorTest.java | 134 +++++++++++++
...teredClientResponseTypesLookupFunctionTest.java | 108 +++++++++++
.../net/shibboleth/idp/module/conf/oidc.properties | 2 +
.../shibboleth/idp/module/conf/relying-party.xml | 1 +
.../idp/module/conf/unregistered-policy.json | 14 ++
20 files changed, 1268 insertions(+), 51 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateClientIDAgainstPolicy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateClientIDAgainstPolicy.java
new file mode 100644
index 00000000..34620603
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateClientIDAgainstPolicy.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultClientIDLookupFunction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+import net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer;
+import net.shibboleth.oidc.profile.config.navigate.UnregisteredClientPolicyLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Validates a client ID against unregistered client policy via configurable policy enforcer. The validation is done
+ * only if {@link OIDCMetadataContext} is not found under the inbound message context.
+ *
+ * @since 4.0.0
+ */
+public class ValidateClientIDAgainstPolicy extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateClientIDAgainstPolicy.class);
+
+ /** Strategy used to locate the unregistered client policy configured for the profile. */
+ @NonnullAfterInit private Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>>
+ unregisteredClientPolicyLookupStrategy;
+
+ /** Strategy used to obtain the client id value for authorize/token request. */
+ @Nonnull private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+ /** Enforcer function for validating client ID against the configured policy. */
+ @Nonnull private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> unregisteredClientPolicyEnforcer;
+
+ /** OAuth2 client id. */
+ @Nullable private ClientID clientId;
+
+ /** The policies used for validating client id. */
+ @Nullable private Map<String, UnregisteredClientPolicy> policies;
+
+ /** Constructor. */
+ public ValidateClientIDAgainstPolicy() {
+ unregisteredClientPolicyLookupStrategy = new UnregisteredClientPolicyLookupFunction();
+ clientIDLookupStrategy = new DefaultClientIDLookupFunction();
+ unregisteredClientPolicyEnforcer = new DefaultMetadataPolicyEnforcer();
+ }
+
+ /**
+ * Set the strategy used to locate the client id of the request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setClientIDLookupStrategy(@Nonnull final Function<MessageContext, ClientID> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ clientIDLookupStrategy =
+ Constraint.isNotNull(strategy, "ClientIDLookupStrategy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the unregistered client policy configured for the profile.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUnregisteredClientPolicyLookupStrategy(
+ final Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>> strategy) {
+ checkSetterPreconditions();
+
+ unregisteredClientPolicyLookupStrategy = Constraint.isNotNull(strategy,
+ "Unregistered client policy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the enforcer function for validating client ID against the configured policy.
+ *
+ * @param enforcer policy enforcer
+ */
+ public void setUnregisteredClientPolicyEnforcer(
+ final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
+ checkSetterPreconditions();
+
+ unregisteredClientPolicyEnforcer = Constraint.isNotNull(enforcer, "Unregistered client policy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final OIDCMetadataContext oidcMetadataContext =
+ profileRequestContext.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class);
+ if (oidcMetadataContext != null && oidcMetadataContext.getClientInformation() != null) {
+ log.debug("{} OIDC metadata context is populated: client ID has already been validated against metadata",
+ getLogPrefix());
+ return false;
+ }
+
+ clientId = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+ if (clientId == null) {
+ log.error("{} Unable to obtain client ID", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ policies = unregisteredClientPolicyLookupStrategy.apply(profileRequestContext);
+ if (policies == null) {
+ log.debug("{} No policy defined", getLogPrefix());
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final Pair<Object, Boolean> enforcerResult =
+ unregisteredClientPolicyEnforcer.apply(clientId.getValue(), policies.get("client_id"));
+ if (enforcerResult.getSecond() && clientId.getValue().equals(enforcerResult.getFirst())) {
+ log.debug("{} The client ID {} is compliant with the policy", getLogPrefix(), clientId.getValue());
+ return;
+ }
+ log.warn("{} The client ID {} is not compliant with the policy", getLogPrefix(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
index 3954000d..c6d3b719 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURI.java
@@ -15,7 +15,9 @@
package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
import java.net.URI;
+import java.util.Map;
import java.util.Set;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -27,8 +29,13 @@ import org.slf4j.LoggerFactory;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestRedirectURILookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultValidRedirectUrisLookupFunction;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+import net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyEnforcer;
+import net.shibboleth.oidc.profile.config.navigate.UnregisteredClientPolicyLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.Pair;
import net.shibboleth.shared.logic.Constraint;
/**
@@ -48,6 +55,13 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
/** Strategy used to obtain registered redirect uris to compare if request had no redirect uri value. */
@Nonnull private Function<ProfileRequestContext, Set<URI>> registeredRedirectURIsLookupStrategy;
+ /** Strategy used to locate the unregistered client policy configured for the request. */
+ @Nonnull private Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>>
+ unregisteredClientPolicyLookupStrategy;
+
+ /** Enforcer function for validating redirect URI against the configured policy. */
+ @Nonnull private BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> unregisteredClientPolicyEnforcer;
+
/** Whether to require redirect uri value in the request also when only single value is registered. */
private boolean requireRequestedValue = true;
@@ -58,6 +72,8 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
redirectURILookupStrategy = new DefaultRequestRedirectURILookupFunction();
validRedirectURIsLookupStrategy = new DefaultValidRedirectUrisLookupFunction();
registeredRedirectURIsLookupStrategy = new DefaultValidRedirectUrisLookupFunction();
+ unregisteredClientPolicyLookupStrategy = new UnregisteredClientPolicyLookupFunction();
+ unregisteredClientPolicyEnforcer = new DefaultMetadataPolicyEnforcer();
}
/**
@@ -104,10 +120,55 @@ public class ValidateRedirectURI extends AbstractOAuthAuthorizationResponseActio
requireRequestedValue = flag;
}
+ /**
+ * Set the strategy used to locate the unregistered client policy configured for the request.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUnregisteredClientPolicyLookupStrategy(
+ final Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>> strategy) {
+ checkSetterPreconditions();
+
+ unregisteredClientPolicyLookupStrategy = Constraint.isNotNull(strategy,
+ "Unregistered client policy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the enforcer function for validating redirect URI against the configured policy.
+ *
+ * @param enforcer policy enforcer
+ */
+ public void setUnregisteredClientPolicyEnforcer(
+ final BiFunction<Object, MetadataPolicy, Pair<Object, Boolean>> enforcer) {
+ checkSetterPreconditions();
+
+ unregisteredClientPolicyEnforcer = Constraint.isNotNull(enforcer, "Unregistered client policy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
final URI requestRedirectURI = redirectURILookupStrategy.apply(profileRequestContext);
+
+ if (requestRedirectURI != null && getMetadataContext() == null) {
+ final Map<String, UnregisteredClientPolicy> policies =
+ unregisteredClientPolicyLookupStrategy.apply(profileRequestContext);
+ if (policies != null && policies.containsKey("redirect_uri")) {
+ final Pair<Object, Boolean> result =
+ unregisteredClientPolicyEnforcer.apply(requestRedirectURI.toString(),
+ policies.get("redirect_uri"));
+ if (result.getSecond() && requestRedirectURI.toString().equals(result.getFirst())) {
+ log.debug("{} Redirection URI {} accepted by the policy for unregistered clients", getLogPrefix(),
+ requestRedirectURI);
+ getOidcResponseContext().setRedirectURI(requestRedirectURI);
+ return;
+ }
+ }
+ log.warn("{} Redirection URI {} is not accepted by the policy for unregistered clients", getLogPrefix(),
+ requestRedirectURI);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+ return;
+ }
final Set<URI> redirectionURIs = validRedirectURIsLookupStrategy.apply(profileRequestContext);
if (redirectionURIs == null || redirectionURIs.isEmpty()) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseType.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseType.java
index 8cc0fd3d..21fc6523 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseType.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseType.java
@@ -25,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.nimbusds.oauth2.sdk.ResponseType;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultUnregisteredClientResponseTypesLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestResponseTypeLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultValidResponseTypesLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
@@ -46,12 +47,18 @@ public class ValidateResponseType extends AbstractOAuthAuthorizationResponseActi
/** Lookup strategy for fetching the valid response types matching the request message. */
@NonnullAfterInit private Function<ProfileRequestContext, Set<ResponseType>> validResponseTypesLookupStrategy;
+ /** Lookup strategy for fetching the valid response types for unregistered clients. */
+ @NonnullAfterInit private Function<ProfileRequestContext, Set<ResponseType>>
+ unregisteredClientValidResponseTypesLookupStrategy;
+
/**
* Constructor.
*/
public ValidateResponseType() {
requestedResponseTypeLookupStrategy = new DefaultRequestResponseTypeLookupFunction();
validResponseTypesLookupStrategy = new DefaultValidResponseTypesLookupFunction();
+ unregisteredClientValidResponseTypesLookupStrategy =
+ new DefaultUnregisteredClientResponseTypesLookupFunction();
}
/**
@@ -61,7 +68,7 @@ public class ValidateResponseType extends AbstractOAuthAuthorizationResponseActi
*/
public void setRequestedResponseTypeLookupStrategy(
@Nonnull final Function<ProfileRequestContext, ResponseType> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
requestedResponseTypeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
@@ -72,21 +79,37 @@ public class ValidateResponseType extends AbstractOAuthAuthorizationResponseActi
*/
public void setValidResponseTypesLookupStrategy(
@Nonnull final Function<ProfileRequestContext, Set<ResponseType>> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
+ checkSetterPreconditions();
validResponseTypesLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /**
+ * Set the lookup strategy for fetching the valid response types for unregistered clients.
+ *
+ * @param strategy What to set.
+ */
+ public void setUnregisteredClientValidResponseTypesLookupStrategy(@Nonnull final
+ Function<ProfileRequestContext, Set<ResponseType>> strategy) {
+ checkSetterPreconditions();
+ unregisteredClientValidResponseTypesLookupStrategy = Constraint.isNotNull(strategy,
+ "Validation strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
final Set<ResponseType> registeredTypes = getMetadataContext() != null
? getMetadataContext().getClientInformation().getMetadata().getResponseTypes()
- : null;
+ : unregisteredClientValidResponseTypesLookupStrategy.apply(profileRequestContext);
+
final ResponseType requestedType = requestedResponseTypeLookupStrategy.apply(profileRequestContext);
+
if (registeredTypes == null || registeredTypes.isEmpty() || !registeredTypes.contains(requestedType)) {
log.warn("{} The response type {} is not registered for this RP", getLogPrefix(), requestedType);
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_RESPONSE_TYPE);
}
+
final Set<ResponseType> validTypes = validResponseTypesLookupStrategy.apply(profileRequestContext);
if (validTypes == null || validTypes.isEmpty() || !validTypes.contains(requestedType)) {
log.warn("{} The response type {} is not valid for this request", getLogPrefix(), requestedType);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundAuthenticationResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundAuthenticationResponseMessageContext.java
index dbd16360..07e2e488 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundAuthenticationResponseMessageContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundAuthenticationResponseMessageContext.java
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.function.Function;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
@@ -94,13 +95,13 @@ public class InitializeOutboundAuthenticationResponseMessageContext
@Nonnull private Function<ProfileRequestContext, SAMLMetadataContext> samlMetadataCtxLookupStrategy;
/** The OIDC metadata context used as a source for the SAML metadata context. */
- private OIDCMetadataContext oidcMetadataCtx;
+ @Nullable private OIDCMetadataContext oidcMetadataCtx;
/** The relying party context used for storing the SAML metadata context. */
- private RelyingPartyContext relyingPartyCtx;
+ @Nullable private RelyingPartyContext relyingPartyCtx;
/** The default language when it has not been defined in the metadata. */
- private String defaultLanguage;
+ @Nonnull private String defaultLanguage;
/**
* Constructor.
@@ -204,26 +205,22 @@ public class InitializeOutboundAuthenticationResponseMessageContext
if (!super.doPreExecute(profileRequestContext)) {
return false;
}
-
+
oidcMetadataCtx = oidcMetadataCtxLookupStrategy.apply(profileRequestContext);
if (oidcMetadataCtx == null) {
- log.error("{} No OIDC metadata context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
- return false;
+ log.debug("{} No OIDC metadata context", getLogPrefix());
}
-
+
relyingPartyCtx = relyingPartyCtxLookupStrategy.apply(profileRequestContext);
if (relyingPartyCtx == null) {
log.error("{} No relying party context", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
-
+
return true;
}
- // Checkstyle: CyclomaticComplexity OFF
-
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -236,9 +233,24 @@ public class InitializeOutboundAuthenticationResponseMessageContext
final SAMLMetadataContext samlContext = new SAMLMetadataContext();
final EntityDescriptor entityDescriptor = new EntityDescriptorBuilder().buildObject();
+ final SPSSODescriptor spDescriptor = new SPSSODescriptorBuilder().buildObject();
+
+ if (oidcMetadataCtx != null) {
+ populateEntityDescriptor(entityDescriptor, spDescriptor);
+ }
+
+ samlContext.setEntityDescriptor(entityDescriptor);
+ samlContext.setRoleDescriptor(spDescriptor);
+
+ relyingPartyCtx.setRelyingPartyIdContextTree(samlContext);
+ }
+
+ // Checkstyle: CyclomaticComplexity OFF
+
+ protected void populateEntityDescriptor(final EntityDescriptor entityDescriptor,
+ final SPSSODescriptor spDescriptor) {
entityDescriptor.setEntityID(oidcMetadataCtx.getClientInformation().getID().getValue());
final OIDCClientMetadata oidcMetadata = oidcMetadataCtx.getClientInformation().getOIDCMetadata();
- final SPSSODescriptor spDescriptor = new SPSSODescriptorBuilder().buildObject();
final UIInfo uiInfo = new UIInfoBuilder().buildObject();
for (final LangTag tag : oidcMetadata.getLogoURIEntries().keySet()) {
final Logo logo = new LogoBuilder().buildObject();
@@ -282,12 +294,8 @@ public class InitializeOutboundAuthenticationResponseMessageContext
final Extensions extensions = new ExtensionsBuilder().buildObject();
extensions.getUnknownXMLObjects().add(uiInfo);
spDescriptor.setExtensions(extensions);
- samlContext.setEntityDescriptor(entityDescriptor);
- samlContext.setRoleDescriptor(spDescriptor);
-
- relyingPartyCtx.setRelyingPartyIdContextTree(samlContext);
}
-
+
// Checkstyle: CyclomaticComplexity ON
-
+
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAllowedScopeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAllowedScopeLookupFunction.java
new file mode 100644
index 00000000..f9d53521
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultAllowedScopeLookupFunction.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.Scope;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoScopeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default function for looking up the allowed scope. The scope is obtained from {@link OIDCMetadataContext} if found,
+ * via unregistered client policy instead.
+ *
+ * @since 3.0.0
+ */
+public class DefaultAllowedScopeLookupFunction extends AbstractInitializableComponent
+ implements Function<ProfileRequestContext, Scope> {
+
+ /** Lookup strategy for fetching the allowed scope for unregistered clients. */
+ @Nonnull private Function<ProfileRequestContext, Scope>
+ unregisteredClientAllowedScopeLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultAllowedScopeLookupFunction() {
+ unregisteredClientAllowedScopeLookupStrategy = new DefaultUnregisteredClientAllowedScopeLookupFunction();
+ }
+
+ /**
+ * Set the lookup strategy for fetching the allowed scope for unregistered clients.
+ *
+ * @param strategy What to set.
+ */
+ public void setUnregisteredClientAllowedScopeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Scope> strategy) {
+ checkSetterPreconditions();
+ unregisteredClientAllowedScopeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ @Override @Nullable
+ public Scope apply(@Nullable final ProfileRequestContext input) {
+ final OIDCMetadataContext metadataCtx = new DefaultOIDCMetadataContextLookupFunction().apply(input);
+ return metadataCtx == null ? unregisteredClientAllowedScopeLookupStrategy.apply(input) :
+ new ClientInfoScopeLookupFunction().apply(metadataCtx);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunction.java
new file mode 100644
index 00000000..c2f23003
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunction.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.Scope;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+import net.shibboleth.oidc.profile.config.navigate.UnregisteredClientPolicyLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default lookup function for fetching the allowed scope from the unregistered client policy. The allowed scope
+ * value must be set there via 'value' operator.
+ *
+ * @since 3.0.0
+ */
+public class DefaultUnregisteredClientAllowedScopeLookupFunction implements Function<ProfileRequestContext, Scope> {
+
+ /** Strategy used to locate the unregistered client policy. */
+ @NonnullAfterInit private Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>>
+ unregisteredClientPolicyLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultUnregisteredClientAllowedScopeLookupFunction() {
+ unregisteredClientPolicyLookupStrategy = new UnregisteredClientPolicyLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the unregistered client policy.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUnregisteredClientPolicyLookupStrategy(
+ final Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>> strategy) {
+ unregisteredClientPolicyLookupStrategy = Constraint.isNotNull(strategy,
+ "Unregistered client policy lookup strategy cannot be null");
+ }
+
+ @Override @Nonnull
+ public Scope apply(@Nullable final ProfileRequestContext input) {
+ final Map<String, UnregisteredClientPolicy> policies = unregisteredClientPolicyLookupStrategy.apply(input);
+ if (policies == null || policies.isEmpty()) {
+ return new Scope();
+ }
+ return Optional.ofNullable((MetadataPolicy) policies.get("scope"))
+ .map(policy -> policy.getValue())
+ .filter(value -> value != null)
+ .map(value -> value.toString())
+ .map(Scope::parse)
+ .orElse(new Scope());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidator.java
new file mode 100644
index 00000000..4737d435
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidator.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.Map;
+import java.util.Optional;
+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 net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+import net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyValidator;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Default validator for the unregistered client policies. Scope must be specified vie value -operator. Response types
+ * must be specified via one_of -operator. Client ID and redirect URIs must be specified via one_of or regexp
+ * -operators.
+ *
+ * @since 3.0.0
+ */
+public class DefaultUnregisteredClientPolicyValidator extends AbstractInitializableComponent
+ implements Predicate<Map<String, UnregisteredClientPolicy>> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUnregisteredClientPolicyValidator.class);
+
+ /** The generic syntax validator for the policy. */
+ @Nonnull private Predicate<Map<String, MetadataPolicy>> syntaxValidator;
+
+ /**
+ * Constructor.
+ */
+ public DefaultUnregisteredClientPolicyValidator() {
+ syntaxValidator = new DefaultMetadataPolicyValidator();
+ }
+
+ /**
+ * Set the generic syntax validator for the policy.
+ * @param predicate What to set.
+ */
+ public void setSyntaxValidator(@Nonnull final Predicate<Map<String, MetadataPolicy>> predicate) {
+ checkSetterPreconditions();
+ syntaxValidator = Constraint.isNotNull(predicate, "The syntax validator predicate cannot be null");
+ }
+
+ @Override
+ public boolean test(@Nullable final Map<String, UnregisteredClientPolicy> map) {
+ if (map == null || map.isEmpty() || !map.containsKey("scope") || !map.containsKey("response_type")
+ || !map.containsKey("redirect_uri") || !map.containsKey("client_id")) {
+ log.error("The map of policies is not containing all mandatory items "
+ + "(scope, response_type, redirect_uri, client_id)");
+ return false;
+ }
+ if (!syntaxValidator.test(map.entrySet().stream().collect(Collectors.toMap(
+ entry -> entry.getKey(),
+ entry -> (MetadataPolicy) entry.getValue())))) {
+ return false;
+ }
+
+ for (final String key : map.keySet()) {
+ final MetadataPolicy policy = map.get(key);
+ switch(key) {
+ case "scope":
+ if (!Optional.ofNullable(policy)
+ .filter(scopePolicy -> preChecksForMandatoryKey(scopePolicy, "scope"))
+ .filter(scopePolicy -> verifyScopePolicy(scopePolicy))
+ .isPresent()) {
+ return false;
+ }
+ break;
+ case "response_type":
+ if (!Optional.ofNullable(policy)
+ .filter(responseType -> preChecksForMandatoryKey(responseType, "response_type"))
+ .filter(responseType -> verifyResponseTypePolicy(responseType))
+ .isPresent()) {
+ return false;
+ }
+ break;
+ case "redirect_uri":
+ if (!Optional.ofNullable(policy)
+ .filter(responseType -> preChecksForMandatoryKey(responseType, "redirect_uri"))
+ .filter(responseType -> verifyRedirectUriPolicy(responseType))
+ .isPresent()) {
+ return false;
+ }
+ break;
+ case "client_id":
+ if (!Optional.ofNullable(policy)
+ .filter(responseType -> preChecksForMandatoryKey(responseType, "client_id"))
+ .filter(responseType -> verifyClientIdPolicy(responseType))
+ .isPresent()) {
+ return false;
+ }
+ break;
+ default:
+ log.warn("Ignorting unsupported item '{}' from the policy", key);
+ }
+
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the scope is specified with value -operator.
+ * @param policy The policy to be verified.
+ * @return true if verified, false otherwise.
+ */
+ protected boolean verifyScopePolicy(@Nonnull final MetadataPolicy policy) {
+ if (policy.getOneOfValues() != null || policy.getRegexp() != null || policy.getValue() == null) {
+ log.error("Only 'value' should be used together with 'scope' in the policy");
+ return false;
+ }
+ if (!(policy.getValue() instanceof String)) {
+ log.error("Only string values can be specified for 'scope' in the policy");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the response type is specified with one_of -operator.
+ * @param policy The policy to be verified.
+ * @return true if verified, false otherwise.
+ */
+ protected boolean verifyResponseTypePolicy(@Nonnull final MetadataPolicy policy) {
+ if (policy.getRegexp() != null || policy.getValue() != null || policy.getOneOfValues() == null) {
+ log.error("Only 'one_of' should be used together with 'response_type' in the policy");
+ return false;
+ }
+ if (policy.getOneOfValues().isEmpty()) {
+ log.error("No 'one_of' values specified for 'response_type' in the policy");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the redirect_uri is specified with one_of or regexp -operators.
+ * @param policy The policy to be verified.
+ * @return true if verified, false otherwise.
+ */
+ protected boolean verifyRedirectUriPolicy(@Nonnull final MetadataPolicy policy) {
+ if (policy.getValue() != null) {
+ log.error("'value' cannot be specified for 'redirect_uri' in the policy");
+ return false;
+ }
+ if ((policy.getOneOfValues() == null || policy.getOneOfValues().isEmpty()) && policy.getRegexp() == null) {
+ log.error("No 'one_of' or 'regexp' values specified for 'redirect_uri' in the policy");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the client_id is specified with one_of or regexp -operators.
+ * @param policy The policy to be verified.
+ * @return true if verified, false otherwise.
+ */
+ protected boolean verifyClientIdPolicy(@Nonnull final MetadataPolicy policy) {
+ if (policy.getValue() != null) {
+ log.error("'value' cannot be specified for 'client_id' in the policy");
+ return false;
+ }
+ if ((policy.getOneOfValues() == null || policy.getOneOfValues().isEmpty()) && policy.getRegexp() == null) {
+ log.error("No 'one_of' or 'regexp' values specified for 'client_id' in the policy");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the policy is not containing forbidden content.
+ * @param policy The policy to be verified.
+ * @param key The claim to be verified, used for logging only.
+ * @return true if verified, false otherwise.
+ */
+ protected boolean preChecksForMandatoryKey(@Nullable final MetadataPolicy policy, final String key) {
+ if (policy == null) {
+ log.error("'{}' cannot be unspecified in the policy", key);
+ return false;
+ }
+ if (policy.isEssential()) {
+ log.warn("Essential-flag is ignored for the policy item '{}'", key);
+ }
+ if (policy.getAdd() != null || policy.getDefaultValue() != null || policy.getSubsetOfValues() != null
+ || policy.getSupersetOfValues() != null) {
+ log.error("'{}': 'add', default_value', 'subset_of' or 'superset_of' are not supported for policy items",
+ key);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunction.java
new file mode 100644
index 00000000..8b223561
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunction.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.ResponseType;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+import net.shibboleth.oidc.profile.config.navigate.UnregisteredClientPolicyLookupFunction;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Default function for fetching allowed response types via unregistered client policy.
+ *
+ * @since 3.0.0
+ */
+public class DefaultUnregisteredClientResponseTypesLookupFunction extends AbstractInitializableComponent
+ implements Function<ProfileRequestContext, Set<ResponseType>> {
+
+ /** Strategy used to locate the unregistered client policy. */
+ @Nonnull private Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>>
+ unregisteredClientPolicyLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public DefaultUnregisteredClientResponseTypesLookupFunction() {
+ unregisteredClientPolicyLookupStrategy = new UnregisteredClientPolicyLookupFunction();
+ }
+
+ /**
+ * Set the strategy used to locate the unregistered client policy.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUnregisteredClientPolicyLookupStrategy(
+ final Function<ProfileRequestContext, Map<String, UnregisteredClientPolicy>> strategy) {
+ checkSetterPreconditions();
+ unregisteredClientPolicyLookupStrategy = Constraint.isNotNull(strategy,
+ "Unregistered client policy lookup strategy cannot be null");
+ }
+
+ @Override @Nonnull
+ public Set<ResponseType> apply(@Nonnull final ProfileRequestContext input) {
+ final Map<String, UnregisteredClientPolicy> policies = unregisteredClientPolicyLookupStrategy.apply(input);
+ if (policies == null || policies.isEmpty()) {
+ return Collections.emptySet();
+ }
+ return Optional.ofNullable((MetadataPolicy) policies.get("response_type"))
+ .map(policy -> policy.getOneOfValues())
+ .orElseGet(Collections::emptyList)
+ .stream()
+ .filter(object -> object != null)
+ .map(object -> object.toString())
+ .filter(string -> StringSupport.trimOrNull(string) != null)
+ .map(string -> new ResponseType(string))
+ .collect(Collectors.toSet());
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 9eb9c27d..ff84ee14 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -230,14 +230,15 @@
This is a default souurce of "allowed" scope/audience for access tokens. It's public to allow an overridden
source function to pull in the original metadata-registered values easily.
-->
- <bean id="shibboleth.oidc.DefaultAllowedScopeStrategy" parent="shibboleth.Functions.Compose">
- <constructor-arg name="g">
- <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoScopeLookupFunction"
- p:defaultScope="#{'%{idp.oauth2.defaultAllowedScope:}'.trim()}" />
- </constructor-arg>
- <constructor-arg name="f">
- <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction" />
- </constructor-arg>
+ <bean id="shibboleth.oidc.DefaultAllowedScopeStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultAllowedScopeLookupFunction">
+ <property name="unregisteredClientAllowedScopeLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultUnregisteredClientAllowedScopeLookupFunction">
+ <property name="unregisteredClientPolicyLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.UnregisteredClientPolicyLookupFunction" />
+ </property>
+ </bean>
+ </property>
</bean>
<bean id="shibboleth.oidc.DefaultAllowedAudienceStrategy" parent="shibboleth.Functions.Compose">
@@ -689,4 +690,52 @@
</property>
</bean>
+ <bean id="shibboleth.oidc.DefaultUnregisteredPolicyFilename" class="java.lang.String" factory-method="valueOf">
+ <constructor-arg value="%{idp.oidc.DefaultUnregisteredPolicyFile:}" />
+ </bean>
+
+ <bean id="shibboleth.oidc.DefaultUnregisteredPolicyLookupStrategy"
+ parent="shibboleth.oidc.UnregisteredPolicyLookupStrategy"
+ c:resource-ref="shibboleth.oidc.DefaultUnregisteredPolicyFilename"
+ c:id="DefaultUnregisteredPolicyCache" />
+
+ <bean id="shibboleth.oidc.UnregisteredBatchMetadataCacheBuilderSpec"
+ class="net.shibboleth.oidc.metadata.cache.impl.BatchMetadataCacheBuilderSpec"
+ p:parsingStrategy-ref="shibboleth.oidc.UnregisteredDefaultJSONMapParsingStrategy"
+ p:criteriaToIdentifierStrategy-ref="shibboleth.oidc.UnregisteredDefaultMetadataCriteriaToIdentifierStrategy"
+ p:sourceMetadataExpiryStrategy-ref="shibboleth.oidc.UnregisteredDefaultExpirationTimeStrategy"
+ p:identifierExtractionStrategy-ref="shibboleth.oidc.DefaultUnregisteredPolicyIdentifierExtractionStrategy"
+ p:metadataValidPredicate-ref="shibboleth.oidc.DefaultUnregisteredPolicyValidator"/>
+
+ <bean id="shibboleth.oidc.DefaultUnregisteredPolicyValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultUnregisteredClientPolicyValidator" />
+
+ <bean id="shibboleth.oidc.UnregisteredDefaultJSONMapParsingStrategy"
+ class="net.shibboleth.oidc.metadata.cache.impl.DefaultJSONMapParsingStrategy"
+ c:valueClass="net.shibboleth.oidc.metadata.policy.MetadataPolicy"/>
+
+ <bean id="shibboleth.oidc.UnregisteredDefaultMetadataCriteriaToIdentifierStrategy"
+ parent="shibboleth.Functions.Constant" c:target="#{getObject('shibboleth.oidc.DefaultUnregisteredPolicyFilename') ?: 'undefined'}" />
+
+ <bean id="shibboleth.oidc.UnregisteredDefaultExpirationTimeStrategy"
+ class="net.shibboleth.oidc.metadata.cache.impl.DefaultSourceMetadataExpirationTimeStrategy" c:duration="PT10M"/>
+
+ <bean id="shibboleth.oidc.DefaultUnregisteredPolicyIdentifierExtractionStrategy"
+ parent="shibboleth.Functions.Constant" c:target="#{getObject('shibboleth.oidc.DefaultUnregisteredPolicyFilename') ?: 'undefined'}" />
+
+ <bean id="shibboleth.oidc.UnregisteredPolicyLookupStrategyFactory"
+ class="net.shibboleth.oidc.metadata.cache.impl.MetadataPolicyLookupStrategyFactory" />
+
+ <bean id="shibboleth.oidc.UnregisteredPolicyLookupStrategy" abstract="true"
+ factory-bean="shibboleth.oidc.UnregisteredPolicyLookupStrategyFactory"
+ factory-method="buildFileLoadingMetadataPolicyResolver"
+ c:cacheSpec-ref="shibboleth.oidc.UnregisteredBatchMetadataCacheBuilderSpec"
+ c:criteriaSetLookupStrategy="#{null}"/>
+
+ <bean id="shibboleth.oidc.DefaultUnregisteredPolicy"
+ factory-bean="shibboleth.oidc.DefaultUnregisteredPolicyLookupStrategy"
+ factory-method="apply" c:_0="#{null}">
+ <!-- PRC given to the apply-method can be null as the default function doesn't exploit that -->
+ </bean>
+
</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index 2393baaf..0a82b3f1 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -69,6 +69,11 @@
</property>
</bean>
+ <bean id="ValidateClientIDAgainstPolicy"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateClientIDAgainstPolicy"
+ p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy"
+ scope="prototype" />
+
<bean id="ValidateAuthorizationRequestType"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAuthorizationRequestType"
scope="prototype"
@@ -305,7 +310,11 @@
scope="prototype" p:requireRequestedValue="true" />
<bean id="ValidateResponseType" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateResponseType"
- scope="prototype" />
+ scope="prototype">
+ <property name="unregisteredClientValidResponseTypesLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultUnregisteredClientResponseTypesLookupFunction" />
+ </property>
+ </bean>
<bean id="ValidateCodeChallenge" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateCodeChallenge"
scope="prototype" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
index 229b43d7..30fa08b2 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
@@ -54,6 +54,7 @@
</subflow-state>
<action-state id="OutboundContextsAndSecurityParameters">
+ <evaluate expression="ValidateClientIDAgainstPolicy" />
<evaluate expression="ValidateAuthorizationRequestType" />
<evaluate expression="InitializeOutboundMessageContext" />
<evaluate expression="SetRequestObjectToResponseContext" />
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURITest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURITest.java
index 1755d01c..d9946619 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURITest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateRedirectURITest.java
@@ -16,11 +16,15 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
import java.net.URI;
import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -39,10 +43,17 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
private String requestUri = "https://client.example.org/cb";
private void init() throws ComponentInitializationException, URISyntaxException {
- init(true, new URI(requestUri), null, null);
+ init(true, new URI(requestUri), null, null, null);
+ }
+
+ private void init(final boolean requireRequestedValue, final URI requestedUri, final Set<URI> validUris,
+ final Set<URI> registeredUris) throws ComponentInitializationException {
+ init(requireRequestedValue, requestedUri, validUris, registeredUris, null);
}
- private void init(final boolean requireRequestedValue, final URI requestedUri, final Set<URI> validUris, final Set<URI> registeredUris) throws ComponentInitializationException {
+ private void init(final boolean requireRequestedValue, final URI requestedUri, final Set<URI> validUris,
+ final Set<URI> registeredUris, final Map<String, UnregisteredClientPolicy> policies)
+ throws ComponentInitializationException {
action = new ValidateRedirectURI();
action.setRequireRequestedValue(requireRequestedValue);
action.setRedirectURILookupStrategy(prc -> requestedUri);
@@ -52,6 +63,9 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
if (registeredUris != null) {
action.setRegisteredRedirectURIsLookupStrategy(prc -> registeredUris);
}
+ if (policies != null) {
+ action.setUnregisteredClientPolicyLookupStrategy(prc -> policies);
+ }
action.initialize();
}
@@ -62,7 +76,7 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
* @throws URISyntaxException
*/
@Test
- public void testNoCtx() throws ComponentInitializationException, URISyntaxException {
+ public void testNoCtxNoPolicy() throws ComponentInitializationException, URISyntaxException {
init();
profileRequestCtx.getInboundMessageContext().removeSubcontext(OIDCMetadataContext.class);
final Event event = action.execute(requestCtx);
@@ -76,12 +90,14 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
* @throws URISyntaxException
*/
@Test
- public void testNoMatch() throws ComponentInitializationException, URISyntaxException {
+ public void testNoMatchViaMetadata() throws ComponentInitializationException, URISyntaxException {
init();
- OIDCMetadataContext oidcCtx = profileRequestCtx.getInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class);
+ OIDCMetadataContext oidcCtx =
+ profileRequestCtx.getInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class);
OIDCClientMetadata metaData = new OIDCClientMetadata();
metaData.setRedirectionURI(new URI("https://notmatching.org"));
- OIDCClientInformation information = new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
+ OIDCClientInformation information =
+ new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
oidcCtx.setClientInformation(information);
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_REDIRECT_URI);
@@ -97,16 +113,38 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
@Test
public void testMatch() throws ComponentInitializationException, URISyntaxException {
init();
- OIDCMetadataContext oidcCtx = profileRequestCtx.getInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class);
+ OIDCMetadataContext oidcCtx =
+ profileRequestCtx.getInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class);
OIDCClientMetadata metaData = new OIDCClientMetadata();
metaData.setRedirectionURI(new URI("https://client.example.org/cb"));
- OIDCClientInformation information = new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
+ OIDCClientInformation information =
+ new OIDCClientInformation(new ClientID("test"), null, metaData, null, null, null);
oidcCtx.setClientInformation(information);
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertProceedEvent(event);
Assert.assertNotNull(respCtx.getRedirectURI());
}
+ @Test
+ public void testNoMatchViaPolicy() throws ComponentInitializationException, URISyntaxException {
+ init(true, new URI(requestUri), null, null, Map.of("redirect_uri", new UnregisteredClientPolicy(
+ new MetadataPolicy.Builder().withOneOfValues(List.of("https://notmatching.org")).build())));
+ profileRequestCtx.getInboundMessageContext().removeSubcontext(OIDCMetadataContext.class);
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_REDIRECT_URI);
+ Assert.assertNull(respCtx.getRedirectURI());
+ }
+
+ @Test
+ public void testMatchViaPolicy() throws ComponentInitializationException, URISyntaxException {
+ init(true, new URI(requestUri), null, null, Map.of("redirect_uri", new UnregisteredClientPolicy(
+ new MetadataPolicy.Builder().withRegexp("^https:\\/\\/(?:([^.]+).)?example.org\\/(.*)").build())));
+ profileRequestCtx.getInboundMessageContext().removeSubcontext(OIDCMetadataContext.class);
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertNotNull(respCtx.getRedirectURI());
+ }
+
@Test
public void testNullRequesNotAllowed() throws ComponentInitializationException, URISyntaxException {
init(true, null, Set.of(new URI(requestUri)), Set.of(new URI(requestUri)));
@@ -117,15 +155,18 @@ public class ValidateRedirectURITest extends BaseOIDCResponseActionTest {
@Test
public void testNullRequesAllowedNotSingleValidUri() throws ComponentInitializationException, URISyntaxException {
- init(false, null, Set.of(new URI(requestUri), new URI("https://another.example.org")), Set.of(new URI(requestUri)));
+ init(false, null, Set.of(new URI(requestUri), new URI("https://another.example.org")),
+ Set.of(new URI(requestUri)));
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_REDIRECT_URI);
Assert.assertNull(respCtx.getRedirectURI());
}
@Test
- public void testNullRequesAllowedNotSingleRegisteredUri() throws ComponentInitializationException, URISyntaxException {
- init(false, null, Set.of(new URI(requestUri)), Set.of(new URI(requestUri), new URI("https://another.example.org")));
+ public void testNullRequesAllowedNotSingleRegisteredUri()
+ throws ComponentInitializationException, URISyntaxException {
+ init(false, null, Set.of(new URI(requestUri)), Set.of(new URI(requestUri),
+ new URI("https://another.example.org")));
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_REDIRECT_URI);
Assert.assertNull(respCtx.getRedirectURI());
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseTypeTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseTypeTest.java
index 5fd8e882..6772563b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseTypeTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateResponseTypeTest.java
@@ -26,7 +26,7 @@ import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.shared.component.ComponentInitializationException;
import org.springframework.webflow.execution.Event;
-import org.testng.annotations.BeforeMethod;
+import org.testng.Assert;
import org.testng.annotations.Test;
import com.nimbusds.oauth2.sdk.ParseException;
@@ -41,17 +41,14 @@ public class ValidateResponseTypeTest extends BaseOIDCResponseActionTest {
private ValidateResponseType action;
private OIDCClientMetadata metaData;
-
- @BeforeMethod
- private void init() throws ComponentInitializationException, URISyntaxException, ParseException {
+
+ private void initMetadata(final Set<ResponseType> responseTypes) throws ComponentInitializationException,
+ URISyntaxException, ParseException {
action = new ValidateResponseType();
action.initialize();
final OIDCMetadataContext oidcCtx =
profileRequestCtx.getInboundMessageContext().ensureSubcontext(OIDCMetadataContext.class);
metaData = new OIDCClientMetadata();
- final Set<ResponseType> responseTypes = new HashSet<ResponseType>();
- responseTypes.add(ResponseType.parse("code"));
- responseTypes.add(ResponseType.parse("id_token token"));
metaData.setResponseTypes(responseTypes);
metaData.setRedirectionURI(new URI("https://notmatching.org"));
OIDCClientInformation information =
@@ -63,7 +60,11 @@ public class ValidateResponseTypeTest extends BaseOIDCResponseActionTest {
* Test that action accepts the "id_token token" response type.
*/
@Test
- public void testSuccess() throws ComponentInitializationException {
+ public void testSuccessWithMetadata() throws ComponentInitializationException, ParseException, URISyntaxException {
+ final Set<ResponseType> responseTypes = new HashSet<ResponseType>();
+ responseTypes.add(ResponseType.parse("code"));
+ responseTypes.add(ResponseType.parse("id_token token"));
+ initMetadata(responseTypes);
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertProceedEvent(event);
}
@@ -72,10 +73,41 @@ public class ValidateResponseTypeTest extends BaseOIDCResponseActionTest {
* Test that action rejects the "id_token token" response type.
*/
@Test
- public void testFailure() throws ComponentInitializationException, ParseException {
+ public void testFailureWithMetadata() throws ComponentInitializationException, ParseException, URISyntaxException {
final Set<ResponseType> responseTypes = new HashSet<ResponseType>();
responseTypes.add(ResponseType.parse("code"));
- metaData.setResponseTypes(responseTypes);
+ initMetadata(responseTypes);
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_RESPONSE_TYPE);
+ }
+
+ private void initPolicy(final Set<ResponseType> responseTypes) {
+ action = new ValidateResponseType();
+ action.setUnregisteredClientValidResponseTypesLookupStrategy(prc -> responseTypes);
+ try {
+ action.initialize();
+ } catch (ComponentInitializationException e) {
+ Assert.fail("Initializiation failed", e);
+ }
+ profileRequestCtx.getInboundMessageContext().removeSubcontext(OIDCMetadataContext.class);
+ }
+
+ @Test
+ public void testSuccessWithPolicy() throws ParseException {
+ final Set<ResponseType> responseTypes = new HashSet<ResponseType>();
+ responseTypes.add(ResponseType.parse("code"));
+ responseTypes.add(ResponseType.parse("id_token token"));
+ initPolicy(responseTypes);
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ }
+
+ @Test
+ public void testFailureWithPolicy() throws ComponentInitializationException, ParseException, URISyntaxException {
+ final Set<ResponseType> responseTypes = new HashSet<ResponseType>();
+ responseTypes.add(ResponseType.parse("code"));
+ initPolicy(responseTypes);
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_RESPONSE_TYPE);
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 6d7edf7c..cb48bfc0 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -116,6 +116,58 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNull(successResponse.getIssuer());
}
+ @Test
+ public void testWithAuthorizationCodeFlow_noMetadata_policyCompliant() throws IOException, SessionException {
+ setRequestParameters(List.of(new Pair<>("client_id", "policyAcceptedClient1"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertNotNull(getSidFromAuthorizeCodeClaimsSet(successResponse));
+ Assert.assertNull(successResponse.getIssuer());
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlow_noMetadata_unrecognizedClientId() throws IOException, SessionException {
+ setRequestParameters(List.of(new Pair<>("client_id", "policyNotFoundForThis"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("redirect_uri", redirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlow_noMetadata_unexpectedRedirectUri() throws IOException, SessionException {
+ setRequestParameters(List.of(new Pair<>("client_id", "policyAcceptedClient1"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("redirect_uri", "https://notinpolicy.invalid.org/cb")));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
@Test
public void testWithAuthorizationCodeFlowRequestObjectEnforcedNoRO() throws IOException, SessionException {
setRequestParameters(List.of(new Pair<>("client_id", "mockClientIdRequestObjectEnforced"),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunctionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunctionTest.java
new file mode 100644
index 00000000..a70ef7f8
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientAllowedScopeLookupFunctionTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+
+/**
+ * Unit tests for {@link DefaultUnregisteredClientAllowedScopeLookupFunction}.
+ */
+public class DefaultUnregisteredClientAllowedScopeLookupFunctionTest {
+
+ DefaultUnregisteredClientAllowedScopeLookupFunction function;
+
+ public void setup(final Map<String, UnregisteredClientPolicy> policies) {
+ function = new DefaultUnregisteredClientAllowedScopeLookupFunction();
+ function.setUnregisteredClientPolicyLookupStrategy(prc -> policies);
+ }
+
+ @Test
+ public void testWithNullPolicies() {
+ setup(null);
+ final Scope result = function.apply(new ProfileRequestContext());
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ }
+
+ @Test
+ public void testWithEmptyPolicies() {
+ setup(Collections.emptyMap());
+ final Scope result = function.apply(new ProfileRequestContext());
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ }
+
+ @Test
+ public void testWithNullScopePolicy() {
+ final Map<String, UnregisteredClientPolicy> map = new HashMap<>();
+ map.put("scope", null);
+ setup(map);
+ final Scope result = function.apply(new ProfileRequestContext());
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 0);
+ }
+
+ @Test
+ public void testWithScopeValue() {
+ setup(Map.of("scope", new UnregisteredClientPolicy(new MetadataPolicy.Builder()
+ .withValue("openid profile").build())));
+ final Scope result = function.apply(new ProfileRequestContext());
+ Assert.assertNotNull(result);
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertTrue(result.contains(OIDCScopeValue.OPENID));
+ Assert.assertTrue(result.contains(OIDCScopeValue.PROFILE));
+ }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidatorTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidatorTest.java
new file mode 100644
index 00000000..05cb6080
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientPolicyValidatorTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+
+/**
+ * Unit tests for {@link DefaultUnregisteredClientPolicyValidator}.
+ */
+public class DefaultUnregisteredClientPolicyValidatorTest {
+
+ DefaultUnregisteredClientPolicyValidator predicate = new DefaultUnregisteredClientPolicyValidator();
+
+ public void setup(final Map<String, UnregisteredClientPolicy> policies) {
+
+ }
+
+ @Test
+ public void testNull() {
+ Assert.assertFalse(predicate.test(null));
+ }
+
+ @Test
+ public void testEmpty() {
+ Assert.assertFalse(predicate.test(new HashMap<>()));
+ }
+
+ @Test
+ public void testValidPolicies() {
+ Assert.assertTrue(predicate.test(validPolicies()));
+ }
+
+ @Test
+ public void testMissingScope() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.remove("scope");
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testInvalidScope() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.put("scope", validRedirectUri());
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testMissingResponseType() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.remove("response_type");
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testInvalidResponseType() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.put("response_type", validRedirectUri());
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testMissingRedirectUri() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.remove("redirect_uri");
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testInvalidRedirectUri() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.put("redirect_uri", validScope());
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testMissingClientId() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.remove("client_id");
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ @Test
+ public void testInvalidClientId() {
+ Map<String, UnregisteredClientPolicy> map = validPolicies();
+ map.put("client_id", validScope());
+ Assert.assertFalse(predicate.test(map));
+ }
+
+ protected Map<String, UnregisteredClientPolicy> validPolicies() {
+ final Map<String, UnregisteredClientPolicy> map = new HashMap<>( Map.of("scope", validScope(),
+ "response_type", validResponseType(),
+ "redirect_uri", validRedirectUri(),
+ "client_id", validClientId()));
+ return map;
+ }
+
+ protected UnregisteredClientPolicy validScope() {
+ return new UnregisteredClientPolicy(new MetadataPolicy.Builder().withValue("openid profile").build());
+ }
+
+ protected UnregisteredClientPolicy validResponseType() {
+ return new UnregisteredClientPolicy(new MetadataPolicy.Builder().withOneOfValues(List.of("code")).build());
+ }
+
+ protected UnregisteredClientPolicy validRedirectUri() {
+ return new UnregisteredClientPolicy(new MetadataPolicy.Builder().withRegexp("mockRegexp").build());
+ }
+
+ protected UnregisteredClientPolicy validClientId() {
+ return new UnregisteredClientPolicy(new MetadataPolicy.Builder()
+ .withOneOfValues(List.of("client1", "client2")).build());
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunctionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunctionTest.java
new file mode 100644
index 00000000..987037fa
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultUnregisteredClientResponseTypesLookupFunctionTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.ResponseType;
+
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.oidc.metadata.policy.UnregisteredClientPolicy;
+
+/**
+ * Unit tests for {@link DefaultUnregisteredClientResponseTypesLookupFunction}.
+ */
+public class DefaultUnregisteredClientResponseTypesLookupFunctionTest {
+
+ DefaultUnregisteredClientResponseTypesLookupFunction function;
+
+ public void setup(final UnregisteredClientPolicy policy) {
+ function = new DefaultUnregisteredClientResponseTypesLookupFunction();
+ function.setUnregisteredClientPolicyLookupStrategy(prc -> policy == null ?
+ Collections.emptyMap() : Map.of("response_type", policy));
+ }
+
+ @Test
+ public void testNoPolicy() {
+ setup(null);
+ Assert.assertTrue(function.apply(new ProfileRequestContext()).isEmpty());
+ }
+
+ @Test
+ public void testNoResponseTypePolicy() {
+ setup(new UnregisteredClientPolicy(new MetadataPolicy()));
+ Assert.assertTrue(function.apply(new ProfileRequestContext()).isEmpty());
+ }
+
+ @Test
+ public void testWithIntegers() {
+ setup(buildOneOfPolicy(List.of(1, 2)));
+ final Set<ResponseType> result = function.apply(new ProfileRequestContext());
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertTrue(result.contains(new ResponseType("1")));
+ Assert.assertTrue(result.contains(new ResponseType("2")));
+ }
+
+ @Test
+ public void testWithStrings() {
+ setup(buildOneOfPolicy(List.of("code", "id_token")));
+ final Set<ResponseType> result = function.apply(new ProfileRequestContext());
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertTrue(result.contains(new ResponseType(ResponseType.Value.CODE)));
+ Assert.assertTrue(result.contains(ResponseType.IDTOKEN));
+ }
+
+ @Test
+ public void testWithStringsWithNull() {
+ final List<Object> list = new ArrayList<>(List.of("code", "id_token"));
+ list.add(null);
+ setup(buildOneOfPolicy(list));
+ final Set<ResponseType> result = function.apply(new ProfileRequestContext());
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertTrue(result.contains(new ResponseType(ResponseType.Value.CODE)));
+ Assert.assertTrue(result.contains(ResponseType.IDTOKEN));
+ }
+
+ @Test
+ public void testWithLists() {
+ setup(buildOneOfPolicy(List.of(List.of("1"), List.of("2"))));
+ final Set<ResponseType> result = function.apply(new ProfileRequestContext());
+ Assert.assertEquals(result.size(), 2);
+ Assert.assertTrue(result.contains(new ResponseType(List.of("1").toString())));
+ Assert.assertTrue(result.contains(new ResponseType(List.of("2").toString())));
+ }
+
+ @Test
+ public void testWithMixedContent() {
+ setup(buildOneOfPolicy(List.of(1, List.of("2"), "code")));
+ final Set<ResponseType> result = function.apply(new ProfileRequestContext());
+ Assert.assertEquals(result.size(), 3);
+ Assert.assertTrue(result.contains(new ResponseType("1")));
+ Assert.assertTrue(result.contains(new ResponseType(List.of("2").toString())));
+ Assert.assertTrue(result.contains(ResponseType.CODE));
+ }
+
+ protected UnregisteredClientPolicy buildOneOfPolicy(final List<Object> values) {
+ return new UnregisteredClientPolicy(new MetadataPolicy.Builder().withOneOfValues(values).build());
+ }
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
index 117a8451..b5d3ad2c 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
@@ -19,3 +19,5 @@ idp.oauth2.grantTypes = authorization_code,refresh_token,client_credentials
idp.oauth2.defaultAllowedAudience = https://rp.example.org
idp.oidc.discovery.resolver.values = CustomConfigurationValues
+
+idp.oidc.DefaultUnregisteredPolicyFile = src/test/resources/net/shibboleth/idp/module/conf/unregistered-policy.json
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 78a9adfd..b13e57c1 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -39,6 +39,7 @@
<ref bean="OIDC.Keyset" />
<ref bean="OIDC.Registration" />
<ref bean="OIDC.Configuration" />
+ <bean parent="OIDC.SSO" p:unregisteredClientPolicyLookupStrategy-ref="shibboleth.oidc.DefaultUnregisteredPolicyLookupStrategy"/>
<ref bean="OAUTH2.Token" />
<bean parent="OAUTH2.TokenAudience" p:encryptionOptional="true" />
<ref bean="OAUTH2.Introspection" />
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/unregistered-policy.json b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/unregistered-policy.json
new file mode 100644
index 00000000..6ac45565
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/unregistered-policy.json
@@ -0,0 +1,14 @@
+{
+ "client_id": {
+ "one_of": ["policyAcceptedClient1","policyAcceptedClient2"]
+ },
+ "scope": {
+ "value": "openid info"
+ },
+ "redirect_uri": {
+ "regexp": "^https:\/\/(?:([^.]+).)?example.org\/(.*)"
+ },
+ "response_type": {
+ "one_of": ["code"]
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list