[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