[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