[java-idp-oidc] branch main updated: JOIDC-83 - Admin flow to read/delete client registrations
Scott Cantor
cantor.2 at osu.edu
Mon Mar 21 17:30:31 UTC 2022
This is an automated email from the git hooks/post-receive script.
scantor 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=96961a6ddb31198cea990e6b40e51df7763e5fae
The following commit(s) were added to refs/heads/main by this push:
new 96961a6d JOIDC-83 - Admin flow to read/delete client registrations
96961a6d is described below
commit 96961a6ddb31198cea990e6b40e51df7763e5fae
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Mar 21 13:30:28 2022 -0400
JOIDC-83 - Admin flow to read/delete client registrations
https://shibboleth.atlassian.net/browse/JOIDC-83
---
.../op/admin/impl/DoClientManagementOperation.java | 252 +++++++++++++++++++++
.../plugin/oidc/op/admin/impl/package-info.java | 21 ++
.../META-INF/net.shibboleth.idp/postconfig.xml | 23 ++
.../idp/flows/admin/oidc/clients/clients-beans.xml | 46 ++++
.../idp/flows/admin/oidc/clients/clients-flow.xml | 40 ++++
.../idp/flows/oidc/register/register-beans.xml | 5 -
6 files changed, 382 insertions(+), 5 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/DoClientManagementOperation.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/DoClientManagementOperation.java
new file mode 100644
index 00000000..57c832d7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/DoClientManagementOperation.java
@@ -0,0 +1,252 @@
+/*
+ * 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.admin.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+import net.shibboleth.oidc.metadata.ClientInformationManager;
+import net.shibboleth.oidc.metadata.ClientInformationManagerException;
+import net.shibboleth.oidc.metadata.ClientInformationResolver;
+import net.shibboleth.oidc.metadata.criterion.ClientIDCriterion;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+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.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+
+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.databind.ObjectMapper;
+import com.github.jasminb.jsonapi.models.errors.Error;
+import com.github.jasminb.jsonapi.models.errors.Errors;
+import com.google.common.base.Strings;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+/**
+ * Action that implements a JSON REST API for querying and deleting OIDC client information.
+ *
+ * <p>The API supports GET and DELETE at the moment, using jsonapi.org conventions.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ *
+ * @since 3.1.0
+ */
+public class DoClientManagementOperation extends AbstractProfileAction {
+
+ /** Flow variable indicating ID of storage key. */
+ @Nonnull @NotEmpty public static final String CLIENT_ID = "clientId";
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DoClientManagementOperation.class);
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** {@link ClientInformationResolver} to operate on. */
+ @NonnullAfterInit private ClientInformationResolver resolver;
+
+ /** {@link ClientInformationManager} to operate on. */
+ @NonnullAfterInit private ClientInformationManager manager;
+
+ /** Client ID to operate on. */
+ @Nullable @NotEmpty private String clientId;
+
+ /**
+ * Set the JSON {@link ObjectMapper} to use for serialization.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+ }
+
+ /**
+ * Set the {@link ClientInformationResolver} to use for retrieval.
+ *
+ * @param theResolver client info resolver
+ */
+ public void setClientInformationResolver(@Nonnull final ClientInformationResolver theResolver) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ resolver = Constraint.isNotNull(theResolver, "ClientInformationResolver cannot be null");
+ }
+
+ /**
+ * Set the {@link ClientInformationManager} to use for deletion.
+ *
+ * @param theManager client info manager
+ */
+ public void setClientInformationManager(@Nonnull final ClientInformationManager theManager) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ manager = Constraint.isNotNull(theManager, "ClientInformationManager cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ } else if (getHttpServletRequest() == null || getHttpServletResponse() == null) {
+ log.warn("{} No HttpServletRequest or HttpServletResponse available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ try {
+ final SpringRequestContext springRequestContext =
+ profileRequestContext.getSubcontext(SpringRequestContext.class);
+ if (springRequestContext == null) {
+ log.warn("{} Spring request context not found in profile request context", getLogPrefix());
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ "Internal Server Error", "System misconfiguration.");
+ return false;
+ }
+
+ final RequestContext requestContext = springRequestContext.getRequestContext();
+ if (requestContext == null) {
+ log.warn("{} Web Flow request context not found in Spring request context", getLogPrefix());
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ "Internal Server Error", "System misconfiguration.");
+ return false;
+ }
+
+ clientId = (String) requestContext.getFlowScope().get(CLIENT_ID);
+ if (Strings.isNullOrEmpty(clientId)) {
+ sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Missing client_id", "No client identifier specified.");
+ return false;
+ }
+
+ } catch (final IOException e) {
+ log.error("{} I/O error issuing API response", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(final ProfileRequestContext profileRequestContext) {
+
+ try {
+ final HttpServletRequest request = getHttpServletRequest();
+ final HttpServletResponse response = getHttpServletResponse();
+
+ response.setContentType("application/json");
+ response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+
+ if ("GET".equals(request.getMethod())) {
+ final OIDCClientInformation record;
+ try {
+ record = resolver.resolveSingle(new CriteriaSet(new ClientIDCriterion(new ClientID(clientId))));
+ if (record != null) {
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getOutputStream().print(record.toJSONObject().toJSONString());
+ } else {
+ sendError(HttpServletResponse.SC_NOT_FOUND,
+ "Record Not Found", "The specified record was not present or has expired.");
+ }
+ } catch (final ResolverException e) {
+ log.error("{} Resolver error looking up client ID {}", getLogPrefix(), clientId, e);
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+ "Client info resolution error.");
+ }
+ } else if ("DELETE".equals(request.getMethod())) {
+ try {
+ manager.destroyClientInformation(new ClientID(clientId));
+ response.setStatus(HttpServletResponse.SC_NO_CONTENT);
+ } catch (final ClientInformationManagerException e) {
+ log.error("{} Error deleting client ID {}", getLogPrefix(), clientId, e);
+ sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+ "ClientInformationManager error.");
+ }
+
+ } else {
+ log.warn("{} Invalid method: {}", getLogPrefix(), request.getMethod());
+ sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
+ "Unknown Operation", "Only GET and DELETE are supported.");
+ }
+
+ } catch (final IOException e) {
+ log.error("{} I/O error responding to request", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ }
+
+ /**
+ * Output an error object.
+ *
+ * @param status HTTP status
+ * @param title fixed error description
+ * @param detail human-readable error description
+ *
+ * @throws IOException if unable to output the error
+ */
+ private void sendError(final int status, @Nonnull @NotEmpty final String title,
+ @Nonnull @NotEmpty final String detail) throws IOException {
+
+ final HttpServletResponse response = getHttpServletResponse();
+ response.setContentType("application/json");
+ response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+ response.setStatus(status);
+
+ final Error e = new Error();
+ final Errors errors = new Errors();
+ errors.setErrors(Collections.singletonList(e));
+ e.setStatus(Integer.toString(status));
+ e.setTitle(title);
+ e.setDetail(detail);
+
+ objectMapper.writer().withDefaultPrettyPrinter().writeValue(response.getOutputStream(), errors);
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/package-info.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/package-info.java
new file mode 100644
index 00000000..efca2572
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/admin/impl/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes implementing administrative functionality.
+ */
+package net.shibboleth.idp.plugin.oidc.op.admin.impl;
\ 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 6ee54d32..87e184ad 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
@@ -106,6 +106,12 @@
class="net.shibboleth.oidc.metadata.impl.ReloadingRelyingPartyClientInformationProvider"
c:resolverService-ref="shibboleth.ClientInformationResolverService" />
+ <!-- Used to storage/mgmt of client registrations. -->
+
+ <bean id="shibboleth.oidc.ClientInformationManager"
+ class="net.shibboleth.oidc.metadata.impl.StorageServiceClientInformationManager" scope="prototype"
+ p:storageService-ref="#{'%{idp.oidc.dynreg.StorageService:shibboleth.StorageService}'.trim()}" />
+
<!-- Necessary for encoder parsing and claims mapping to function. -->
<bean parent="shibboleth.RegistryNamingFunction" c:claz="net.minidev.json.JSONObject">
@@ -285,6 +291,23 @@
</property>
</bean>
+ <bean parent="shibboleth.AdminFlow"
+ c:id="http://shibboleth.net/ns/profiles/oidc/clients"
+ p:loggingId="%{idp.oidc.admin.clients.logging:ClientManagement}"
+ p:policyName="%{idp.oidc.admin.clients.accessPolicy:AccessByIPAddress}"
+ p:nonBrowserSupported="%{idp.oidc.clients.registration.nonBrowserSupported:true}"
+ p:authenticated="%{idp.oidc.admin.clients.authenticated:false}"
+ p:resolveAttributes="%{idp.oidc.admin.clients.resolveAttributes:false}">
+ <property name="postAuthenticationFlows">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.oidc.admin.clients.postAuthenticationFlows:}'.trim()}" />
+ </property>
+ <property name="defaultAuthenticationMethodsByString">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.oidc.admin.clients.defaultAuthenticationMethods:}'.trim()}" />
+ </property>
+ </bean>
+
<alias name="%{idp.oidc.ResponseHeaderFilter:shibboleth.oidc.EmptyResponseHeaderFilter}"
alias="shibboleth.oidc.ResponseHeaderFilter" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-beans.xml
new file mode 100644
index 00000000..994eb232
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-beans.xml
@@ -0,0 +1,46 @@
+<?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/clients" />
+
+ <!-- Default operation/resource suppliers for access checks. -->
+
+ <bean id="shibboleth.AdminOperationLookupStrategy" parent="shibboleth.ContextFunctions.Expression"
+ c:outputType="#{T(java.lang.String)}"
+ c:expression="#custom.getMethod()"
+ p:customObject-ref="shibboleth.HttpServletRequest" />
+
+ <bean id="shibboleth.AdminResourceLookupStrategy" parent="shibboleth.ContextFunctions.Expression"
+ c:outputType="#{T(java.lang.String)}"
+ c:expression="#input.getSubcontext(T(net.shibboleth.idp.profile.context.SpringRequestContext)).getRequestContext().getFlowScope().get('clientId')" />
+
+ <!-- Work beans. -->
+
+ <bean id="DoClientManagementOperation"
+ class="net.shibboleth.idp.plugin.oidc.op.admin.impl.DoClientManagementOperation" scope="prototype"
+ p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+ p:httpServletResponse-ref="shibboleth.HttpServletResponse"
+ p:objectMapper-ref="shibboleth.JSONObjectMapper"
+ p:clientInformationResolver-ref="shibboleth.ClientInformationResolver"
+ p:clientInformationManager-ref="shibboleth.oidc.ClientInformationManager" />
+
+ <bean id="RecordResponseComplete" class="net.shibboleth.idp.profile.impl.RecordResponseComplete"
+ scope="prototype" />
+
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-flow.xml
new file mode 100644
index 00000000..ed96cc60
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/oidc/clients/clients-flow.xml
@@ -0,0 +1,40 @@
+<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">
+
+ <on-start>
+ <!-- Extract 'id' parameter in case authentication disturbs the URL. -->
+ <evaluate expression="T(net.shibboleth.utilities.java.support.primitive.StringSupport).trimOrNull(externalContext.getNativeRequest().getParameter('client_id'))" result="flowScope.clientId" />
+ </on-start>
+
+ <!-- Start action. -->
+
+ <action-state id="InitializeProfileRequestContext">
+ <evaluate expression="InitializeProfileRequestContext" />
+ <evaluate expression="FlowStartPopulateAuditContext" />
+ <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="WriteAuditLog" />
+ <evaluate expression="DoClientManagementOperation" />
+ <evaluate expression="RecordResponseComplete" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="end" />
+ </action-state>
+
+ <!-- Terminus -->
+
+ <end-state id="end" />
+
+ <bean-import resource="clients-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
index e2ca1965..c01b8454 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
@@ -152,11 +152,6 @@
<bean id="AddRemainingClaimsToClientMetadata"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddRemainingClaimsToClientMetadata" />
- <bean id="shibboleth.oidc.ClientInformationManager"
- class="net.shibboleth.oidc.metadata.impl.StorageServiceClientInformationManager"
- scope="prototype" p:id="OIDCClientInformationManager"
- p:storageService-ref="#{'%{idp.oidc.dynreg.StorageService:shibboleth.StorageService}'.trim()}" />
-
<bean id="StoreClientInformation"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.StoreClientInformation" scope="prototype"
p:clientInformationManager-ref="shibboleth.oidc.ClientInformationManager">
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list