[java-idp-oidc] branch main updated: JOIDC-21 - Use token authentication for OIDC dynamic client registration

Henri Mikkonen henri.mikkonen at iki.fi
Fri Feb 18 11:41:13 UTC 2022


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=56202be2337559a3b56db014447722c437192c54

The following commit(s) were added to refs/heads/main by this push:
     new 56202be2 JOIDC-21 - Use token authentication for OIDC dynamic client registration
56202be2 is described below

commit 56202be2337559a3b56db014447722c437192c54
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Feb 18 13:39:58 2022 +0200

    JOIDC-21 - Use token authentication for OIDC dynamic client registration
    
    https://shibboleth.atlassian.net/browse/JOIDC-21
    
    Initial, non-complete version of the admin flow and CLI that can be used
    for issuing initial registration access tokens.
---
 idp-oidc-extension-impl/pom.xml                    |   5 +
 .../cli/IssueRegistrationAccessTokenArguments.java |  99 +++++++
 .../profile/impl/IssueRegistrationAccessToken.java | 330 +++++++++++++++++++++
 ...efaultMetadataPolicyCriteriaLookupFunction.java |  55 ++++
 ...efaultMetadataPolicyLocationLookupFunction.java |  57 ++++
 ...ultRegistrationTokenLifetimeLookupFunction.java |  62 ++++
 ...istrationTokenRelyingPartyIdLookupFunction.java |  55 ++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  17 ++
 .../issue-registration-access-token-beans.xml      | 147 +++++++++
 .../issue-registration-access-token-flow.xml       |  51 ++++
 .../idp/plugin/oidc/op/conf/oidc.properties        |   4 +
 .../flow/IssueRegistrationAccessTokenFlowTest.java | 106 +++++++
 .../impl/IssueRegistrationAccessTokenTest.java     | 193 ++++++++++++
 13 files changed, 1181 insertions(+)

diff --git a/idp-oidc-extension-impl/pom.xml b/idp-oidc-extension-impl/pom.xml
index 4d512f90..f87b0e40 100644
--- a/idp-oidc-extension-impl/pom.xml
+++ b/idp-oidc-extension-impl/pom.xml
@@ -152,6 +152,11 @@
             <artifactId>idp-admin-impl</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>com.beust</groupId>
+            <artifactId>jcommander</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- Test Dependencies -->
         <dependency>
             <groupId>commons-io</groupId>
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/cli/IssueRegistrationAccessTokenArguments.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/cli/IssueRegistrationAccessTokenArguments.java
new file mode 100644
index 00000000..7d42422f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/cli/IssueRegistrationAccessTokenArguments.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.cli;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.beust.jcommander.Parameter;
+
+import net.shibboleth.idp.cli.AbstractCommandLineArguments;
+
+/** Command line processing for issue-registration-access-token flow. */
+public class IssueRegistrationAccessTokenArguments extends AbstractCommandLineArguments {
+    
+    /** The URL parameter name for the metadata policy location. */
+    public static final String URL_PARAM_METADATA_POLICY_LOCATION = "metadataPolicyLocation";
+    
+    /** The URL parameter name for the access token lifetime. */
+    public static final String URL_PARAM_LIFETIME = "tokenLifetime";
+    
+    /** The URL parameter name for the relying party identifier. */
+    public static final String URL_PARAM_RELYING_PARTY_ID = "relyingPartyId";
+
+    /** Metadata policy for the requested OIDC dynamic client registration metadata. */
+    @Parameter(names = {"-m", "--metadataPolicyLocation"}, required = false, description = "Metadata policy location")
+    @Nullable private String metadata;
+    
+    /** Lifetime for the access token to be issued. */
+    @Parameter(names = {"-l", "--lifetime"}, required = false, description = "Lifetime for the access token")
+    @Nullable private String lifetime;
+    
+    /** Relying party identifier for the access token to be issued. */
+    @Parameter(names = {"-i", "--relyingPartyId"}, required = false, description = "Relying party ID the access token")
+    @Nullable private String relyingPartyId;
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    public void validate() {
+        if (metadata == null) {
+            throw new IllegalArgumentException("No metadata spefified");
+        }
+        if (lifetime == null) {
+            throw new IllegalArgumentException("No lifetime specified");
+        }
+        if (relyingPartyId == null) {
+            throw new IllegalArgumentException("No relyingPartyId specified");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected StringBuilder doBuildURL(@Nonnull final StringBuilder builder) {
+
+        if (getPath() == null) {
+            builder.append("/profile/admin/oidc/issue-registration-access-token");
+        }
+
+        if (builder.toString().contains("?")) {
+            builder.append('&');
+        } else {
+            builder.append('?');
+        }
+
+        try {
+            builder
+                .append(URL_PARAM_LIFETIME + "=")
+                .append(URLEncoder.encode(lifetime, "UTF-8"))
+                .append("&" + URL_PARAM_METADATA_POLICY_LOCATION + "=")
+                .append(URLEncoder.encode(metadata, "UTF-8"))
+                .append("&" + URL_PARAM_RELYING_PARTY_ID + "=")
+                .append(URLEncoder.encode(relyingPartyId, "UTF-8"));
+        } catch (final UnsupportedEncodingException e) {
+            // UTF-8 is a required encoding.
+            throw new RuntimeException("URL encoding failed", e);
+        }
+
+        return builder;
+     }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessToken.java
new file mode 100644
index 00000000..c74eeaeb
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessToken.java
@@ -0,0 +1,330 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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 org.springframework.webflow.execution.RequestContext;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.Tokens;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultRegistrationTokenLifetimeLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultRegistrationTokenRelyingPartyIdLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RegistrationClaimsSet;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.security.DataSealer;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+
+/**
+ * Action that issues access token to be used for the OIDC dynamic registration endpoint.
+ * 
+ * <p>On success, {@link AccessTokenResponse} is built and attached as a message for the outbound message context. Also
+ * a proceed event is built. On error, a non-proceed event is built.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ */
+public class IssueRegistrationAccessToken extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(IssueRegistrationAccessToken.class);
+
+    /** Data sealer for handling access token. */
+    @NonnullAfterInit private DataSealer dataSealer;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext, IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Lookup function for the metadata policy. */
+    @NonnullAfterInit private Function<ProfileRequestContext, Map<String,MetadataPolicy>> metadataPolicyLookupStrategy;
+
+    /** Lookup function for the token lifetime. */
+    @Nonnull private Function<ProfileRequestContext, Duration> tokenLifetimeLookupStrategy;
+    
+    /** Lookup function for the token issuer. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> issuerLookupStrategy;
+    
+    /** Lookup function for the relying party identifier. */
+    @Nonnull private Function<ProfileRequestContext, String> relyingPartyIdLookupStrategy;
+
+    /** The identifier generator to use. */
+    @Nullable private IdentifierGenerationStrategy idGenerator;
+
+    /** The resolved metadata policy. */
+    @Nullable private Map<String,MetadataPolicy> metadataPolicy;
+    
+    /** The token issuer. */
+    @Nonnull private String issuer;
+    
+    /** The relying party identifier. */
+    @Nullable private String relyingPartyId;
+
+    /** The token lifetime. */
+    private Duration tokenLifetime;
+    
+    /**
+     * Constructor.
+     */
+    public IssueRegistrationAccessToken() {
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+        tokenLifetimeLookupStrategy = new DefaultRegistrationTokenLifetimeLookupFunction();
+        relyingPartyIdLookupStrategy = new DefaultRegistrationTokenRelyingPartyIdLookupFunction();
+    }
+    
+    /**
+     * Set the data sealer for handling access token.
+     * 
+     * @param sealer data sealer.
+     */
+    public void setSealer(@Nonnull final DataSealer sealer) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        dataSealer = Constraint.isNotNull(sealer, "Data sealer cannot be null");
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, IdentifierGenerationStrategy> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set a lookup strategy for the metadata policy.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMetadataPolicyLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Map<String,MetadataPolicy>> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        metadataPolicyLookupStrategy =
+                Constraint.isNotNull(strategy, "Metadata policy lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set a lookup strategy for the token lifetime.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setTokenLifetimeLookupStrategy(@Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        tokenLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Token lifetime lookup strategy cannot be null");
+    }
+
+    /**
+     * Set a lookup strategy for the token issuer.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+
+    /**
+     * Set a lookup strategy for the relying party identifier.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyIdLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        relyingPartyIdLookupStrategy = Constraint.isNotNull(strategy,
+                "Relying party ID lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (dataSealer == null) {
+            throw new ComponentInitializationException("Data sealer cannot be null");
+        }
+        
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
+
+        if (metadataPolicyLookupStrategy == null) {
+            throw new ComponentInitializationException("Metadata policy lookup strategy cannot be null");
+        }
+
+        if (issuerLookupStrategy == null) {
+            throw new ComponentInitializationException("Issuer lookup strategy cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        final SpringRequestContext springRequestContext =
+                profileRequestContext.getSubcontext(SpringRequestContext.class);
+        if (springRequestContext == null) {
+            log.warn("{} Spring request context not found in profile request context", getLogPrefix());
+            return false;
+        }
+
+        final RequestContext requestContext = springRequestContext.getRequestContext();
+        if (requestContext == null) {
+            log.warn("{} Web Flow request context not found in Spring request context", getLogPrefix());
+            return false;
+        }
+        
+        if (profileRequestContext.getOutboundMessageContext() == null) {
+            log.error("{} No outbound message context found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;            
+        }
+
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.error("{} No identifier generation strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        metadataPolicy = metadataPolicyLookupStrategy.apply(profileRequestContext);
+        // null is not allowed - empty metadata policy is
+        if (metadataPolicy == null) {
+            log.warn("{} No metadata policy could be resolved, nothing to do", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+
+        issuer = issuerLookupStrategy.apply(profileRequestContext);
+        if (issuer == null) {
+            log.warn("{} No issuer could be resolved, nothing to do", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        relyingPartyId = relyingPartyIdLookupStrategy.apply(profileRequestContext);
+        if (relyingPartyId == null) {
+            log.warn("{} No relying party ID could be resolved, nothing to do", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        tokenLifetime = tokenLifetimeLookupStrategy.apply(profileRequestContext);
+        if (tokenLifetime == null) {
+            log.warn("{} No token lifetime could be resolved, nothing to do", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final String id = idGenerator.generateIdentifier();
+        
+        final Instant now = Instant.now();
+        final Instant exp = now.plus(tokenLifetime);
+
+        final RegistrationClaimsSet claimsSet = new RegistrationClaimsSet.Builder(id)
+                .withIssuer(issuer)
+                .withIssuedAt(now)
+                .withMetadata(metadataPolicy)
+                .withExpiration(exp)
+                .withRelyingPartyId(relyingPartyId)
+                .build();
+        
+        //TODO: possible end-user authentication claims:
+        //via Builder: .withAcr .withAuthTime .withPrincipal
+        
+        final AccessToken accessToken;
+        
+        try {
+            final String value = objectMapper.writeValueAsString(claimsSet);
+            log.debug("{} Built the following JSON to be sealed {}", getLogPrefix(), value);
+            final String encryptedValue = dataSealer.wrap(value, claimsSet.getExpiration());
+            log.debug("{} Encrypted the JSON into {}", getLogPrefix(), encryptedValue);
+            accessToken = new BearerAccessToken(encryptedValue, tokenLifetime.getSeconds(), null);
+        } catch (JsonProcessingException e) {
+            log.error("{} Could not build JSON", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        } catch (DataSealerException e) {
+            log.error("{} Could not encrypt the claims set", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+        
+        final TokenResponse response = new AccessTokenResponse(new Tokens(accessToken, null));
+        profileRequestContext.getOutboundMessageContext().setMessage(response);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyCriteriaLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyCriteriaLookupFunction.java
new file mode 100644
index 00000000..1cb076c2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyCriteriaLookupFunction.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.metadata.criterion.ResourceLocationCriterion;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * A function returning a {@link CriteriaSet} which contains the metadata policy document location as {@link
+ * ResourceLocationCriterion}. The value is fetched from the SWF request parameters, using {@link
+ * DefaultMetadataPolicyLocationLookupFunction}.
+ */
+public class DefaultMetadataPolicyCriteriaLookupFunction implements Function<ProfileRequestContext, CriteriaSet> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultMetadataPolicyCriteriaLookupFunction.class);
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public CriteriaSet apply(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final String location = new DefaultMetadataPolicyLocationLookupFunction().apply(profileRequestContext);
+        if (StringSupport.trimOrNull(location) == null) {
+            log.warn("Could not find the location for building the criteria set, returning null");
+            return null;
+        }
+        log.trace("Found a location {} to be included in the criteria set", location);
+        final ResourceLocationCriterion criterion = new ResourceLocationCriterion(location);
+        return new CriteriaSet(criterion);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyLocationLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyLocationLookupFunction.java
new file mode 100644
index 00000000..7275c072
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultMetadataPolicyLocationLookupFunction.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.cli.IssueRegistrationAccessTokenArguments;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * A lookup function that fetches the metadata policy document location from the SWF request parameters.
+ * 
+ * The parameter key is {@link IssueRegistrationAccessTokenArguments#URL_PARAM_METADATA_POLICY_LOCATION}.
+ */
+public class DefaultMetadataPolicyLocationLookupFunction implements Function<ProfileRequestContext, String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final SpringRequestContext springRequestContext =
+            profileRequestContext.getSubcontext(SpringRequestContext.class);
+        if (springRequestContext == null) {
+            return null;
+        }
+
+        final RequestContext requestContext = springRequestContext.getRequestContext();
+        if (requestContext == null) {
+            return null;
+        }
+
+        final String metadataPolicyUrl = (String) requestContext.getFlowScope().get(
+                IssueRegistrationAccessTokenArguments.URL_PARAM_METADATA_POLICY_LOCATION);
+        return StringSupport.trimOrNull(metadataPolicyUrl);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenLifetimeLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenLifetimeLookupFunction.java
new file mode 100644
index 00000000..dc8a0fcd
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenLifetimeLookupFunction.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.cli.IssueRegistrationAccessTokenArguments;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+
+/**
+ * A lookup function that fetches the token lifetime from the SWF request parameters.
+ * 
+ * The parameter key is {@link IssueRegistrationAccessTokenArguments#URL_PARAM_LIFETIME}.
+ */
+public class DefaultRegistrationTokenLifetimeLookupFunction implements Function<ProfileRequestContext, Duration> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public Duration apply(final @Nonnull ProfileRequestContext profileRequestContext) {
+        final SpringRequestContext springRequestContext =
+                profileRequestContext.getSubcontext(SpringRequestContext.class);
+        if (springRequestContext == null) {
+            return null;
+        }
+
+        final RequestContext requestContext = springRequestContext.getRequestContext();
+        if (requestContext == null) {
+            return null;
+        }
+
+        final String lifetime = (String) requestContext.getFlowScope().get(
+                IssueRegistrationAccessTokenArguments.URL_PARAM_LIFETIME);
+        try {
+            return lifetime == null ? null : Duration.parse(lifetime);
+        } catch (final DateTimeParseException e) {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenRelyingPartyIdLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenRelyingPartyIdLookupFunction.java
new file mode 100644
index 00000000..7be97fce
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRegistrationTokenRelyingPartyIdLookupFunction.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.cli.IssueRegistrationAccessTokenArguments;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+
+/**
+ * A lookup function that fetches the relying party identifier from the SWF request parameters.
+ * 
+ * The parameter key is {@link IssueRegistrationAccessTokenArguments#URL_PARAM_RELYING_PARTY_ID}.
+ */
+public class DefaultRegistrationTokenRelyingPartyIdLookupFunction implements Function<ProfileRequestContext, String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(final @Nonnull ProfileRequestContext profileRequestContext) {
+        final SpringRequestContext springRequestContext =
+                profileRequestContext.getSubcontext(SpringRequestContext.class);
+        if (springRequestContext == null) {
+            return null;
+        }
+
+        final RequestContext requestContext = springRequestContext.getRequestContext();
+        if (requestContext == null) {
+            return null;
+        }
+
+        return (String) requestContext.getFlowScope().get(
+                IssueRegistrationAccessTokenArguments.URL_PARAM_RELYING_PARTY_ID);
+    }
+}
\ No newline at end of file
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 80025284..36b87bd2 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
@@ -263,4 +263,21 @@
         </property>
     </bean>
 
+    <bean parent="shibboleth.AdminFlow"
+            c:id="http://shibboleth.net/ns/profiles/oidc/issue-registration-access-token"
+            p:loggingId="%{idp.oidc.issue-registration-access-token.logging:IssueRegistrationAccessToken}"
+            p:policyName="%{idp.oidc.issue-registration-access-token.accessPolicy:AccessByIPAddress}"
+            p:nonBrowserSupported="%{idp.oidc.issue-registration-access-token.nonBrowserSupported:false}"
+            p:authenticated="%{idp.oidc.issue-registration-access-token.authenticated:false}"
+            p:resolveAttributes="%{idp.oidc.issue-registration-access-token.resolveAttributes:false}">
+        <property name="postAuthenticationFlows">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.oidc.issue-registration-access-token.postAuthenticationFlows:}'.trim()}" />
+        </property>
+        <property name="defaultAuthenticationMethodsByString">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.oidc.issue-registration-access-token.defaultAuthenticationMethods:}'.trim()}" />
+         </property>
+    </bean>
+
 </beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-beans.xml
new file mode 100644
index 00000000..ff0c5e0a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-beans.xml
@@ -0,0 +1,147 @@
+<?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 class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <!-- Profile ID for flow. -->    
+    <bean id="shibboleth.AdminProfileId" class="java.lang.String"
+        c:_0="http://shibboleth.net/ns/profiles/oidc/issue-registration-access-token" />
+
+    <!-- Default operation/resource suppliers for access checks. -->
+    
+    <bean id="shibboleth.AdminOperationLookupStrategy" parent="shibboleth.Functions.Constant" c:target="issue" />
+    
+    <bean id="shibboleth.AdminResourceLookupStrategy" parent="shibboleth.Functions.Constant"
+        c:target="oidc/issue-registration-access-token" />
+    
+    <!-- Work beans. -->
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundTokenResponseMessageContext"
+        scope="prototype" />
+
+    <bean id="IssueRegistrationAccessToken"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.IssueRegistrationAccessToken" scope="prototype"
+        p:httpServletResponse-ref="shibboleth.HttpServletResponse"
+        p:sealer-ref="#{'%{idp.oidc.dynreg.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidc.JSONObjectMapper"
+        p:metadataPolicyLookupStrategy-ref="%{idp.oidc.admin.registration.lookup.policy:shibboleth.oidc.admin.DefaultMetadataPolicyLookupStrategy}"
+        p:issuerLookupStrategy-ref="%{idp.oidc.admin.registration.lookup.issuer:shibboleth.oidc.admin.DefaultIssuerLookupStrategy}"/>
+
+    <bean id="shibboleth.oidc.admin.DefaultIssuerLookupStrategy" parent="shibboleth.Functions.Constant"
+        c:target="%{idp.oidc.issuer:%{idp.entityID:}}" />
+        
+    <bean id="shibboleth.oidc.admin.DefaultMetadataPolicyLookupStrategy"
+        class="net.shibboleth.oidc.profile.config.navigate.ResolverBasedRegistrationMetadataPolicyLookupFunction"
+        p:metadataPolicyResolver-ref="shibboleth.oidc.admin.DefaultMetadataPolicyResolver" 
+        p:criteriaSetLookupStrategy-ref="shibboleth.oidc.admin.MetadataPolicyCriteriaLookupFunction"/>
+
+    <bean id="shibboleth.oidc.admin.MetadataPolicyCriteriaLookupFunction"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultMetadataPolicyCriteriaLookupFunction" />
+
+    <bean id="shibboleth.oidc.admin.BaseMetadataPolicyResolver" abstract="true"
+        class="net.shibboleth.oidc.metadata.policy.impl.OIDCMetadataPolicyResolver" />
+
+    <bean id="shibboleth.oidc.admin.DefaultMetadataPolicyResolver"
+        parent="shibboleth.oidc.admin.BaseMetadataPolicyResolver">
+        <constructor-arg>
+            <bean parent="shibboleth.oidc.admin.DynamicCacheBuilder">
+                <constructor-arg>
+                    <bean p:cacheId="AdminMetadataPolicyeCache" parent="shibboleth.oidc.admin.BaseDynamicCacheBuilderSpec" />
+                </constructor-arg>
+            </bean>
+        </constructor-arg>
+    </bean>
+    
+    <bean id="shibboleth.oidc.admin.MetadataPolicyValidator"
+        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyValidator" />
+
+    <bean id="shibboleth.oidc.admin.ParsingStrategy"
+        class="net.shibboleth.oidc.metadata.cache.impl.DefaultJSONMapParsingStrategy"
+        c:valueClass="net.shibboleth.oidc.metadata.policy.MetadataPolicy"/>
+
+    <bean id="shibboleth.oidc.admin.CriteriaToIdentifierStrategy"
+       parent="shibboleth.Functions.Constant" c:target="AdminFlowProvidedMetadataPolicy" />
+
+    <bean id="shibboleth.oidc.admin.ExpirationTimeStrategy" parent="shibboleth.Functions.Constant"
+        c:target="#{T(java.time.Instant).now()}" />
+
+    <bean id="shibboleth.oidc.admin.MetadataPolicyIdentifierExtractionStrategy"
+       parent="shibboleth.Functions.Constant" c:target="AdminFlowProvidedMetadataPolicy" />
+        
+    <bean id="dynamicCacheFactory" class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilder$Builder"/>
+    
+    <bean id="shibboleth.oidc.admin.DynamicCacheBuilder" factory-bean="dynamicCacheFactory" factory-method="build"
+        abstract="true"/>
+    
+    <bean class="net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCacheBuilderSpec" 
+        id="shibboleth.oidc.admin.BaseDynamicCacheBuilderSpec" abstract="true"        
+        p:fetchStrategy-ref="shibboleth.oidc.admin.MetadataPolicyFetchingStrategy" 
+        p:criteriaToIdentifierStrategy-ref="shibboleth.oidc.admin.CriteriaToIdentifierStrategy" 
+        p:metadataExpirationTimeStrategy-ref="shibboleth.oidc.admin.ExpirationTimeStrategy" 
+        p:identifierExtractionStrategy-ref="shibboleth.oidc.admin.MetadataPolicyIdentifierExtractionStrategy" 
+       />
+       
+    <bean id="shibboleth.oidc.admin.MetadataPolicyFetchingStrategy"
+        class="net.shibboleth.oidc.metadata.policy.impl.MetadataPolicyViaLocationFetchingStrategy"
+        c:client-ref="shibboleth.InternalHttpClient"
+        p:parsingStrategy-ref="shibboleth.oidc.admin.ParsingStrategy"
+        c:handler-ref="shibboleth.oidc.admin.DefaultMetadataPolicyResponseHandler"/>
+
+    <bean id="shibboleth.oidc.admin.DefaultMetadataPolicyResponseHandler"
+        class="net.shibboleth.oidc.metadata.policy.impl.DefaultMetadataPolicyResponseHandler"
+        p:parsingStrategy-ref="shibboleth.oidc.admin.ParsingStrategy" />
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundTokenResponseMessage" scope="prototype" />
+
+    <bean id="oidc.messageEncoderFactory"
+        class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.OIDCResponseEncoderFactory"
+        p:messageEncoder-ref="oidc.nimbusEncoder" scope="prototype" />
+
+    <bean id="oidc.nimbusEncoder" class="net.shibboleth.idp.plugin.oidc.op.encoding.impl.NimbusResponseEncoder"
+        scope="prototype" p:httpServletResponse-ref="shibboleth.HttpServletResponse" init-method=""
+        p:velocityEngine-ref="shibboleth.VelocityEngine" />
+
+    <bean id="EncodeMessage" class="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
+        p:messageEncoderFactory-ref="oidc.messageEncoderFactory"
+        p:httpServletResponse-ref="shibboleth.HttpServletResponse" />
+
+    <bean id="RecordResponseComplete" class="net.shibboleth.idp.profile.impl.RecordResponseComplete"
+        scope="prototype" />
+
+    <bean id="LogEvent" class="org.opensaml.profile.action.impl.LogEvent" scope="prototype"
+        p:suppressedEvents="#{getObject('shibboleth.SuppressedEvents') ?: getObject('shibboleth.DefaultSuppressedEvents')}">
+        <property name="eventContextLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+        </property>
+    </bean>
+    
+    <bean id="PostDecodePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidc.PostDecodeAuditExtractors') ?: getObject('shibboleth.oidc.DefaultPostDecodeAuditExtractors')}" />
+
+    <bean id="PostLookupPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidc.PostLookupAuditExtractors') ?: getObject('shibboleth.DefaultPostLookupAuditExtractors')}" />
+
+    <bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.oidc.PostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultPostResponseAuditExtractorsForFlow') ?: getObject('shibboleth.oidc.DefaultPostResponseAuditExtractors')}" />
+        
+    <bean id="WriteAuditLog" class="net.shibboleth.idp.profile.audit.impl.WriteAuditLog" scope="prototype"
+        p:formattingMap-ref="shibboleth.AuditFormattingMap"
+        p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+        p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest" />
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-flow.xml
new file mode 100644
index 00000000..42f2dcd7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/issue-registration-access-token/issue-registration-access-token-flow.xml
@@ -0,0 +1,51 @@
+<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"
+    parent="admin.abstract">
+
+    <!-- Start action. -->
+
+    <on-start>
+        <!-- Extract the parameters in case authentication disturbs the URL. -->
+        <evaluate expression="T(net.shibboleth.utilities.java.support.primitive.StringSupport).trimOrNull(externalContext.getNativeRequest().getParameter('tokenLifetime'))" result="flowScope.tokenLifetime" />
+        <evaluate expression="T(net.shibboleth.utilities.java.support.primitive.StringSupport).trimOrNull(externalContext.getNativeRequest().getParameter('metadataPolicyLocation'))" result="flowScope.metadataPolicyLocation" />
+        <evaluate expression="T(net.shibboleth.utilities.java.support.primitive.StringSupport).trimOrNull(externalContext.getNativeRequest().getParameter('relyingPartyId'))" result="flowScope.relyingPartyId" />
+    </on-start>
+    
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to determine if authentication is required. -->
+        <transition on="proceed" to="DoAdminPreamble" />
+    </action-state>
+
+    <!-- Resume actual flow processing. -->
+
+    <action-state id="DoProfileWork">
+        <evaluate expression="CheckAccess" />
+        <evaluate expression="IssueRegistrationAccessToken" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CommitResponse" />
+    </action-state>
+    
+    <!-- Terminus -->
+
+    
+    <!-- end state -->
+    <end-state id="CommitResponse">
+        <on-entry>
+            <evaluate expression="EncodeMessage" />
+            <evaluate expression="PostResponsePopulateAuditContext" />
+            <evaluate expression="WriteAuditLog" />
+            <evaluate expression="RecordResponseComplete" />
+        </on-entry>
+    </end-state>
+
+    <bean-import resource="issue-registration-access-token-beans.xml" />
+    <bean-import resource="../../../oidc/abstract/oidc-abstract-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
index 1ed13bce..58358f64 100644
--- a/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
+++ b/idp-oidc-extension-impl/src/main/resources/net/shibboleth/idp/plugin/oidc/op/conf/oidc.properties
@@ -111,3 +111,7 @@ idp.oidc.subject.salt = this_too_should_be_ch4ng3d
 # Regular expression matching OAuth login flows to enable.
 # For most deployments, the default is sufficient to accomodate a variety of methods
 #idp.oauth2.authn.flows = OAuth2Client
+
+# Beans to be used by the issue-registration-access-token flow
+#idp.oidc.admin.registration.function.policy = shibboleth.oidc.admin.DefaultMetadataPolicyLookupStrategy
+#idp.oidc.admin.registration.function.issuer = shibboleth.oidc.admin.DefaultIssuerLookupStrategy
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssueRegistrationAccessTokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssueRegistrationAccessTokenFlowTest.java
new file mode 100644
index 00000000..ee815276
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssueRegistrationAccessTokenFlowTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.springframework.webflow.execution.FlowExecutionOutcome;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+
+import net.shibboleth.idp.plugin.oidc.op.cli.IssueRegistrationAccessTokenArguments;
+
+/**
+ * issue-registration-access-token flow test.
+ */
+public class IssueRegistrationAccessTokenFlowTest extends AbstractOidcFlowTest {
+
+    /** The flow id. */
+    @Nonnull public final static String FLOW_ID = "admin/oidc/issue-registration-access-token";
+
+    public IssueRegistrationAccessTokenFlowTest() {
+        super(FLOW_ID);
+    }
+
+    /**
+     * Test the flow without any parameters.
+     * 
+     * @throws Exception if an error occurs
+     */
+    @Test
+    public void testWithNoParameters() throws Exception {
+
+        buildRequest(null, null, null);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+        final FlowExecutionOutcome outcome = result.getOutcome();
+        //assertEquals(response.getStatus(), 300);
+        //assertEquals(outcome.getId(), "ErrorView");
+        //TODO: implement the error handling to the flow
+    }
+
+    /**
+     * Test the flow with valid parameters.
+     * 
+     * @throws Exception if an error occurs
+     */
+    @Test
+    public void testWithValidParameters() throws Exception {
+
+        buildRequest("P1D", "src/test/resources/conf/metadata-policy1.json", "mockRpId");
+        
+        initializeThreadLocals();
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+        final FlowExecutionOutcome outcome = result.getOutcome();
+        assertEquals(response.getStatus(), 200);
+        assertEquals(outcome.getId(), "CommitResponse");
+        final AccessTokenResponse response = parseSuccessResponse(result, AccessTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        
+        //TODO: verify the contents of the access token
+    }
+
+    private void buildRequest(@Nullable final String lifetime, @Nullable final String metadataLocation,
+            @Nullable final String relyingPartyId)
+            throws UnsupportedEncodingException {
+        request.setMethod("GET");
+        if (lifetime != null) {
+            request.addParameter(IssueRegistrationAccessTokenArguments.URL_PARAM_LIFETIME,
+                    URLEncoder.encode(lifetime, "UTF-8"));
+        }
+        if (metadataLocation != null) {
+            request.addParameter(IssueRegistrationAccessTokenArguments.URL_PARAM_METADATA_POLICY_LOCATION,
+                    metadataLocation);
+        }
+        if (relyingPartyId != null) {
+            request.addParameter(IssueRegistrationAccessTokenArguments.URL_PARAM_RELYING_PARTY_ID, relyingPartyId);
+        }
+    }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessTokenTest.java
new file mode 100644
index 00000000..c093361e
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/IssueRegistrationAccessTokenTest.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.oauth2.sdk.AccessTokenResponse;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+
+import net.shibboleth.idp.plugin.oidc.op.cli.IssueRegistrationAccessTokenArguments;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RegistrationClaimsSet;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.metadata.policy.MetadataPolicy;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.security.DataSealer;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+/**
+ * Unit tests for {@link IssueRegistrationAccessToken}.
+ */
+public class IssueRegistrationAccessTokenTest {
+    
+    private IssueRegistrationAccessToken action;
+    
+    private RequestContext requestCtx;
+
+    private ProfileRequestContext prc;
+    
+    private DataSealer dataSealer;
+    
+    private ObjectMapper objectMapper;
+    
+    private String issuer = "mockIssuer";
+    
+    private String relyingPartyId = "tokenRpId";
+    
+    private String lifetime = "P1D";
+    
+    @BeforeMethod
+    public void init() throws ComponentInitializationException, NoSuchAlgorithmException {
+        dataSealer = BaseOIDCResponseActionTest.initializeDataSealer();
+        objectMapper = new ObjectMapper();
+        action = new IssueRegistrationAccessToken();
+        action.setObjectMapper(objectMapper);
+        action.setMetadataPolicyLookupStrategy(FunctionSupport.constant(defaultMetadataPolicy()));
+        action.setSealer(dataSealer);
+        action.setIssuerLookupStrategy(FunctionSupport.constant(issuer));
+        action.initialize();
+        requestCtx = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+    }
+    
+    protected Map<String, MetadataPolicy> defaultMetadataPolicy() {
+        final Map<String, MetadataPolicy> policy = new HashMap<>();
+        policy.put("claim1", new MetadataPolicy.Builder().withAdd("addValue").build());
+        return policy;
+    }
+
+    @Test(expectedExceptions = { ComponentInitializationException.class })
+    public void testNoSealer() throws ComponentInitializationException {
+        action = new IssueRegistrationAccessToken();
+        action.setObjectMapper(objectMapper);
+        action.setMetadataPolicyLookupStrategy(FunctionSupport.constant(defaultMetadataPolicy()));
+        action.setIssuerLookupStrategy(FunctionSupport.constant(issuer));
+        action.initialize();
+    }
+
+    @Test(expectedExceptions = { ComponentInitializationException.class })
+    public void testNoObjectMapper() throws ComponentInitializationException {
+        action = new IssueRegistrationAccessToken();
+        action.setSealer(dataSealer);
+        action.setMetadataPolicyLookupStrategy(FunctionSupport.constant(defaultMetadataPolicy()));
+        action.setIssuerLookupStrategy(FunctionSupport.constant(issuer));
+        action.initialize();
+    }
+
+    @Test(expectedExceptions = { ComponentInitializationException.class })
+    public void testNoMetadataPolicyLookup() throws ComponentInitializationException {
+        action = new IssueRegistrationAccessToken();
+        action.setSealer(dataSealer);
+        action.setObjectMapper(objectMapper);
+        action.setIssuerLookupStrategy(FunctionSupport.constant(issuer));
+        action.initialize();
+    }
+
+    @Test(expectedExceptions = { ComponentInitializationException.class })
+    public void testNoIssuerLookup() throws ComponentInitializationException {
+        action = new IssueRegistrationAccessToken();
+        action.setSealer(dataSealer);
+        action.setMetadataPolicyLookupStrategy(FunctionSupport.constant(defaultMetadataPolicy()));
+        action.setObjectMapper(objectMapper);
+        action.initialize();
+    }
+
+    @Test
+    public void testNoMetadataPolicy() throws ComponentInitializationException {
+        action = new IssueRegistrationAccessToken();
+        action.setSealer(dataSealer);
+        action.setObjectMapper(objectMapper);
+        action.setMetadataPolicyLookupStrategy(FunctionSupport.constant(null));
+        action.setIssuerLookupStrategy(FunctionSupport.constant(issuer));
+        action.initialize();
+        requestCtx.getFlowScope().put(IssueRegistrationAccessTokenArguments.URL_PARAM_LIFETIME, lifetime);
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);        
+    }
+    
+    @Test
+    public void testNoTokenLifetime() {
+        requestCtx.getFlowScope().put(IssueRegistrationAccessTokenArguments.URL_PARAM_RELYING_PARTY_ID,
+                relyingPartyId);
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+    }
+
+    @Test
+    public void testNoRelyingPartyId() {
+        requestCtx.getFlowScope().put(IssueRegistrationAccessTokenArguments.URL_PARAM_LIFETIME, lifetime);
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+    }
+
+    @Test
+    public void testSuccess() throws DataSealerException, JsonMappingException, JsonProcessingException {
+        requestCtx.getFlowScope().put(IssueRegistrationAccessTokenArguments.URL_PARAM_LIFETIME, lifetime);
+        requestCtx.getFlowScope().put(IssueRegistrationAccessTokenArguments.URL_PARAM_RELYING_PARTY_ID,
+                relyingPartyId);
+        final Instant start = Instant.now();
+        final Event event = action.execute(requestCtx);
+        ActionTestingSupport.assertProceedEvent(event);
+        final Object rawMessage = prc.getOutboundMessageContext().getMessage();
+        Assert.assertNotNull(rawMessage);
+        Assert.assertTrue(rawMessage instanceof AccessTokenResponse);
+        final AccessTokenResponse tokenResponse = (AccessTokenResponse) rawMessage;
+        Assert.assertNotNull(tokenResponse.getTokens());
+        final BearerAccessToken accessToken = tokenResponse.getTokens().getBearerAccessToken();
+        Assert.assertNotNull(accessToken);
+        final String decryptedToken = dataSealer.unwrap(accessToken.getValue());
+        final RegistrationClaimsSet claimsSet = objectMapper.readValue(decryptedToken, RegistrationClaimsSet.class);
+        Assert.assertEquals(claimsSet.getKeyType(), "rt");
+        Assert.assertNotNull(claimsSet.getJti());
+        Assert.assertEquals(claimsSet.getIssuer(), issuer);
+        Assert.assertEquals(claimsSet.getRelyingPartyId(), relyingPartyId);
+        final Map<String, MetadataPolicy> tokenPolicy = claimsSet.getMetadata();
+        Assert.assertNotNull(tokenPolicy);
+        Assert.assertEquals(tokenPolicy.size(), 1);
+        Assert.assertTrue(tokenPolicy.containsKey("claim1"));
+        final MetadataPolicy claimPolicy = tokenPolicy.get("claim1");
+        Assert.assertEquals(claimPolicy.getAdd(), "addValue");
+        assertInstantWithSkew(claimsSet.getIssuedAt(), start);
+        assertInstantWithSkew(claimsSet.getExpiration(), start.plus(Duration.ofDays(1)));
+    }
+
+    protected void assertInstantWithSkew(final Instant instant, final Instant target) {
+        final Duration skew = Duration.ofSeconds(5);
+        Assert.assertTrue(instant.isAfter(target.minus(skew)));
+        Assert.assertTrue(instant.isBefore(target.plus(skew)));
+    }
+
+}

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


More information about the commits mailing list