[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage other users keys

Phil Smart philip.smart at jisc.ac.uk
Fri Apr 26 15:58:37 UTC 2024


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-idp-plugin-webauthn.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-webauthn.git;a=commit;h=fead4a6984ad8571a7399be6ca923bf2ef6fc41e

The following commit(s) were added to refs/heads/main by this push:
     new fead4a6  JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage other users keys
fead4a6 is described below

commit fead4a6984ad8571a7399be6ca923bf2ef6fc41e
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Apr 26 16:58:34 2024 +0100

    JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage
    other users keys
    
     - A working WiP for the admin UI to allow an admin to remove other
    users credential registrations.
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-8
---
 .../admin/WebAuthnRegistrationEventIds.java        |   3 +
 .../context/WebAuthnManagementContext.java         | 142 ++++++++++++++++++
 .../navigate/UsernameLookupFromSubjectContext.java |   2 +-
 .../StorageServiceCredentialRepository.java        |  21 +++
 .../impl/AbstractWebAuthnManagementAction.java     | 138 ++++++++++++++++++
 .../impl/AbstractWebAuthnRegistrationAction.java   |  95 +-----------
 ...al.java => AdminDeletePublicKeyCredential.java} |  48 +++----
 ...xtractKeyRemovalInformationFromFormRequest.java | 105 ++++++++++++++
 .../admin/impl/DeletePublicKeyCredential.java      |   2 +
 .../impl/ExtractUsernameSearchFromFormRequest.java |  96 +++++++++++++
 .../admin/impl/LookupCredentialsForUser.java       |  64 +++++++++
 .../impl/PopulateWebAuthnManagementContext.java    | 112 +++++++++++++++
 .../admin/impl/StorePublicKeyCredential.java       |  41 ------
 .../impl/AbstractWebAuthnAuthenticationAction.java |   1 +
 .../webauthn/impl/AbstractWebAuthnBaseAction.java  |  68 +--------
 .../authn/webauthn/impl/BaseWebAuthnAction.java    | 116 +++++++++++++++
 .../IdPStorageServiceCredentialRespository.java    |  90 +++++++++++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  24 +++-
 .../webauthn-management-beans.xml                  |  58 ++++++++
 .../webauthn-management-flow.xml}                  | 106 ++++++--------
 .../webauthn-registration-beans.xml                |   7 +-
 .../webauthn-registration-flow.xml                 |   2 +-
 .../authn/webauthn/conf/authn/webauthn.properties  |  10 ++
 .../idp/plugin/authn/webauthn/module.properties    |  10 +-
 .../webauthn/views/webauthn-authn-username.vm      |   2 +-
 .../plugin/authn/webauthn/views/webauthn-authn.vm  |   2 +-
 .../{webauthn-register-end.vm => webauthn-end.vm}  |  32 ++++-
 .../webauthn/views/webauthn-management-search.vm   |  71 +++++++++
 .../authn/webauthn/views/webauthn-management.vm    | 125 ++++++++++++++++
 .../authn/webauthn/views/webauthn-register.vm      |   2 +-
 ...IdPStorageServiceCredentialRespositoryTest.java | 159 +++++++++++++++++++++
 .../storage/impl/InMemoryRegistrationStorage.java  |  15 ++
 32 files changed, 1463 insertions(+), 306 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
index 0985ba7..e5dcb16 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
@@ -35,5 +35,8 @@ public final class WebAuthnRegistrationEventIds {
     
     /** Registration context is invalid. */
     @Nonnull @NotEmpty public static final String INVALID_REGISTRATION_CTX = "InvalidRegistrationContext";
+    
+    /** Management context is invalid. */
+    @Nonnull @NotEmpty public static final String INVALID_MANAGEMENT_CTX = "InvalidManagmentContext";
 
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java
new file mode 100644
index 0000000..7835c80
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A WebAuthn admin management context for the management admin flow.
+ */
+public class WebAuthnManagementContext extends BaseContext {
+    
+    /** Canonical principal name of subject. */
+    @Nullable private String principalName;  
+    
+    /** The username of the user to find and manage credentials for. */
+    @Nullable private String searchUsername;    
+    
+    /** The set of credentials registered for the given searchUsername.*/
+    @Nullable @Unmodifiable @NotLive private Collection<CredentialRegistration> foundCredentials;
+    
+    /** The ID of the credential that is going to be removed.*/
+    @Nullable private byte[] credentialIdToRemove;  
+    
+    /**
+     * Get the canonical principal name of the subject.
+     * 
+     * @return the canonical principal name
+     */
+    @Nullable public String getPrincipalName() {
+        return principalName;
+    }
+
+    /**
+     * Set the canonical principal name of the subject.
+     * 
+     * @param name the canonical principal name
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnManagementContext setPrincipalName(@Nullable final String name) {
+        principalName = name;
+        
+        return this;
+    }
+    
+    /**
+     * Get the username of the user to find and manage credentials for.
+     * 
+     * @return the username
+     */
+    @Nullable public String getSearchUsername() {
+        return searchUsername;
+    }
+
+    /**
+     * Set the username of the user to find and manage credentials for.
+     * 
+     * @param name the username
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnManagementContext setSearchUsername(@Nullable final String name) {
+        searchUsername = name;
+        
+        return this;
+    }
+    
+    /**
+     * Set the credentials found to belong to the given search user.
+     * 
+     * @param credentials the set of credentials
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnManagementContext setFoundCredentials(
+            @Nullable final Collection<CredentialRegistration> credentials) {
+        if (credentials == null) {
+            foundCredentials = CollectionSupport.emptyList();
+        } else {
+            foundCredentials = CollectionSupport.copyToList(credentials);
+        }
+        return this;
+    }
+    
+    /**
+     * Get the credentials found to belong to the given search user.
+     * 
+     * @return the credentials.
+     */
+    @Nonnull @Unmodifiable @NotLive public Collection<CredentialRegistration> getFoundCredentials() {
+        final Collection<CredentialRegistration> localFoundCredentials = foundCredentials;
+        if (localFoundCredentials == null) {
+            return CollectionSupport.emptyList();
+        }
+        return localFoundCredentials;
+    }
+    
+    /**
+     * Set the ID of the credential that is going to be removed.
+     * 
+     * @param id the credential identifier
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnManagementContext setCredentialIdToRemove(@Nullable final byte[] id) {
+        credentialIdToRemove = id;
+        return this;
+    }
+    
+    /**
+     * Get the ID of the credential that is going to be removed.
+     * 
+     * @return Returns the credentialIdToRemove.
+     */
+    @Nullable public byte[] getCredentialIdToRemove() {
+        return credentialIdToRemove;
+    }
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
index 18393ab..c2b1384 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
@@ -26,7 +26,7 @@ import net.shibboleth.idp.authn.context.SubjectContext;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Pull out a username from a the {@link SubjectContext#getPrincipalName()} if it exists.
+ * Pull out a username/principalName from the {@link SubjectContext#getPrincipalName()} if it exists.
  */
 public class UsernameLookupFromSubjectContext implements Function<ProfileRequestContext, String> {
     
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
index 16cbb1b..dc6a754 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
@@ -24,6 +24,7 @@ import com.yubico.webauthn.data.ByteArray;
 
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 
 /**
  * An extension of the {@link CredentialRepository} interface to support additional read and add operations.
@@ -86,4 +87,24 @@ public interface StorageServiceCredentialRepository extends CredentialRepository
      */
     boolean removeRegistrationByUsername(@Nonnull final String username, 
             @Nonnull final CredentialRegistration credentialRegistration);
+    
+    
+    /**
+     * Remove the credential by its identifier for any user it is registered for. This should, in practice, only be
+     * registered to a single user.
+     * 
+     * @param credentialId the credential to remove
+     * 
+     * @return the number of credentials that have been removed. Should always be 1 if removed, 0 if not.
+     */
+    int removeRegistrationByCredentialId(@Nonnull final ByteArray credentialId);    
+    
+    
+    /** 
+     * Get all credential registrations known to the repository. Typically used to provide an administrative view
+     * of the repository.
+     * 
+     * @return all credential registrations know to the system.
+     */
+    @Nonnull @NotLive @Unmodifiable Set<CredentialRegistration> getAllRegistrations();
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java
new file mode 100644
index 0000000..b8c1427
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java
@@ -0,0 +1,138 @@
+/*
+ * 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.authn.webauthn.admin.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.BaseWebAuthnAction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * <p>A base class for WebAuthn administrative management function actions.</p>
+ * 
+ * <p>In addition to the work performed by {@link AbstractProfileAction}, this action also looks up
+ * and makes available the {@link WebAuthnManagementContext}.</p>
+ * 
+ * <p>WebAuthn administrative action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, WebAuthnManagementContext)} method.</p>
+ * 
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
+ */
+public abstract class AbstractWebAuthnManagementAction extends BaseWebAuthnAction {
+    
+    /** Class logger. */
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnManagementAction.class);
+    
+    /** Lookup strategy to locate the webauthn management context. */
+    @Nonnull 
+    private Function<ProfileRequestContext,WebAuthnManagementContext> webauthnManagementContextLookupStrategy;
+    
+    /** The WebAuthn management Context.*/
+    @NonnullBeforeExec private WebAuthnManagementContext webauthnManagementContext;
+    
+    /** Constructor.*/
+    protected AbstractWebAuthnManagementAction() {
+        //prc -> WebAuthnContext
+        webauthnManagementContextLookupStrategy = new ChildContextLookup<>(WebAuthnManagementContext.class);
+    }
+    
+    /**
+     * Set WebAuthn management context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setWebauthnManagementContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,WebAuthnManagementContext> strategy) {
+        checkSetterPreconditions();
+
+        webauthnManagementContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "WebauthnManagementContextLookupStrategy strategy cannot be null");
+    }
+
+    
+    /** {@inheritDoc} */
+    @Override
+    protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+      
+        webauthnManagementContext = webauthnManagementContextLookupStrategy.apply(profileRequestContext);
+        if (webauthnManagementContext == null) {
+            log.warn("{} No WebAuthn management context returned by lookup strategy",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+            
+        }        
+        return doPreExecute(profileRequestContext, webauthnManagementContext);
+    }
+    
+    /**
+     * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnManagementContext)} to perform the 
+     * actual action. Implementations can not override this method.
+     * 
+     * @param profileRequestContext the current IdP profile request context
+     */
+    @Override
+    protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        doExecute(profileRequestContext, webauthnManagementContext);
+    }
+    
+    /**
+     * Performs this admin action's pre-execute step. Default implementation just returns true.
+     * 
+     * @param profileRequestContext the current IdP profile request context
+     * @param context the WebAuthn management context
+     * 
+     * @return true iff execution should continue
+     */
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {
+        return true;
+    }
+    
+    /**
+     * Performs this webauthn authentication action using the supplied WebAuthn management context. Implementations
+     * should override this method.
+     * 
+     * @param profileRequestContext the current IdP profile request context
+     * @param context the WebAuthn management context
+     */
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {
+        
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
index 3b0a83a..4b01b18 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
@@ -28,16 +27,11 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import com.yubico.fido.metadata.FidoMetadataService;
-
-import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.BaseWebAuthnAction;
 import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
@@ -54,7 +48,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
  * @post <pre>AuthenticationContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
  */
-public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfileAction {
+public abstract class AbstractWebAuthnRegistrationAction extends BaseWebAuthnAction {
     
     /** Class logger. */
     @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnRegistrationAction.class);
@@ -65,62 +59,14 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
     
     /** The WebAuthn registration Context.*/
     @NonnullBeforeExec private WebAuthnRegistrationContext webauthnRegistrationContext;
-    
-    /** The WebAuthn client to use.*/
-    @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
-    
-    /** The credential respository to store valid credentials in.*/
-    @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
-    
-    /** Optional FIDO metadata service resolver.*/ 
-    @Nullable private FidoMetadataService fidoMetadataService;    
+
     
     /** Constructor.*/
     protected AbstractWebAuthnRegistrationAction() {
         //prc -> WebAuthnContext
         webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
     }
-    
-    /**
-     * Set the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @param client The webauthnClient to set.
-     */
-    public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
-        checkSetterPreconditions();
-        webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
-    }
-    
-    /**
-     * Get the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @return the webAuthnClient.
-     */
-    @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
-        checkComponentActive();
-        return webAuthnClient;
-    }
-    
-    /**
-     * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
-     * 
-     * @param service The FIDO metadata service to set.
-     */
-    public void setFidoMetadataService(@Nullable final FidoMetadataService service) {
-        checkSetterPreconditions();
-        fidoMetadataService = service;
-    }
-    
-    /**
-     * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
-     * 
-     * @return the fido metadata service.
-     */
-    @Nullable protected FidoMetadataService getFidoMetadataService() {
-        checkComponentActive();
-        return fidoMetadataService;
-    }
-    
+
     /**
      * Set WebAuthn registration context lookup strategy to use.
      * 
@@ -134,39 +80,6 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
                 Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
     }
     
-    
-    /**
-     * Set the credential repository used to store WebAuthn credentials.
-     *  
-     * @param repository The respository to set.
-     */
-    public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
-        checkSetterPreconditions();
-        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
-    }
-    
-    /**
-     * Get the credential repository used to store WebAuthn credentials.
-     * 
-     * @return the credential repository.
-     */
-    @NonnullAfterInit protected StorageServiceCredentialRepository getCredentialRepository() {
-        return credentialRepository;
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (webAuthnClient == null) {
-            throw new ComponentInitializationException("WebAuthn Client can not be null");
-        }
-        if (credentialRepository == null) {
-            throw new ComponentInitializationException("CredentialRepository can not be null");
-        }
-    }
-    
     /** {@inheritDoc} */
     @Override
     protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
similarity index 57%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
index b849c32..0cbe6a7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
@@ -17,8 +17,6 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
-import java.util.Optional;
-
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.ActionSupport;
@@ -28,53 +26,47 @@ import org.slf4j.Logger;
 import com.yubico.webauthn.data.ByteArray;
 
 import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * An action that removes a public key credential from the credential repository.
+ * An action that removes a public key credential from the credential repository based on the credential Id
+ * found in the management context.
  * 
  * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION_CTX}
- * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
  * @post a credential is removed from credential repository
  */
-public class DeletePublicKeyCredential extends AbstractWebAuthnRegistrationAction {
+public class AdminDeletePublicKeyCredential extends AbstractWebAuthnManagementAction {
 
     /** Class logger. */
     @Nonnull
-    private final Logger log = LoggerFactory.getLogger(DeletePublicKeyCredential.class);    
+    private final Logger log = LoggerFactory.getLogger(AdminDeletePublicKeyCredential.class);    
 
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final WebAuthnRegistrationContext context) {
+            @Nonnull final WebAuthnManagementContext context) {
 
-        final String username = context.getUsername();
-        if (username == null) {
-            log.error("{} Unable to find username in registration context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
-            return;
-        }
         
         final byte[] credentialId = context.getCredentialIdToRemove();
         if (credentialId == null) {
             log.error("{} Unable to find credentialId in registration context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext,  WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            ActionSupport.buildEvent(profileRequestContext,  WebAuthnRegistrationEventIds.INVALID_MANAGEMENT_CTX);
             return;
         }
+
+        final int removed = getCredentialRepository().removeRegistrationByCredentialId(new ByteArray(credentialId));
         
-        final Optional<CredentialRegistration> credential = 
-                getCredentialRepository().getRegistrationByUsernameAndCredentialId(
-                        username, new ByteArray(credentialId));
-        
-        if (credential.isEmpty()) {
-            // This is not an error
-            log.debug("{} Unable to find credential to remove, nothing to remove", getLogPrefix());            
-        } else {
-            final boolean removed = getCredentialRepository().removeRegistrationByUsername(username, credential.get());
-            log.debug("{} Credential '{}' {} removed", getLogPrefix(), credential.get().getCredentialIdBase64Url(),
-                    removed ? "was" : "was not");
-        }
+        if (log.isDebugEnabled()) {
+            try {
+                log.debug("{} Credential '{}' {} removed", getLogPrefix(), Base64Support.encodeURLSafe(credentialId),
+                        removed > 0 ? "was" : "was not");
+            } catch (final EncodingException e) {
+                // Do nothing if the encoding fails.
+            }
+        }        
         
         // Remove the key to be deleted in-case the context state is re-used
         context.setCredentialIdToRemove(null);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java
new file mode 100644
index 0000000..7168959
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.authn.webauthn.admin.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that extracts the credential identifier for removal from the incoming HTTP request.
+ * 
+ * @event {AuthnEventIds#NO_CREDENTIALS}
+ * @event {EventIds#IO_ERROR}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
+ * @post add credential ID to remove from the the management context
+ */
+//TODO this is identical other than the context to ExtractKeyRemovalInformationFromFormRequest, we should only need one
+public class AdminExtractKeyRemovalInformationFromFormRequest extends AbstractWebAuthnManagementAction {
+
+    /** Default credential Id parameter name. */
+    @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "credentialId";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AdminExtractKeyRemovalInformationFromFormRequest.class);
+    
+    /** Name of credential Id parameter. */
+    @NonnullAfterInit @NotEmpty private String credentialIdParameterName;
+
+    /** Constructor. */
+    public AdminExtractKeyRemovalInformationFromFormRequest() {       
+        credentialIdParameterName = DEFAULT_PARAMETER_NAME;
+    }
+    
+    /**
+     * Set the name of the credential Id parameter.
+     * 
+     * @param field field name
+     */
+    public void setCedentialIdParameterName(@Nonnull @NotEmpty final String field) {
+        checkSetterPreconditions();
+        
+        credentialIdParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Credential ID "
+                + "parameter cannot be null or empty");
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+
+        final String credentialId = request.getParameter(credentialIdParameterName);         
+        if (credentialId == null) {
+            log.debug("{} CredentialID not found in HTTP request",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext,  AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }           
+        try {
+            // Should be base64 encoded credential.
+            final byte[] credentialIdAsBytes = Base64Support.decode(credentialId);
+            context.setCredentialIdToRemove(credentialIdAsBytes); 
+            log.trace("{} Credential to remove '{}'",getLogPrefix(),credentialId);
+        } catch (final DecodingException e) {
+            log.debug("{} Unable to base64 decode credentialID, can not remove credential", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+
+    }
+    
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
index b849c32..ecb346e 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
@@ -49,6 +49,8 @@ public class DeletePublicKeyCredential extends AbstractWebAuthnRegistrationActio
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final WebAuthnRegistrationContext context) {
 
+        // This should come from the context before the registration page i.e. it should not comes from a form the user
+        // can't manipulate.
         final String username = context.getUsername();
         if (username == null) {
             log.error("{} Unable to find username in registration context", getLogPrefix());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java
new file mode 100644
index 0000000..328b609
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.authn.webauthn.admin.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that extracts the username search value from the incoming HTTP request.
+ * 
+ * @event {AuthnEventIds#NO_CREDENTIALS}
+ * @event {EventIds#IO_ERROR}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post add the parameter value to the context
+ */
+//TODO just make this generic? would need a consumer to set the information?
+public class ExtractUsernameSearchFromFormRequest extends AbstractWebAuthnManagementAction {
+
+    /** Default credential Id parameter name. */
+    @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "username_search";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameSearchFromFormRequest.class);
+    
+    /** Name of the request parameter to extract. */
+    @NonnullAfterInit @NotEmpty private String requestParameterToExtract;
+
+    /** Constructor. */
+    public ExtractUsernameSearchFromFormRequest() {       
+        requestParameterToExtract = DEFAULT_PARAMETER_NAME;
+    }
+    
+    /**
+     * Set the name of the request parameter to extract the value from.
+     * 
+     * @param field field name
+     */
+    public void setRequestParameterName(@Nonnull @NotEmpty final String parameter) {
+        checkSetterPreconditions();
+        
+        requestParameterToExtract = Constraint.isNotNull(StringSupport.trimOrNull(parameter), "Request parameter "
+                + "parameter cannot be null or empty");
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
+            return;
+        }
+
+        final String parameterValue = request.getParameter(requestParameterToExtract);         
+        if (parameterValue == null) {
+            log.debug("{} {} not found in HTTP request",getLogPrefix(), requestParameterToExtract);
+            ActionSupport.buildEvent(profileRequestContext,  AuthnEventIds.UNKNOWN_USERNAME);
+            return;
+        }
+        log.trace("{} Extracting username '{}' to manage credentials for",getLogPrefix(), parameterValue);
+        context.setSearchUsername(parameterValue);
+        
+
+    }
+    
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java
new file mode 100644
index 0000000..1d8d5c6
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * An action that lookups registered credentials for a username contained in the WebAuthn management 
+ * context and sets them back onto the context. 
+ * 
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @post BaseWebAuthnContext.setExistingCredentials() is either null if no existing credentials are found, or contains
+ * the credentials from the credential repository
+ */
+public class LookupCredentialsForUser extends AbstractWebAuthnManagementAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(LookupCredentialsForUser.class);    
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {        
+        
+        final String userToSearchFor = context.getSearchUsername();
+        if (StringSupport.trimOrNull(userToSearchFor) == null) {
+            log.trace("{} No username to search for", getLogPrefix());
+            context.setFoundCredentials(CollectionSupport.emptyList());
+        } else {
+            log.trace("{} Searching registered credentials for '{}'",getLogPrefix(), userToSearchFor);
+            assert userToSearchFor != null;
+            final Collection<CredentialRegistration> credentials = 
+                    getCredentialRepository().getRegistrationsByUsername(userToSearchFor);      
+            log.debug("{} Found '{}' credentials", getLogPrefix(), credentials.size());
+            context.setFoundCredentials(credentials);
+        }
+
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java
new file mode 100644
index 0000000..8bf5e09
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action to create (or lookup) the {@link WebAuthnManagementContext} and set the admin principal. 
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @post a {@link WebAuthnManagementContext} is created.
+ */
+public class PopulateWebAuthnManagementContext extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnManagementContext.class);
+    
+    /** Strategy used to locate or create the {@link WebAuthnManagementContext} to populate. */
+    @Nonnull private 
+    Function<ProfileRequestContext, WebAuthnManagementContext> webAuthnManagementContextCreationStrategy;
+    
+    /** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
+    @Nonnull private Function<ProfileRequestContext, String> principalNameLookupStrategy;
+    
+    /** Constructor.*/
+    public PopulateWebAuthnManagementContext() {
+        // Default creates a WebAuthn management context under the profile request context.
+        webAuthnManagementContextCreationStrategy =
+                new ChildContextLookup<>(WebAuthnManagementContext.class, true);
+        
+        principalNameLookupStrategy = new CanonicalUsernameLookupStrategy();
+
+    }
+    
+    /**
+     * Set the strategy used to lookup or create the WebAuthn managment context.
+     * 
+     * @param strategy The strategy to set.
+     */
+    public void setWebAuthnManagementContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, WebAuthnManagementContext> strategy) {
+        checkSetterPreconditions();
+        webAuthnManagementContextCreationStrategy = Constraint.isNotNull(
+                strategy,"WebAuthnManagementContextCreationStrategy can not be null");
+    }
+    
+    /**
+     * Set the lookup strategy to use to determine the admin principal.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setPrincipalNameLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+
+        principalNameLookupStrategy = Constraint.isNotNull(strategy, "Principal name lookup strategy cannot be null");
+    }
+    
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        
+        final WebAuthnManagementContext context = 
+                webAuthnManagementContextCreationStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.error("{} Error creating WebAuthnManagementContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        
+        final String principalName = principalNameLookupStrategy.apply(profileRequestContext);
+        if (principalName == null) {
+            log.error("{} Error creating WebAuthnManagementContext, no principal name found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        context.setPrincipalName(principalName);
+
+        log.debug("Created WebAuthn management context for '{}'", context.getPrincipalName());
+    }
+    
+    
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
index 0255a32..c509ade 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -26,9 +26,6 @@ import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.storage.StorageCapabilities;
-import org.opensaml.storage.StorageSerializer;
-import org.opensaml.storage.StorageService;
 import org.slf4j.Logger;
 
 import com.yubico.fido.metadata.AAGUID;
@@ -42,13 +39,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventI
 import net.shibboleth.idp.plugin.authn.webauthn.authn.RegistrationResult;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistrationSerializer;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullElements;
 import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.collection.CollectionSupport;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -63,40 +56,6 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
 
-    /** Backing service. */
-    @NonnullAfterInit private StorageService storageService;    
-
-    /** Storage record serializer. */
-    @Nonnull  private final StorageSerializer<Set<CredentialRegistration>> serializer;
-
-    /** Constructor. */
-    public StorePublicKeyCredential() {
-        serializer = new CredentialRegistrationSerializer();
-    }
-
-    /**
-     * Set the {@link StorageService} back-end to use.
-     * 
-     * @param storage
-     *            the back-end to use
-     */
-    public void setStorageService(@Nonnull final StorageService storage) {
-        checkSetterPreconditions();
-
-        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
-        final StorageCapabilities caps = storageService.getCapabilities();
-        Constraint.isTrue(caps.isServerSide(), "StorageService cannot be client-side");
-    }
-
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-
-        if (storageService == null) {
-            throw new ComponentInitializationException("StorageService cannot be null");
-        }
-    }
-
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final WebAuthnRegistrationContext context) {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
index 2736b5c..6ea7976 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
@@ -53,6 +53,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
  * @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
  */
+//TODO somehow this should be a child of the base webauthn action (BaseWebAuthnAction)?
 public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
     
     /** Class logger. */
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
index 482c410..726840f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
@@ -28,14 +28,10 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
 import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
@@ -53,7 +49,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @post <pre>AuthenticationContext.getSubcontext(BaseWebAuthnContext.class) != null</pre>
  */
 //TODO a few abstract action types, remove overlap
-public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
+public abstract class AbstractWebAuthnBaseAction extends BaseWebAuthnAction {
     
     /** Class logger. */
     @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnBaseAction.class);
@@ -64,34 +60,7 @@ public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
     
     /** The WebAuthn base Context.*/
     @NonnullBeforeExec private BaseWebAuthnContext webAuthnBaseContext;
-    
-    /** The WebAuthn client to use.*/
-    @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
-    
-    /** The credential repository to store valid credentials in.*/
-    @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
-    
-    
-    /**
-     * Set the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @param client The webauthnClient to set.
-     */
-    public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
-        checkSetterPreconditions();
-        webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
-    }
-    
-    /**
-     * Get the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @return the webAuthnClient.
-     */
-    @NonnullBeforeExec public WebAuthnAuthenticationClient getWebAuthnClient() {
-        checkComponentActive();
-        return webAuthnClient;
-    }
-        
+            
     
     /** Constructor.*/
     protected AbstractWebAuthnBaseAction() {
@@ -113,38 +82,7 @@ public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
         webAuthnBaseContextLookupStrategy = 
                 Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
     }    
-    
-    /**
-     * Set the credential repository used to store WebAuthn credentials.
-     *  
-     * @param repository The respository to set.
-     */
-    public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
-        checkSetterPreconditions();
-        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
-    }
-    
-    /**
-     * Get the credential repository used to store WebAuthn credentials.
-     * 
-     * @return the credential repository.
-     */
-    public StorageServiceCredentialRepository getCredentialRepository() {
-        return credentialRepository;
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (webAuthnClient == null) {
-            throw new ComponentInitializationException("WebAuthn Client can not be null");
-        }
-        if (credentialRepository == null) {
-            throw new ComponentInitializationException("CredentialRepository can not be null");
-        }
-    }
+
     
     /** {@inheritDoc} */
     @Override
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
new file mode 100644
index 0000000..c4be7c8
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.yubico.fido.metadata.FidoMetadataService;
+
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A base action for WebAuthn actions that require access to the client, credential repository, and metadata service.
+ */
+public abstract class BaseWebAuthnAction extends AbstractProfileAction {
+    
+    /** The WebAuthn client to use.*/
+    @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+    
+    /** The credential respository to store valid credentials in.*/
+    @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
+    
+    /** Optional FIDO metadata service resolver.*/ 
+    @Nullable private FidoMetadataService fidoMetadataService;  
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (webAuthnClient == null) {
+            throw new ComponentInitializationException("WebAuthn Client can not be null");
+        }
+        if (credentialRepository == null) {
+            throw new ComponentInitializationException("CredentialRepository can not be null");
+        }
+    }    
+    
+    /**
+     * Set the WebAuthn client used to handle registration and authentication ceremonies.
+     * 
+     * @param client The webauthnClient to set.
+     */
+    public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
+        checkSetterPreconditions();
+        webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
+    }
+    
+    /**
+     * Get the WebAuthn client used to handle registration and authentication ceremonies.
+     * 
+     * @return the webAuthnClient.
+     */
+    @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
+        checkComponentActive();
+        return webAuthnClient;
+    }
+    
+    /**
+     * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
+     * 
+     * @param service The FIDO metadata service to set.
+     */
+    public void setFidoMetadataService(@Nullable final FidoMetadataService service) {
+        checkSetterPreconditions();
+        fidoMetadataService = service;
+    }
+    
+    /**
+     * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
+     * 
+     * @return the fido metadata service.
+     */
+    @Nullable protected FidoMetadataService getFidoMetadataService() {
+        checkComponentActive();
+        return fidoMetadataService;
+    }
+    
+    /**
+     * Set the credential repository used to store WebAuthn credentials.
+     *  
+     * @param repository The respository to set.
+     */
+    public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
+        checkSetterPreconditions();
+        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+    }
+    
+    /**
+     * Get the credential repository used to store WebAuthn credentials.
+     * 
+     * @return the credential repository.
+     */
+    @NonnullAfterInit protected StorageServiceCredentialRepository getCredentialRepository() {
+        return credentialRepository;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
index c55a821..9dbcb68 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
@@ -19,6 +19,8 @@ import java.util.Collection;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.locks.Lock;
@@ -45,7 +47,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.exception.CredentialRepositoryEx
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -55,8 +59,8 @@ import net.shibboleth.shared.logic.ConstraintViolationException;
 /**
  * A {@link CredentialRepository} adaptor for the Shibboleth {@link StorageService}.
  * 
- * <p>Username is used as the key to the storage context. The object to store (value of the storage record) is a set of 
- * {@link CredentialRegistration registered credentials}. Storage records do not expire.</p>
+ * <p>Username (user.name) is used as the key to the storage context. The object to store (value of the storage record) 
+ * is a set of {@link CredentialRegistration registered credentials}. Storage records do not expire.</p>
  * 
  * <p>Note, any exception is wrapped in an unchecked {@link CredentialRepositoryException}. If the caller does not deem 
  * this terminal, they should catch and handle that error appropriately.</p>
@@ -358,6 +362,63 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
             writeLock.unlock();
         }         
     }
+    
+    /** {@inheritDoc} */
+    @Override
+    public int removeRegistrationByCredentialId(final ByteArray credentialId) {
+        checkComponentActive();
+        final Lock writeLock = lock.writeLock();
+        try {
+            writeLock.lock();
+            int removalCount = 0;
+            for (final Iterator<String> i = storageService.getContextKeys(STORAGE_CONTEXT, null).iterator(); 
+                    i.hasNext();) {
+                final String usernameKey = i.next();
+                assert usernameKey != null;
+                
+                final Set<CredentialRegistration> existingRegistrations = getRegistrationsByUsername(usernameKey);
+                
+                if (existingRegistrations.isEmpty()) {
+                    log.trace("No existing registrations, nothing to remove");
+                    // Nothing to do
+                    continue;
+                }
+                
+                // Find an matching credential from the existing registration
+                final List<CredentialRegistration> matchingRegistrations = existingRegistrations.stream()
+                    .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toList())).get();
+                
+                if (matchingRegistrations.size() != 1) {
+                    // If no match, there is nothing to remove
+                    continue;
+                }
+                final CredentialRegistration registrationToRemove = matchingRegistrations.get(0);
+                
+                final Set<CredentialRegistration> updateSet = new LinkedHashSet<>(existingRegistrations);
+                updateSet.remove(registrationToRemove);
+                if (updateSet.isEmpty()) {
+                    //remove the entire storage record
+                    if (storageService.delete(STORAGE_CONTEXT, usernameKey)) {
+                        removalCount++;
+                    }
+                } else {
+                    //else, add back what remains
+                    assert serializer != null;
+                    if (storageService.update(STORAGE_CONTEXT, usernameKey, updateSet, serializer, null)) {
+                        removalCount ++;
+                    }
+                }                            
+            }           
+            // Nothing to do if the registration does not exist
+            return removalCount;
+            
+        } catch (final IOException e) {
+            throw new CredentialRepositoryException(e);            
+        } finally {
+            writeLock.unlock();
+        }         
+    }
 
     /** {@inheritDoc} */
     @Override
@@ -403,5 +464,30 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
         } 
     }
 
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NotLive @Unmodifiable public Set<CredentialRegistration> getAllRegistrations() {
+        checkComponentActive();
+        final Lock readLock = lock.readLock();
+        try {
+            readLock.lock();
+            final Set<CredentialRegistration> allCredentials = new HashSet<>();
+            for (final Iterator<String> i = storageService.getContextKeys(STORAGE_CONTEXT, null).iterator(); 
+                    i.hasNext();) {
+                final String usernameKey = i.next();
+                assert usernameKey != null;
+                allCredentials.addAll(getRegistrationsByUsername(usernameKey).stream()
+                        .filter(Objects::nonNull)
+                        .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get());
+
+            }
+            return CollectionSupport.copyToSet(allCredentials);
+        } catch (final IOException e) {
+            throw new CredentialRepositoryException(e);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
 
 }
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 8e42bb9..307a389 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -45,7 +45,7 @@
     
     <!-- Admim flow for WebAuthn credential registration -->
     <bean parent="shibboleth.AdminFlow"
-            c:id="http://shibboleth.net/ns/profiles/webauthn/register-credential"
+            c:id="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential"
             p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
             p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByIPAddress}"
             p:nonBrowserSupported="false"
@@ -65,6 +65,28 @@
          </property>
     </bean>
     
+    <!-- Admim flow for admins to manage user credential registrations -->
+    <bean parent="shibboleth.AdminFlow"
+            c:id="http://shibboleth.net/ns/profiles/webauthn/manage-credentials"
+            p:loggingId="%{idp.authn.webauthn.admin.management.logging:WebAuthnCredentialManagement}"
+            p:policyName="%{idp.authn.webauthn.admin.management.accessPolicy:AccessByAdminUser}"
+            p:nonBrowserSupported="false"
+            p:authenticated="true"
+            p:resolveAttributes="%{idp.authn.webauthn.admin.management.resolveIdentityAttributes:true}">
+        <property name="authenticationFlows">
+           <bean parent="shibboleth.CommaDelimStringArray"
+             c:_0="#{'%{idp.authn.webauthn.admin.management.authenticationFlows:}'.trim()}" />
+        </property>
+        <property name="postAuthenticationFlows">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.authn.webauthn.admin.management.postAuthenticationFlows:}'.trim()}" />
+        </property>
+        <property name="defaultAuthenticationMethodsByString">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.authn.webauthn.admin.management.defaultAuthenticationMethods:}'.trim()}" />
+         </property>
+    </bean>
+    
      <!-- Singleton clients and repositories -->
      <!--  TODO configure these with getbeans and properties -->
     <bean id="shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory" scope="singleton"
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml
new file mode 100644
index 0000000..84c3fd5
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.AdminProfileId" class="java.lang.String"
+        c:_0="http://shibboleth.net/ns/profiles/webauthn/manage-credentials" />
+
+    <!-- Flow Functions -->
+    
+    <bean id="shibboleth.ChildLookup.WebAuthnManagementContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext) }" />
+
+
+
+    <!-- Abstract parent beans -->
+
+    <bean id="AbstractWebAuthnManagementAction" scope="prototype" abstract="true"
+        p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"
+        p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"
+        p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
+  
+
+    <!-- Flow beans -->
+
+    <bean id="PopulateWebAuthnManagementContext" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnManagementContext">
+        <property name="principalNameLookupStrategy">
+            <bean id="principalNameFromAuthnResult" scope="prototype"
+                class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromSubjectContext" />
+        </property>
+    </bean>
+    
+    <bean id="ExtractUsernameSearchFromFormRequest" parent="AbstractWebAuthnManagementAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractUsernameSearchFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+    
+    <bean id="LookupCredentialsForUser" parent="AbstractWebAuthnBaseAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LookupCredentialsForUser"
+        p:webauthnManagementContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext" />
+        
+    <bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnManagementAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AdminExtractKeyRemovalInformationFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+        
+    <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnManagementAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AdminDeletePublicKeyCredential" />
+
+
+
+</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
similarity index 63%
copy from webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
copy to webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
index fe13bc0..33af679 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
@@ -10,64 +10,62 @@
         <evaluate expression="FlowStartPopulateAuditContext" />
         <evaluate expression="'proceed'" />        
 
-        <transition on="proceed" to="CollectUsernameView" />
+        <!-- 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="'proceed'" />
+        
+        <transition on="proceed" to="PopulateManagementContext" />
+    </action-state>   
+    
+     <action-state id="PopulateManagementContext">
+       <evaluate expression="PopulateWebAuthnManagementContext"/> 
+      
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="UsernameSearchView" />    
+    </action-state>
     
-    <view-state id="CollectUsernameView" view="webauthn/webauthn-register-username">
+     <view-state id="UsernameSearchView" view="webauthn/webauthn-management-search">
         <on-render>
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext))" result="viewScope.webAuthnManContext" />
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPDigester')" result="requestScope.cspDigester" />
-            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />            
-            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />   
             <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
+            <evaluate expression="T(net.shibboleth.idp.plugin.authn.webauthn.impl.WebAuthnEncoder)" result="viewScope.webAuthnEncoder"/>
             <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
             <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
         </on-render>
-       <transition on="proceed" to="ExtractUsernameAndPopulateContext" />
-       
+        
+       <transition on="proceed" to="ExtractSearchFields" />
+       <transition on="finish" to="ManagementComplete" />       
     </view-state>
     
-    <!-- TODO if we want to do this, we would want this to populate a different context -->
-    <action-state id="ExtractUsernameAndPopulateContext">
-        <evaluate expression="PopulateInitialWebAuthnRegistrationContext"/>
-        <evaluate expression="LookupRegisteredCredentials"/>
+    <action-state id="ExtractSearchFields">       
+        <evaluate expression="ExtractUsernameSearchFromFormRequest"/>
+      
         <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="'proceed'" />
-        
-        <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions" />
-    </action-state>   
+        <transition on="proceed" to="LookupCredentials" />    
+    </action-state> 
     
-     <action-state id="GeneratePublicKeyCredentialCreationOptions">
-        <evaluate expression="PopulateWebAuthnRegistrationContext"/>
-        <evaluate expression="LookupRegisteredCredentials"/>      
-        <evaluate expression="GenerateServerChallenge"/>
-        <evaluate expression="AddUserId"/>
-        <evaluate expression="AddDisplayName"/>
-        <evaluate expression="AddResidentKeyRequirement"/>
-        <evaluate expression="AddAuthenticatorAttachmentRequirement"/>
-         <evaluate expression="AddAttestationConveyancePreference"/>
-        <evaluate expression="AddUserVerificationRequired"/>
-        <evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
+    <action-state id="LookupCredentials">
+       <evaluate expression="LookupCredentialsForUser"/>   
+      
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="DisplayWebAuthnView" />    
-    </action-state>     
+        <transition on="proceed" to="ManagementView" />    
+    </action-state>   
     
-    <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-register">
+    <view-state id="ManagementView" view="webauthn/webauthn-management">
         <on-render>
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
-            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext))" result="viewScope.webAuthnManContext" />
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPDigester')" result="requestScope.cspDigester" />
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />   
             <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
@@ -76,41 +74,25 @@
             <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
         </on-render>
         
-       <transition on="finish" to="RegistrationComplete" />
-       <transition on="addKey" to="AddKey" />
+       <transition on="again" to="UsernameSearchView" />
+       <transition on="finish" to="ManagementComplete" />
        <transition on="deleteKey" to="DeleteKey" />
        
-    </view-state>
-    
-   <action-state id="AddKey">
-        <evaluate expression="ExtractAuthenticatorAttestationFromFormRequest"/>
-        <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
-        <evaluate expression="StorePublicKeyCredential"/> 
-        <evaluate expression="'proceed'" />
-        
-        <transition on="InvalidRegistration" to="GeneratePublicKeyCredentialCreationOptions">
-            <!-- TODO externalise message bundle-->
-            <set name="flashScope.registrationErrorOutcomes" value="'Key registration unsuccessful'"/>
-        </transition>      
-        <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions">
-            <!-- TODO externalise message bundle-->
-            <set name="flashScope.registrationOutcomes" value="'Key was registered successfully'"/>
-        </transition>
-    </action-state>
+    </view-state>   
     
     <action-state id="DeleteKey">
         <evaluate expression="ExtractKeyRemovalInformationFromFormRequest"/>
         <evaluate expression="DeletePublicKeyCredential"/>
         
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions">
+        <transition on="proceed" to="LookupCredentials">
             <!-- TODO externalise message bundle-->
-            <set name="flashScope.registrationOutcomes" value="'Key was removed successfully'"/>
+            <set name="flashScope.managementOutcomes" value="'Key was removed successfully'"/>
         </transition>
-    </action-state> 
+    </action-state>  
 
 
-    <end-state id="RegistrationComplete" view="webauthn/webauthn-register-end">
+    <end-state id="ManagementComplete" view="webauthn/webauthn-end">
          <on-entry>
             <evaluate expression="environment" result="requestScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
@@ -124,7 +106,7 @@
         </on-entry>
     </end-state>
     
-    <bean-import resource="webauthn-registration-beans.xml" />
+    <bean-import resource="webauthn-management-beans.xml" />
     <bean-import resource="../../authn/WebAuthn/webauthn-abstract-beans.xml" />
 	
 </flow>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
index aa5f3d4..521e29f 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -10,7 +10,7 @@
     default-init-method="initialize" default-destroy-method="destroy">
 
     <bean id="shibboleth.AdminProfileId" class="java.lang.String"
-        c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
+        c:_0="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential" />
 
     <!-- Flow Functions -->
 
@@ -110,14 +110,9 @@
     <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
 
-
     <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.StorePublicKeyCredential"
-        p:storageService-ref="shibboleth.authn.webauthn.StorageService"
         p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository" />
 
-    <!-- postconfig.xml? -->
-    <bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
-        class="org.opensaml.storage.impl.MemoryStorageService" p:cleanupInterval="PT10M" />
 
 </beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
index fe13bc0..d193b7f 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -110,7 +110,7 @@
     </action-state> 
 
 
-    <end-state id="RegistrationComplete" view="webauthn/webauthn-register-end">
+    <end-state id="RegistrationComplete" view="webauthn/webauthn-end">
          <on-entry>
             <evaluate expression="environment" result="requestScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
index 13ba04e..74e05f9 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
@@ -87,6 +87,16 @@ idp.authn.webauthn.supportedPrincipals = \
 # State the preference of the IdP during registration to receive an authenticator attestation. One-of 'none', 'indirect', 'direct', or 'enterprise'.
 #idp.authn.webauthn.registration.attestationConveyancePreference = none
 
+
+#### Administrator properties
+
+#idp.authn.webauthn.admin.management.logging = WebAuthnCredentialManagement
+#idp.authn.webauthn.admin.management.accessPolicy = AccessByAdmin
+#idp.authn.webauthn.admin.management.resolveIdentityAttributes = true
+#idp.authn.webauthn.admin.management.authenticationFlows
+#idp.authn.webauthn.admin.management.postAuthenticationFlows
+#idp.authn.webauthn.admin.management.defaultAuthenticationMethod
+
 #### Authentication properties
 
 # Which type of flow is supported? Usernameless or passwordless
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
index 6e944e2..431e2c1 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
@@ -40,7 +40,13 @@ idp.authn.WebAuthn.8.dest = edit-webapp/js/webauthn/webauthn-json.browser-ponyfi
 idp.authn.WebAuthn.9.src = /net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-support.js
 idp.authn.WebAuthn.9.dest = edit-webapp/js/webauthn/webauthn-support.js
 
-idp.authn.WebAuthn.10.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
-idp.authn.WebAuthn.10.dest = views/webauthn/webauthn-register-end.vm
+idp.authn.WebAuthn.10.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
+idp.authn.WebAuthn.10.dest = views/webauthn/webauthn-end.vm
+
+idp.authn.WebAuthn.11.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
+idp.authn.WebAuthn.11.dest = views/webauthn/webauthn-management.vm
+
+idp.authn.WebAuthn.12.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
+idp.authn.WebAuthn.12.dest = views/webauthn/webauthn-management-search.vm
 
 
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
index f898d35..429b5dc 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
@@ -1,5 +1,5 @@
 ##
-## Velocity Template for collection of username for Duo Passwordless use
+## Velocity Template for collection of username for WebAuthn Passwordless use
 ##
 ## Velocity context will contain the following properties
 ## flowExecutionUrl - the form action location
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
index 2ea24b2..efa7284 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -1,5 +1,5 @@
 ##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn authentication view
 ##
 ## Velocity context will contain the following properties
 ## flowExecutionUrl - the form action location
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
similarity index 58%
rename from webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
rename to webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
index 937d077..c66ba71 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
@@ -1,5 +1,5 @@
 ##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn end state view
 ##
 ## Velocity context will contain the following properties
 ## flowExecutionUrl - the form action location
@@ -34,9 +34,35 @@
         <main class="main">
         <header>
             <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+            #set ($serviceName = $rpUIContext.serviceName)
+            #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+                <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+            #end
         </header>
-        <section>           
-             #springMessageText("idp.webauthn.authn.register.ended", "Your registration session has ended")                    
+        <section>
+            #*
+            //
+            // SP Description & Logo (optional)
+            // These idpui lines will display added information (if available
+            // in the metadata) about the Service Provider (SP) that requested
+            // authentication. These idpui lines are "active" in this example
+            // (not commented out) - this extra SP info will be displayed.
+            // Remove or comment out these lines to stop the display of the
+            // added SP information.
+            //
+            *#
+            #set ($logo = $rpUIContext.getLogo())
+            #if ($logo)
+                <img class="service-logo" src="$encoder.encodeForHTMLAttribute($logo)"
+                    alt="$encoder.encodeForHTMLAttribute($serviceName)">
+            #end
+            #set ($desc = $rpUIContext.getServiceDescription())
+            #if ($desc)
+                <p>$encoder.encodeForHTML($desc)</p>
+            #end
+            <div class="content">
+                    #springMessageText("idp.webauthn.authn.register.ended", "Your registration session has ended")                    
+            </div>
         </section>
         <footer>
             <div class="container container-footer">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
new file mode 100644
index 0000000..900bcfd
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
@@ -0,0 +1,71 @@
+##
+## Velocity Template for displaying the WebAuthn administrator view
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## webAuthnManContext = WebAuthn management context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## webAuthnEncoder - WebAuthnEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+##
+<!DOCTYPE html>
+<html>
+   <head>
+      <title>#springMessageText("idp.title", "Web Login Service")</title>
+      <meta charset="UTF-8" />
+      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+      <script type="text/javascript" src="$request.getContextPath()/js/webauthn/webauthn-support.js"></script>
+      <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+      <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/webauthn.css">
+         
+   </head>
+   <body>
+      <main class="main">
+         <header>
+            <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+         </header>
+         <section>
+           
+               <div class="centre">                
+                  <div class="hidden output-message output--error" id="error_div">
+                        <p id="error_message"></p>
+                  </div>                
+                  <div>
+                     <h1>#springMessageText("idp.webauthn.admin.search.title", "Search For Users")</h1>
+                     <form id="username_search_form" action="$flowExecutionUrl" method="post">
+                         #parse("csrf/csrf.vm")
+                         <label for="username_search">#springMessageText("idp.webauthn.admin.search.label", "Username")</label>
+                         <input id="username_search" name="username_search" type="text"/>                         
+                         
+                          <div class="grid">
+                            <div class="grid-item">
+                                <button id="search_button" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.admin.button.search", "Lookup")</button>
+                                <button id="finish_button" type="submit" name="_eventId_finish">#springMessageText("idp.webauthn.admin.button.finish", "Finish")</button>
+                            </div> 
+                          </div>
+                    
+                      </form>
+                    
+                  </div>
+            </div>
+         </section>
+      </main>
+      <footer>
+         <div class="cc">
+            <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+         </div>
+      </footer>
+      </div>    
+   </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
new file mode 100644
index 0000000..c4ed5d2
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
@@ -0,0 +1,125 @@
+##
+## Velocity Template for displaying the WebAuthn administrator view
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## webAuthnManContext = WebAuthn management context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## webAuthnEncoder - WebAuthnEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+##
+<!DOCTYPE html>
+<html>
+   <head>
+      <title>#springMessageText("idp.title", "Web Login Service")</title>
+      <meta charset="UTF-8" />
+      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+      <script type="text/javascript" src="$request.getContextPath()/js/webauthn/webauthn-support.js"></script>
+      <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+      <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/webauthn.css">
+         
+   </head>
+   <body>
+      <main class="main">
+         <header>
+            <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+         </header>
+         <section>
+            <div id="supportedDiv">
+               <div class="centre">
+                  #if ($managementOutcomes)
+                    <p id="reg-success-outcome" class="output-message output--success">$registrationOutcomes</p>
+                  #end
+                   #if ($managementErrorOutcomes)
+                    <p id="reg-error-outcome" class="output-message output--error">$registrationErrorOutcomes</p>
+                  #end 
+                  <div class="hidden output-message output--error" id="error_div">
+                        <p id="error_message"></p>
+                  </div>                
+                  <div>
+                     <h1>Registered keys for '$encoder.encodeForHTML($webAuthnManContext.searchUsername)'</h1>
+                     #if ($webAuthnManContext.foundCredentials)
+                         <table>
+                            <tr>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.userName", "User")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.keyName", "Key Name")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.authenticatorDescription", "Authenticator")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.transports", "Transports")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.passkey", "Passkey?")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
+                            </tr>
+                            #foreach($cred in $webAuthnManContext.foundCredentials)
+                                <tr>
+                                   <td>$encoder.encodeForHTML($cred.username)</td>
+                                   <td>$encoder.encodeForHTML($cred.nickname)</td>
+                                   #if ($cred.authenticatorDescription)
+                                       <td>
+                                       #if ($cred.icon)
+                                            <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon) alt="authenticator-icon"/> 
+                                       #end 
+                                       $encoder.encodeForHTML($cred.authenticatorDescription)</td>
+                                   #else
+                                        <td>#springMessageText("idp.webauthn.admin.table.unknownCredential", "unknown")</td>
+                                   #end
+                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatTransports($cred.transports))</td>
+                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatDiscoverable($cred.isDiscoverable()))</td>
+                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.registrationTime))</td>
+                                   <td>
+                                      <form id="delete_key_form" action="$flowExecutionUrl" method="post">
+                                         #parse("csrf/csrf.vm")
+                                         <input type="hidden" name="credentialId" value="$cred.credentialIdBase64Url"/>
+                                         <button class="webauthn-table-button" onclick="return confirm('#springMessageText("idp.webauthn.admin.credential.remove.confirm", "Are you sure")');" id="removeButton" type="submit" name="_eventId_deleteKey">
+                                        #springMessageText("idp.webauthn.admin.credential.remove", "Remove")</button>
+                                      </form>
+                                   </td>
+                                </tr>
+                             #end 
+                         </table>                                        
+                     #else
+                        <div><span>#springMessageText("idp.webauthn.admin.noKeys", "There are no registered keys")</span></div>
+                     #end
+                     
+                     <br/>
+                        
+                    <form id="finish_button_form" action="$flowExecutionUrl" method="post" class="inline">
+                       #parse("csrf/csrf.vm")   
+                       <div class="grid">
+                        <div class="grid-item">
+                            <button id="searchButton" type="submit" name="_eventId_again">#springMessageText("idp.webauthn.admin.search.again", "Search Again")</button>
+                             <button id="finish_button" type="submit" name="_eventId_finish">#springMessageText("idp.webauthn.admin.finish", "Finish")</button>
+                        </div> 
+                      </div>                        
+                      
+                    </form>
+
+
+                     
+                  </div>
+         
+               </div>
+               <div id="unsupportedDiv" class="hidden">
+                  #springMessageText("idp.webauthn.admin.unsupported", "Your browser is not WebAuthn compatible")
+               </div>
+            </div>
+         </section>
+      </main>
+      <footer>
+         <div class="cc">
+            <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+         </div>
+      </footer>
+      </div>    
+   </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index 6f93cde..c2981d4 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -1,5 +1,5 @@
 ##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn user crential registration view
 ##
 ## Velocity context will contain the following properties
 ## flowExecutionUrl - the form action location
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
index 87cb832..50624af 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
@@ -153,6 +153,165 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
 
     }
     
+    @Test
+    public void testRemoveRegistrationByCredentialId() throws Exception {     
+        
+        final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+        repo.addRegistrationByUsername("jdoe", registration);
+        
+        var registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        final var iterator = registrations.iterator();
+        final var credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());
+        
+        repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+        
+        registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 0);
+    }
+    
+    @Test
+    public void testRemoveRegistrationByCredentialId_TwoRegistrations() throws Exception {     
+        
+        final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+        repo.addRegistrationByUsername("jdoe", registration);
+        
+        final CredentialRegistration registrationTwo = createRegistration("pdoe", "Poe Doe", "user-handle-2".getBytes());
+        repo.addRegistrationByUsername("pdoe", registrationTwo);
+        
+        var registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        var iterator = registrations.iterator();
+        var credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());
+        
+        registrations = repo.getRegistrationsByUsername("pdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        iterator = registrations.iterator();
+        credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"pdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registrationTwo.getCredential().getCredentialId());
+        
+        final int removalCount = repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+        
+        assertEquals(removalCount, 1);
+        
+        registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 0);
+        
+        registrations = repo.getRegistrationsByUsername("pdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testRemoveRegistrationByCredentialId_TwoRegistrationsSameAccount() throws Exception {     
+        
+        final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+        final CredentialRegistration registrationTwo = createRegistration("jdoe", "John Doe", "user-handle-2".getBytes());
+        repo.addRegistrationByUsername("jdoe", registration);
+        repo.addRegistrationByUsername("jdoe", registrationTwo);
+        
+        var registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 2);
+        final var iterator = registrations.iterator();
+        var credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());
+        credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registrationTwo.getCredential().getCredentialId());
+
+        
+        final int removalCount = 
+                repo.removeRegistrationByCredentialId(registrationTwo.getCredential().getCredentialId());
+        
+        assertEquals(removalCount, 1);
+        
+        registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+    }
+    
+    @Test
+    public void testRemoveRegistrationByCredentialId_TwoRegistrationsSameCredential() throws Exception {     
+        
+        final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+        repo.addRegistrationByUsername("jdoe", registration);
+        // Both share the same registration, should not happen in practice
+        repo.addRegistrationByUsername("pdoe", registration);
+        
+        var registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        var iterator = registrations.iterator();
+        var credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());
+        
+        registrations = repo.getRegistrationsByUsername("pdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        iterator = registrations.iterator();
+        credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());
+        
+        final int removalCount = repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+        
+        assertEquals(removalCount, 2);
+        
+        registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 0);
+        
+        registrations = repo.getRegistrationsByUsername("pdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 0);
+    }
+    
+    @Test
+    public void testRemoveRegistrationByCredentialId_NothingToRemove() throws Exception {     
+        
+        final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+        repo.addRegistrationByUsername("jdoe", registration);
+        
+        var registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+        final var iterator = registrations.iterator();
+        final var credReg = iterator.next();
+        assertEquals(credReg.getUsername(),"jdoe");
+        assertEquals(credReg.getCredential().getCredentialId(),
+                registration.getCredential().getCredentialId());        
+        
+        final int removalCount = repo.removeRegistrationByCredentialId(new ByteArray("new-cred".getBytes()));
+        
+        assertEquals(removalCount, 0);
+        
+        registrations = repo.getRegistrationsByUsername("jdoe");
+        assertNotNull(registrations);
+        assertEquals(registrations.size(), 1);
+
+    }
+    
     @Test
     public void testRemoveRegistrationByUsername() throws Exception {     
         
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
index 775a301..15a208b 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -46,6 +46,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
 
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.shared.collection.CollectionSupport;
 
 /**
  * In memory credential repository to use for testing.
@@ -176,4 +177,18 @@ public class InMemoryRegistrationStorage implements StorageServiceCredentialRepo
         // TODO Fill this in if needed.
         return true;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public int removeRegistrationByCredentialId(final ByteArray credentialId) {
+     // TODO Fill this in if needed.
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Set<CredentialRegistration> getAllRegistrations() {
+     // TODO Fill this in if needed.
+        return CollectionSupport.emptySet();
+    }
 }

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


More information about the commits mailing list