[java-idp-oidc] 02/02: JOIDC-252 - Mechanism for custom metadata lookup flows

Henri Mikkonen henri.mikkonen at iki.fi
Fri Sep 5 09:52:05 UTC 2025


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=ee83f90b6667671d3ed4c914ca37308ddb0cb789

commit ee83f90b6667671d3ed4c914ca37308ddb0cb789
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Sep 5 12:51:41 2025 +0300

    JOIDC-252 - Mechanism for custom metadata lookup flows
    
    https://shibboleth.atlassian.net/browse/JOIDC-252
    
    Initial implementation extending the oidc/metadata-lookup flow
    - If no metadata was found via OIDC/SAML metadata resolution, then the lookup extension flows are called
    - Any global beans extending the abstract descriptor 'shibboleth.oidc.MetadataLookupExtensionFlow' bean are auto-wired
      - the flows need to be prefixed with 'oidc/metadata-lookup-ext/'
      - MetadataLookupExtensionContext in available for the flows, containing easy access to clientId and profileId values
      - each extension flow is attempted until one populates OIDCMetadataContext with OIDCClientInformation
        - Similarly to OIDC/SAML resolution, the OIDCMetadataContext is expected to be populated under inbound message context
---
 .../metadata/MetadataLookupExtensionContext.java   |  93 ++++++++++
 .../MetadataLookupExtensionFlowDescriptor.java     | 100 +++++++++++
 ...tadataLookupExtensionFlowDescriptorManager.java |  41 +++++
 .../idp/plugin/oidc/op/metadata/package-info.java  |  18 ++
 .../impl/SelectMetadataLookupExtensionFlow.java    | 198 +++++++++++++++++++++
 ...okupExtensionContextClientIDLookupFunction.java |  44 +++++
 ...kupExtensionContextProfileIDLookupFunction.java |  41 +++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   6 +
 .../oidc/metadata-lookup/metadata-lookup-beans.xml |   9 +
 .../oidc/metadata-lookup/metadata-lookup-flow.xml  |  22 ++-
 .../oidc/op/profile/flow/AuthorizeFlowTest.java    |  44 +++++
 .../op/profile/flow/PopulateClientInformation.java |  69 +++++++
 .../lookupext1/lookupext1-beans.xml                |  23 +++
 .../lookupext1/lookupext1-flow.xml                 |  22 +++
 .../lookupext2/lookupext2-flow.xml                 |  22 +++
 .../net/shibboleth/idp/module/conf/global.xml      |   3 +
 16 files changed, 753 insertions(+), 2 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionContext.java
new file mode 100644
index 00000000..f3e406a5
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.shared.annotation.constraint.Live;
+
+/**
+ * A {@link BaseContext} which holds generic information for the metadata lookup extension flows, including data which
+ * flows are already executed / attempted.
+ */
+public class MetadataLookupExtensionContext extends BaseContext {
+
+    /** The flows that are already attempted. */
+    @Nonnull private List<MetadataLookupExtensionFlowDescriptor> attemptedFlows;
+
+    /** The client ID to be looked up. */
+    @Nullable private String clientId;
+
+    /** The profile ID that initiated the lookup. */
+    @Nullable private String profileId;
+
+    /**
+     * Constructor.
+     */
+    public MetadataLookupExtensionContext() {
+        attemptedFlows = new ArrayList<>();
+    }
+
+    /**
+     * Get the flows that are already attempted.
+     * 
+     * @return the flows that are already attempted
+     */
+    @Nonnull @Live public List<MetadataLookupExtensionFlowDescriptor> getAttemptedFlows() {
+        return attemptedFlows;
+    }
+
+    /**
+     * Set the client ID to be looked up.
+     * 
+     * @param id client ID
+     */
+    public void setClientId(@Nullable final String id) {
+        clientId = id;
+    }
+
+    /**
+     * Get the client ID to be looked up.
+     * 
+     * @return client ID
+     */
+    @Nullable public String getClientId() {
+        return clientId;
+    }
+
+    /**
+     * Set the profile ID that initiated the lookup.
+     * 
+     * @param id profile ID
+     */
+    public void setProfileId(@Nullable final String id) {
+        profileId = id;
+    }
+
+    /**
+     * Get the profile ID that initiated the lookup.
+     * 
+     * @return profile ID
+     */
+    @Nullable public String getProfileId() {
+        return profileId;
+    }
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptor.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptor.java
new file mode 100644
index 00000000..2ea8a8ff
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptor.java
@@ -0,0 +1,100 @@
+/*
+ * 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.metadata;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.idp.profile.FlowDescriptor;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+
+/**
+ * A descriptor for a metadata lookup extension flow.
+ * 
+ * <p>
+ * A metadata lookup extension flow is designed to be injected into a oidc/metadata-lookup to facilitate extensions for
+ * the metadata lookup process. The extension flows must include an activation predicate to indicate suitability based
+ * on the content of the {@link ProfileRequestContext}.
+ * </p>
+ */
+public class MetadataLookupExtensionFlowDescriptor extends AbstractIdentifiableInitializableComponent
+    implements FlowDescriptor, Predicate<ProfileRequestContext> {
+
+    /** Prefix convention for flow IDs. */
+    @Nonnull @NotEmpty public static final String FLOW_ID_PREFIX = "oidc/metadata-lookup-ext/";
+    
+    /** Predicate that must be true for this flow to be usable for a given request. */
+    @Nonnull private Predicate<ProfileRequestContext> activationCondition;
+
+    /** Constructor. */
+    public MetadataLookupExtensionFlowDescriptor() {
+        activationCondition = PredicateSupport.alwaysTrue();
+    }
+
+    /**
+     * Set the activation condition in the form of a {@link Predicate} such that iff the condition evaluates to true
+     * should the corresponding flow be allowed/possible.
+     * 
+     * @param condition predicate that controls activation of the flow
+     */
+    public void setActivationCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkSetterPreconditions();
+        activationCondition = Constraint.isNotNull(condition, "Activation condition predicate cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        return activationCondition.test(input);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return getId().hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj instanceof MetadataLookupExtensionFlowDescriptor flowDescriptor) {
+            return getId().equals(flowDescriptor.getId());
+        }
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("flowId", getId())
+                .toString();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptorManager.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptorManager.java
new file mode 100644
index 00000000..f29ccf1d
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/MetadataLookupExtensionFlowDescriptorManager.java
@@ -0,0 +1,41 @@
+/*
+ * 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.metadata;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import net.shibboleth.shared.spring.config.IdentifiedComponentManager;
+
+/**
+ * Manager of {@link MetadataLookupExtensionFlowDescriptor} objects.
+ */
+public class MetadataLookupExtensionFlowDescriptorManager 
+    extends IdentifiedComponentManager<MetadataLookupExtensionFlowDescriptor> {
+
+    /**
+     * Constructor.
+     *
+     * @param freeObjects free-standing objects to add
+     */
+    @Autowired
+    public MetadataLookupExtensionFlowDescriptorManager(
+        @Nullable final Collection<MetadataLookupExtensionFlowDescriptor> freeObjects) {
+        super(freeObjects);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/package-info.java
new file mode 100644
index 00000000..5406ba0f
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Interfaces and common classes related to OIDC metadata (or client information).
+ */
+package net.shibboleth.idp.plugin.oidc.op.metadata;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/impl/SelectMetadataLookupExtensionFlow.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/impl/SelectMetadataLookupExtensionFlow.java
new file mode 100644
index 00000000..2a5ff735
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/metadata/impl/SelectMetadataLookupExtensionFlow.java
@@ -0,0 +1,198 @@
+/*
+ * 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.metadata.impl;
+
+import java.util.Collection;
+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;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.DefaultClientIDLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext;
+import net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionFlowDescriptor;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A profile action that selects metadata lookup extension flows to invoke.
+ * 
+ * <p>
+ * The flows available to be executed are held by the {@link #availableFlows}. Available flows are executed in
+ * the order that they are configured if their activation condition evaluates to true.
+ * </p>
+ * 
+ * <p>
+ * This action returns the flow ID to be executed or {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID} if
+ * there are no flows available to be executed.
+ * </p>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event Selected flow ID to execute
+ */
+public class SelectMetadataLookupExtensionFlow extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SelectMetadataLookupExtensionFlow.class);
+
+    /** The creation strategy for the metadata lookup extension context. */
+    @Nonnull private Function<ProfileRequestContext, MetadataLookupExtensionContext>
+        metadataLookupExtensionContextCreationStrategy;
+
+    /** The available metadata lookup extension flows. */
+    @Nullable private Collection<MetadataLookupExtensionFlowDescriptor> availableFlows;
+
+    /** The strategy used to obtain the client id value to be stored in the context. */
+    @Nonnull private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+    /** The strategy used to obtain the profile id value to be stored in the context. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> profileIDLookupStrategy;
+
+    /** The context to operate on. */
+    @NonnullBeforeExec private MetadataLookupExtensionContext metadataLookupExtensionContext;
+
+    /**
+     * Constructor.
+     */
+    public SelectMetadataLookupExtensionFlow() {
+        final Function<ProfileRequestContext, MetadataLookupExtensionContext> mleccs =
+                new ChildContextLookup<>(MetadataLookupExtensionContext.class, true).compose(
+                        new InboundMessageContextLookup());
+        assert mleccs != null;
+        metadataLookupExtensionContextCreationStrategy = mleccs;
+        clientIDLookupStrategy = new DefaultClientIDLookupFunction();
+    }
+
+    /**
+     * Set the creation strategy for the metadata lookup extension context.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setMetadataLookupExtensionContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, MetadataLookupExtensionContext> strategy) {
+        checkSetterPreconditions();
+        metadataLookupExtensionContextCreationStrategy = Constraint.isNotNull(strategy,
+                "MetadataLookupExtensionContextCreationStrategy cannot be null");
+    }
+    
+    /**
+     * Set the available metadata lookup extension flows.
+     * 
+     * @param flows available metadata lookup flows
+     */
+    public void setAvailableFlows(@Nullable final Collection<MetadataLookupExtensionFlowDescriptor> flows) {
+        checkSetterPreconditions();
+        availableFlows = flows;
+    }
+
+    /**
+     * 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) {
+        checkSetterPreconditions();
+        clientIDLookupStrategy =
+                Constraint.isNotNull(strategy, "ClientIDLookupStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the profile id to be used.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProfileIDLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        profileIDLookupStrategy = Constraint.isNotNull(strategy, "ProfileIDLookupStrategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (profileIDLookupStrategy == null) {
+            throw new ComponentInitializationException("ProfileIDLookupStrategy cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        metadataLookupExtensionContext = metadataLookupExtensionContextCreationStrategy.apply(profileRequestContext);
+        if (metadataLookupExtensionContext == null) {
+            log.error("{} Could not create the metadata lookup extension context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        if (availableFlows == null || availableFlows.isEmpty()) {
+            log.debug("{} No available metadata lookup extension flows, nothing to do", getLogPrefix());
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (metadataLookupExtensionContext.getClientId() == null) {
+            final ClientID clientId = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+            if (clientId == null) {
+                log.debug("{} Could not resolve client ID to be looked up", getLogPrefix());
+            } else {
+                metadataLookupExtensionContext.setClientId(clientId.getValue());
+            }
+        }
+        if (metadataLookupExtensionContext.getProfileId() == null) {
+            final String profileId = profileIDLookupStrategy.apply(profileRequestContext);
+            if (profileId == null) {
+                log.debug("(} Could not resolve profile ID to be used", getLogPrefix());
+            } else {
+                metadataLookupExtensionContext.setProfileId(profileId);
+            }
+        }
+        for (final MetadataLookupExtensionFlowDescriptor flow : availableFlows) {
+            log.trace("{} Checking flow {}", getLogPrefix(), flow.getId());
+            if (metadataLookupExtensionContext.getAttemptedFlows().contains(flow)) {
+                log.debug("{} Flow (} has already been attempted", getLogPrefix(), flow.getId());
+            } else {
+                log.debug("{} Selecting flow {}", getLogPrefix(), flow.getId());
+                metadataLookupExtensionContext.getAttemptedFlows().add(flow);
+                ActionSupport.buildEvent(profileRequestContext, flow.ensureId());
+                return;
+            }
+        }
+        log.debug("{} No flows available to choose from", getLogPrefix());
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextClientIDLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextClientIDLookupFunction.java
new file mode 100644
index 00000000..5bda97f9
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextClientIDLookupFunction.java
@@ -0,0 +1,44 @@
+/*
+ * 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.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext;
+
+/**
+ * Lookup function to fetch {@link ClientID} via {@link MetadataLookupExtensionContext}.
+ */
+public class DefaultMetadataLookupExtensionContextClientIDLookupFunction
+    implements Function<MessageContext, ClientID>{
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public ClientID apply(@Nullable final MessageContext messageContext) {
+        return Optional.ofNullable(messageContext)
+                .map(msgCtx -> msgCtx.getSubcontext(MetadataLookupExtensionContext.class))
+                .map(extCtx -> extCtx.getClientId())
+                .map(ClientID::new)
+                .orElse(null);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextProfileIDLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextProfileIDLookupFunction.java
new file mode 100644
index 00000000..8b56cb94
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataLookupExtensionContextProfileIDLookupFunction.java
@@ -0,0 +1,41 @@
+/*
+ * 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.Optional;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext;
+
+/**
+ * Lookup function to fetch profile ID via {@link MetadataLookupExtensionContext}.
+ */
+public class DefaultMetadataLookupExtensionContextProfileIDLookupFunction
+    implements Function<ProfileRequestContext, String>{
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        return Optional.ofNullable(profileRequestContext)
+                .map(prc -> prc.getInboundMessageContext())
+                .map(msgCtx -> msgCtx.getSubcontext(MetadataLookupExtensionContext.class))
+                .map(extCtx -> extCtx.getProfileId())
+                .orElse(null);
+    }
+}
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 08c9b2b5..a92f3748 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
@@ -893,4 +893,10 @@
         </property>
     </bean>
 
+    <bean id="shibboleth.oidc.MetadataLookupExtensionFlowDescriptorManager"
+        class="net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionFlowDescriptorManager"/>
+
+    <bean id="shibboleth.oidc.MetadataLookupExtensionFlow" abstract="true"
+        class="net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionFlowDescriptor" />
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
index b898c7b0..6a46ad8c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml
@@ -82,4 +82,13 @@
         </constructor-arg>
     </bean>
 
+    <bean id="SelectMetadataLookupExtensionFlow" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.metadata.impl.SelectMetadataLookupExtensionFlow"
+        p:availableFlows="#{@'shibboleth.oidc.MetadataLookupExtensionFlowDescriptorManager'.getComponents()}"
+        p:clientIDLookupStrategy-ref="shibboleth.ClientIDLookupStrategy">
+        <property name="profileIDLookupStrategy">
+            <bean parent="shibboleth.Functions.Constant" c:target="#{getObject('shibboleth.oidc.profileId') ?: null}"/>
+        </property>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
index 53f58ddf..cb8cc3fb 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-flow.xml
@@ -17,7 +17,7 @@
 
     <decision-state id="CheckIfFoundFromClientInformationService">
         <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext)).isVerified()"
-            then="SelectConfiguration" else="LookupFromSAMLMetadataService" />
+            then="SelectMetadataLookupExtensionFlow" else="LookupFromSAMLMetadataService" />
     </decision-state>
 
     <action-state id="LookupFromSAMLMetadataService">
@@ -30,16 +30,34 @@
 
     <decision-state id="CheckIfFoundFromSAMLMetadata">
         <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)).containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLMetadataContext))"
-            then="PopulateOIDCMetadataContextFromSAML" else="SelectConfiguration" />
+            then="PopulateOIDCMetadataContextFromSAML" else="SelectMetadataLookupExtensionFlow" />
     </decision-state>
     
     <action-state id="PopulateOIDCMetadataContextFromSAML">
         <evaluate expression="PopulateOIDCMetadataContext" />
         <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
         <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SelectMetadataLookupExtensionFlow" />
+    </action-state>
+
+    <action-state id="SelectMetadataLookupExtensionFlow">
+        <evaluate expression="SelectMetadataLookupExtensionFlow" />
+        <evaluate expression="'proceed'" />
+
+        <!-- Call a subflow with the same ID as the event. -->
+        <transition on="#{currentEvent.id.startsWith('oidc/metadata-lookup-ext/')}" to="CallMetadataLookupExtensionFlow" />
         <transition on="proceed" to="SelectConfiguration" />
     </action-state>
 
+    <!--
+    This invokes a flow. Anything but proceed is a terminating state, otherwise control passes
+    back to this flow to select another flow to be executed.
+    -->
+    <subflow-state id="CallMetadataLookupExtensionFlow" subflow="#{currentEvent.id}">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="SelectMetadataLookupExtensionFlow" />
+    </subflow-state>
+
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup/metadata-lookup-beans.xml" />
 
 </flow>
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 2bbd6b4b..9aaa7876 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
@@ -2312,6 +2312,50 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
         assertErrorDescriptionContains(result, "AccessDenied");
     }
 
+    @Test
+    public void testWithAuthorizationCodeFlow_metadataViaLookupExtension1() throws IOException, SessionException {
+        setRequestParameters(List.of(new Pair<>("client_id", "clientIdForLookupExtension1"),
+                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_metadataViaLookupExtension2() throws IOException, SessionException {
+        setRequestParameters(List.of(new Pair<>("client_id", "clientIdForLookupExtension2"),
+                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());
+    }
+
     @Factory
     public Object[] createIdTokenSecurityTests() {
         return new Object[] {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PopulateClientInformation.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PopulateClientInformation.java
new file mode 100644
index 00000000..25103e48
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PopulateClientInformation.java
@@ -0,0 +1,69 @@
+/*
+ * 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.flow;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Support action for the metadata-lookup extension test flows.
+ */
+public class PopulateClientInformation extends AbstractProfileAction {
+
+    /** Strategy used to obtain the client id value for authorize/token request. */
+    @NonnullAfterInit private Function<MessageContext, ClientID> clientIDLookupStrategy;
+
+    /**
+     * 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");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final ClientID clientId = clientIDLookupStrategy.apply(profileRequestContext.getInboundMessageContext());
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(URI.create("https://example.org/cb"));
+        metadata.setScope(Scope.parse("openid profile"));
+        final OIDCClientInformation clientInformation =
+                new OIDCClientInformation(clientId, new Date(), metadata,
+                new Secret("mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret"));
+        final OIDCMetadataContext oidcCtx = new OIDCMetadataContext();
+        oidcCtx.setClientInformation(clientInformation);
+        profileRequestContext.ensureInboundMessageContext().addSubcontext(oidcCtx);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-beans.xml b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-beans.xml
new file mode 100644
index 00000000..9695caa2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-beans.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="InitializeRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype">
+        <property name="clientIDLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextClientIDLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="PopulateClientInformation" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.flow.PopulateClientInformation">
+        <property name="clientIDLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataLookupExtensionContextClientIDLookupFunction" />
+        </property>
+    </bean>
+</beans>
diff --git a/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-flow.xml b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-flow.xml
new file mode 100644
index 00000000..a76378cd
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-flow.xml
@@ -0,0 +1,22 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd">
+
+    <decision-state id="CheckIfClientIdMatch">
+        <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext)).getClientId() != null and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLo [...]
+            then="SetMetadata" else="proceed" />
+    </decision-state>
+
+    <action-state id="SetMetadata">
+        <evaluate expression="PopulateClientInformation" />
+        <evaluate expression="InitializeRelyingPartyContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="proceed">
+        </transition>
+    </action-state>
+
+    <end-state id="proceed"/>
+
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext2/lookupext2-flow.xml b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext2/lookupext2-flow.xml
new file mode 100644
index 00000000..bd88b347
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext2/lookupext2-flow.xml
@@ -0,0 +1,22 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd">
+
+    <decision-state id="CheckIfClientIdMatch">
+        <if test="opensamlProfileRequestContext.ensureInboundMessageContext().containsSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext)) and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLookupExtensionContext)).getClientId() != null and opensamlProfileRequestContext.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.metadata.MetadataLo [...]
+            then="SetMetadata" else="proceed" />
+    </decision-state>
+
+    <action-state id="SetMetadata">
+        <evaluate expression="PopulateClientInformation" />
+        <evaluate expression="InitializeRelyingPartyContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="proceed">
+        </transition>
+    </action-state>
+
+    <end-state id="proceed"/>
+
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/lookupext1/lookupext1-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
index f0408d27..23ee01ca 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/global.xml
@@ -113,4 +113,7 @@
 
     <bean id="alwaysFalsePolicyOperator" class="net.shibboleth.idp.plugin.oidc.op.profile.flow.AlwaysFalseCustomMetadataPolicyOperator" />
 
+    <bean p:id="oidc/metadata-lookup-ext/lookupext1" parent="shibboleth.oidc.MetadataLookupExtensionFlow" />
+    <bean p:id="oidc/metadata-lookup-ext/lookupext2" parent="shibboleth.oidc.MetadataLookupExtensionFlow" />
+
 </beans>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list