[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