[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-51 - Allowing editing of nicknames

Phil Smart philip.smart at jisc.ac.uk
Wed Apr 23 09:33:44 UTC 2025


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=d458f2b168ea363a90b2bb7e730cbf9df1d03fd0

The following commit(s) were added to refs/heads/main by this push:
     new d458f2b  JWEBAUTHN-51 - Allowing editing of nicknames
d458f2b is described below

commit d458f2b168ea363a90b2bb7e730cbf9df1d03fd0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Apr 23 10:29:34 2025 +0100

    JWEBAUTHN-51 - Allowing editing of nicknames
    
     - Allow nickname updates
     - Separate nickname extraction from public key attestation extraction
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-51
---
 .../context/WebAuthnRegistrationContext.java       |  31 ++++++
 .../storage/WebAuthnCredentialRepository.java      |  19 +++-
 .../impl/ExtractKeyInformationFromFormRequest.java |   2 +-
 ...st.java => ExtractNicknameFromFormRequest.java} |  83 +++++---------
 ...licKeyCredentialAttestationFromFormRequest.java |  33 +-----
 ...istrationContextCredentialToModifyConsumer.java |  77 +++++++++++++
 .../admin/impl/UpdateCredentialNickname.java       | 109 +++++++++++++++++++
 .../IdPStorageServiceCredentialRespository.java    |  54 +++++++++-
 .../webauthn-registration-beans.xml                |  23 ++++
 .../webauthn-registration-flow.xml                 |  17 +++
 .../idp/plugin/authn/webauthn/css/webauthn.css     |  31 ++++++
 .../idp/plugin/authn/webauthn/messages.properties  |   3 +
 .../authn/webauthn/views/webauthn-management.vm    |   4 +-
 .../authn/webauthn/views/webauthn-register.vm      |  65 ++++++-----
 .../ExtractKeyInformationFromFormRequestTest.java  | 120 +++++++++++++++++++++
 .../impl/ExtractNicknameFromFormRequestTest.java   |  94 ++++++++++++++++
 ...eyCredentialAttestationFromFormRequestTest.java |  56 +---------
 .../authn/webauthn/flow/TestRegistrationFlow.java  |   5 +-
 18 files changed, 651 insertions(+), 175 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
index 0781695..328cb81 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
@@ -82,6 +82,9 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
     /** Is nickname collection enabled or disabled?.*/
     private boolean nicknameRequired;
     
+    /** The ID of a credential that is going to be modified in some way.*/
+    @Nullable private byte[] credentialIdToModify;    
+    
     
     /**
      * Is nickname collection required or not. The nickname is used for display purposes only.
@@ -251,7 +254,10 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * @param id the credential identifier
      * 
      * @return this context
+     * 
+     * @deprecated Use {@link #setCredentialIdToModify(byte[])} instead
      */
+    @Deprecated(since= "1.2.0", forRemoval = true)
     @Nonnull public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
         credentialIdToRemove = id;
         return this;
@@ -332,6 +338,31 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
     @Nullable public String getDisplayName() {
         return displayName;
     }
+    
+    /**
+     * Set the ID of the credential that is going to be modified.
+     * 
+     * @param id the credential identifier
+     * 
+     * @return this context
+     * 
+     * @since 1.2.0
+     */
+    @Nonnull public WebAuthnRegistrationContext setCredentialIdToModify(@Nullable final byte[] id) {
+        credentialIdToModify = id;
+        return this;
+    }
+    
+    /**
+     * Get the ID of the credential that is going to be modified.
+     * 
+     * @return the identifier of the credential to be modified.
+     * 
+     * @since 1.2.0
+     */
+    @Nullable public byte[] getCredentialIdToModify() {
+        return credentialIdToModify;
+    }
 
     
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
index 9ca5141..db55c70 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
@@ -151,6 +151,23 @@ public interface WebAuthnCredentialRepository extends CredentialRepository {
     default boolean updateLastUsedTime(final @Nonnull String username, final @Nonnull ByteArray credentialId, 
            @Nonnull final Instant lastUsedTime) {
         return false;
-    }    
+    }
+    
+    /**
+     * Update the nickname for the credential that belongs to the given user.
+     * 
+     * @param username the username of the user to update the nickname for
+     * @param credentialId the identifier of the credential to update the nickname for
+     * @param nickname the new nickname of the credential
+     * 
+     * @return true iff the registration was updated, false otherwise.
+     * 
+     * @since 1.2.0
+     */
+    //TODO remove default in v2.
+    default boolean updateNickname(final @Nonnull String username, final @Nonnull ByteArray credentialId, 
+           @Nonnull final String nickname) {
+        return false;
+    }   
     
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
index 3eb6fa6..27445b6 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
@@ -115,7 +115,7 @@ public class ExtractKeyInformationFromFormRequest extends AbstractProfileAction
             // Should be base64 encoded credential.
             final byte[] credentialIdAsBytes = Base64Support.decode(credentialId);
             contextSettingConsumer.accept(profileRequestContext, credentialIdAsBytes);
-            log.trace("{} Credential to remove '{}'",getLogPrefix(),credentialId);
+            log.trace("{} Credential from request '{}'",getLogPrefix(),credentialId);
         } catch (final DecodingException e) {
             log.debug("{} Unable to base64 decode credentialID, can not set credential", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
similarity index 52%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
index 0903471..7e5af84 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
@@ -13,8 +13,6 @@
  */
 package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
-import java.io.IOException;
-
 import javax.annotation.Nonnull;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
@@ -22,12 +20,7 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
 import jakarta.servlet.http.HttpServletRequest;
-import net.shibboleth.idp.authn.AuthnEventIds;
 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.impl.AbstractWebAuthnAction;
@@ -38,63 +31,61 @@ import net.shibboleth.shared.primitive.StringSupport;
 
 
 /**
- * An action that extracts the PublicKeyCredential containing the authenticator attestation response from the incoming
- * HTTP request. Also extracts the user entered credential nickname. Failure to find the attestation results in an
- * a non-proceed event. Similarly, if a nickname is required and not provided a non-proceed event is signalled.
+ * An action that extracts the key nickname from the incoming HTTP request. If a nickname is required and not provided
+ * a non-proceed event is signalled.
  * 
  * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
  * @event {AuthnEventIds#NO_CREDENTIALS}
  * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post add an AuthenticatorAttestationResponse and credential nickname (if present) to the registration context
+ * @post add a credential nickname (if present) to the registration context
  */
-public class ExtractPublicKeyCredentialAttestationFromFormRequest 
+public class ExtractNicknameFromFormRequest 
                     extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
-
-    /** Default public key credential attestation parameter name. */
-    @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "publicKeyCredential";
     
     /** Default nickname parameter name. */
     @Nonnull @NotEmpty public static final String DEFAULT_NICKNAME_FIELD_NAME = "credentialNickname";
     
     /** Class logger. */
     @Nonnull 
-    private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialAttestationFromFormRequest.class);
-    
-    /** Name of the public key credential attestation parameter. */
-    @Nonnull @NotEmpty private String publicKeyCredentialAttestationParameterName;
+    private final Logger log = LoggerFactory.getLogger(ExtractNicknameFromFormRequest.class);
     
     /** Name of the nickname parameter. */
     @Nonnull @NotEmpty private String credentialNicknameParameterName;
     
+    /** 
+     * The EventID of the event to build if no credential nickname can be found in the request and a nickname is 
+     * required. 
+     */
+    @Nonnull @NotEmpty private String noCredentialNicknameEventId;
+        
     /** Constructor. */
-    public ExtractPublicKeyCredentialAttestationFromFormRequest() {
+    public ExtractNicknameFromFormRequest() {
         super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
-        publicKeyCredentialAttestationParameterName = DEFAULT_PARAMETER_NAME;
         credentialNicknameParameterName = DEFAULT_NICKNAME_FIELD_NAME;
+        noCredentialNicknameEventId = WebAuthnRegistrationEventIds.INVALID_REGISTRATION;
     }
     
     /**
-     * Set the name of the parameter to extract the public key credential attestation response from.
+     * Set the name of the parameter to extract the credential nickname from.
      * 
-     * @param field the field name
+     * @param field field name
      */
-    public void setPublickKeyCredentialAttestationParameterName(@Nonnull @NotEmpty final String field) {
+    public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
         checkSetterPreconditions();
         
-        publicKeyCredentialAttestationParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field), 
-                "Attestation parameter name cannot be null or empty");
+        credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
+                "Nickname parameter can not be null or empty");
     }
     
     /**
-     * Set the name of the parameter to extract the credential nickname from.
+     * Set the EventID of the event to build if no credential nickname can be found in the request and a nickname is 
+     * required.  
      * 
-     * @param field field name
+     * @param eventId the eventId to build.
      */
-    public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
+    public void setNoCredentialNicknameEventId(@Nonnull @NotEmpty final String eventId) {
         checkSetterPreconditions();
-        
-        credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
-                "Nickname parameter can not be null or empty");
+        noCredentialNicknameEventId = Constraint.isNotEmpty(eventId, "NoCredentialsEventId can not be null or empty");
     }
     
     @Override
@@ -104,38 +95,20 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
         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 pkCredAttestationJson = request.getParameter(publicKeyCredentialAttestationParameterName);        
-        if (StringSupport.trimOrNull(pkCredAttestationJson) == null) {
-            log.debug("{} No PublicKeyCredential with authenticator attestation response in request", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext,  AuthnEventIds.NO_CREDENTIALS);
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
             return;
         }
-        log.trace("{} PublicKeyCredential authenticator attestation response:'{}'",getLogPrefix(), 
-                pkCredAttestationJson);
         
         final String credNickname = request.getParameter(credentialNicknameParameterName);   
         log.trace("{} Credential nickname is '{}'",getLogPrefix(), credNickname);
         if (StringSupport.trimOrNull(credNickname) == null && context.isNicknameRequired()) {
             log.debug("{} Credential nickname is not in the request but is required", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
-            return;
-        }
-
-        try {
-            final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> 
-                pkCredAttestation = PublicKeyCredential.parseRegistrationResponseJson(pkCredAttestationJson);
-            context.setPublicKeyCredentialAttestationResponse(pkCredAttestation);
-            context.setCredentialNickname(credNickname);
-        } catch (final IOException e) {
-            log.debug("{} Could not parse PublicKeyCredential response from request parameter '{}'", getLogPrefix(),
-                    publicKeyCredentialAttestationParameterName, e);
-            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            ActionSupport.buildEvent(profileRequestContext, noCredentialNicknameEventId);
             return;
         }
+        
+        context.setCredentialNickname(credNickname);
+        
     }
     
 }
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
index 0903471..0402617 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
@@ -39,13 +39,12 @@ import net.shibboleth.shared.primitive.StringSupport;
 
 /**
  * An action that extracts the PublicKeyCredential containing the authenticator attestation response from the incoming
- * HTTP request. Also extracts the user entered credential nickname. Failure to find the attestation results in an
- * a non-proceed event. Similarly, if a nickname is required and not provided a non-proceed event is signalled.
+ * HTTP request. Failure to find the attestation results in an a non-proceed event. 
  * 
  * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
  * @event {AuthnEventIds#NO_CREDENTIALS}
  * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post add an AuthenticatorAttestationResponse and credential nickname (if present) to the registration context
+ * @post add an AuthenticatorAttestationResponse to the registration context
  */
 public class ExtractPublicKeyCredentialAttestationFromFormRequest 
                     extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
@@ -53,9 +52,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
     /** Default public key credential attestation parameter name. */
     @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "publicKeyCredential";
     
-    /** Default nickname parameter name. */
-    @Nonnull @NotEmpty public static final String DEFAULT_NICKNAME_FIELD_NAME = "credentialNickname";
-    
     /** Class logger. */
     @Nonnull 
     private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialAttestationFromFormRequest.class);
@@ -63,14 +59,10 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
     /** Name of the public key credential attestation parameter. */
     @Nonnull @NotEmpty private String publicKeyCredentialAttestationParameterName;
     
-    /** Name of the nickname parameter. */
-    @Nonnull @NotEmpty private String credentialNicknameParameterName;
-    
     /** Constructor. */
     public ExtractPublicKeyCredentialAttestationFromFormRequest() {
         super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
         publicKeyCredentialAttestationParameterName = DEFAULT_PARAMETER_NAME;
-        credentialNicknameParameterName = DEFAULT_NICKNAME_FIELD_NAME;
     }
     
     /**
@@ -85,18 +77,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
                 "Attestation parameter name cannot be null or empty");
     }
     
-    /**
-     * Set the name of the parameter to extract the credential nickname from.
-     * 
-     * @param field field name
-     */
-    public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
-        checkSetterPreconditions();
-        
-        credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
-                "Nickname parameter can not be null or empty");
-    }
-    
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final WebAuthnRegistrationContext context) {
@@ -116,20 +96,11 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
         }
         log.trace("{} PublicKeyCredential authenticator attestation response:'{}'",getLogPrefix(), 
                 pkCredAttestationJson);
-        
-        final String credNickname = request.getParameter(credentialNicknameParameterName);   
-        log.trace("{} Credential nickname is '{}'",getLogPrefix(), credNickname);
-        if (StringSupport.trimOrNull(credNickname) == null && context.isNicknameRequired()) {
-            log.debug("{} Credential nickname is not in the request but is required", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
-            return;
-        }
 
         try {
             final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> 
                 pkCredAttestation = PublicKeyCredential.parseRegistrationResponseJson(pkCredAttestationJson);
             context.setPublicKeyCredentialAttestationResponse(pkCredAttestation);
-            context.setCredentialNickname(credNickname);
         } catch (final IOException e) {
             log.debug("{} Could not parse PublicKeyCredential response from request parameter '{}'", getLogPrefix(),
                     publicKeyCredentialAttestationParameterName, e);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.java
new file mode 100644
index 0000000..7a26530
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.java
@@ -0,0 +1,77 @@
+/*
+ * 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.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link BiConsumer} that sets the credential ID to modify onto the WebAuthn registration context.
+ */
+public class RegistrationContextCredentialToModifyConsumer extends AbstractIdentifiableInitializableComponent 
+    implements BiConsumer<ProfileRequestContext, byte[]> {
+        
+    /** Class logger. */
+    @Nonnull 
+    private final Logger log = LoggerFactory.getLogger(RegistrationContextCredentialToModifyConsumer.class);
+    
+    /** Lookup strategy to locate the WebAuthn registration context. */
+    @Nonnull 
+    private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
+    
+    /** Constructor.*/
+    protected RegistrationContextCredentialToModifyConsumer() {
+        //prc -> WebAuthnContext
+        webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+    }
+    
+    /**
+     * Set WebAuthn registration context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setWebauthnRegistrationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
+        checkSetterPreconditions();
+
+        webauthnRegistrationContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(final ProfileRequestContext prc, final byte[] credentialId) {
+        final WebAuthnRegistrationContext webauthnRegistrationContext = 
+                webauthnRegistrationContextLookupStrategy.apply(prc);
+        if (webauthnRegistrationContext == null) {
+            log.warn("{} No WebAuthn registration context returned by lookup strategy, can not set credential "
+                    + "identifier to modify",getId()); 
+            return;
+        } 
+        webauthnRegistrationContext.setCredentialIdToModify(credentialId);    
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java
new file mode 100644
index 0000000..86f281b
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java
@@ -0,0 +1,109 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+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.audit.impl.AbstractWebAuthnAuditingAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * An action that updates the nickname of a credential in the credential repository.
+ * 
+ * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post a credential's nickname is updated in the credential repository
+ */
+public class UpdateCredentialNickname extends AbstractWebAuthnAuditingAction<WebAuthnRegistrationContext> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(UpdateCredentialNickname.class);  
+    
+    /** The credential repository to use.*/
+    @NonnullAfterInit private WebAuthnCredentialRepository repository;   
+    
+    /** Constructor.*/
+    protected UpdateCredentialNickname() {
+        super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        repository = getCredentialRepository();
+        if (repository == null) {
+            throw new ComponentInitializationException("Credential repository can not be null");
+        }
+    }
+
+    @Override
+    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 come from a form the user
+        // can manipulate (it should be the authenticated user).
+        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;
+        }
+        
+        // This comes from the credential the user input
+        final byte[] credentialId = context.getCredentialIdToModify();        
+        if (credentialId == null) {
+            log.error("{} Unable to find credentialId in registration context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext,  WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            return;
+        }
+        final ByteArray credentialIdAsByteArray = new ByteArray(credentialId);
+        
+        // This comes from the nickname the user entered directly.
+        final String nickname = context.getCredentialNickname();
+        if (StringSupport.trimOrNull(nickname) == null) {
+            log.error("{} Unable to find nickname in registration context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext,  WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            return;
+        }        
+        
+        final boolean updated = repository.updateNickname(username, credentialIdAsByteArray, nickname);
+        log.info("{} Credential '{}' {} updated for user '{}'", getLogPrefix(), 
+                credentialIdAsByteArray.getBase64(), updated ? "was" : "was not", username);
+        if(updated) {
+            auditSuccess(profileRequestContext, "credential-updated");
+        } else {
+            auditFailure(profileRequestContext, "credential-updated");
+        }        
+        
+        // Remove the key ID to be updated in case the context state is re-used
+        context.setCredentialIdToModify(null);
+    }
+    
+}
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 6eeca45..387ccf5 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
@@ -624,7 +624,7 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
                     .findFirst();
             
             if (credential.isEmpty()) {
-                log.warn("Can not update signature count for user '{}' and credential '{}'. "
+                log.warn("Can not update last used time for user '{}' and credential '{}'. "
                         + "No existing credential found.", username, credentialId.getBase64());
                 return false;                
             }
@@ -641,7 +641,57 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
             
             if (updateCredentialSet.isEmpty()) {
                 // We are updating, so this should not be possible
-                log.debug("Can not update signature count for user '{}' and credential '{}'. "
+                log.debug("Can not last used time for user '{}' and credential '{}'. "
+                        + "Update set is empty.", username, credentialId.getBase64());
+                return false;
+            }
+
+            try {
+                final Long updatedVersion = storageService.updateWithVersion(
+                        existingRegistrations.getVersion(), STORAGE_CONTEXT, username, updateCredentialSet, 
+                        serializer, null);                
+                return updatedVersion != null;
+            } catch (final IOException | VersionMismatchException e) {
+                throw new CredentialRepositoryException(e);    
+            } 
+            
+        } finally {
+            writeLock.unlock();
+        }        
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean updateNickname(@Nonnull final String username, @Nonnull final ByteArray credentialId, 
+            final String nickname) {
+        final Lock writeLock = lock.writeLock();
+        try {
+            writeLock.lock();
+            
+            final VersionedCredentialSet existingRegistrations = getRegistrationsByUsernameWithVersion(username);
+            final Optional<CredentialRecord> credential = existingRegistrations.getCredentials().stream()
+                    .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId()))
+                    .findFirst();
+            
+            if (credential.isEmpty()) {
+                log.warn("Can not update nickname for user '{}' and credential '{}'. "
+                        + "No existing credential found.", username, credentialId.getBase64());
+                return false;                
+            }
+
+            // Copy the credential into a new credential and update the last used time
+            final CredentialRecord updatedRegistration = credential.get().toBuilder()
+                    .withCredentialNickname(nickname)
+                    .build();
+            
+            final Set<CredentialRecord> updateCredentialSet = 
+                    new LinkedHashSet<>(existingRegistrations.getCredentials());
+            updateCredentialSet.remove(credential.get());
+            updateCredentialSet.add(updatedRegistration);
+            
+            if (updateCredentialSet.isEmpty()) {
+                // We are updating, so this should not be possible
+                log.debug("Can not nickname for user '{}' and credential '{}'. "
                         + "Update set is empty.", username, credentialId.getBase64());
                 return false;
             }
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 ad9076b..3a39e08 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
@@ -178,6 +178,15 @@
     <bean id="ExtractPublicKeyCredentialAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractPublicKeyCredentialAttestationFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+        
+    <bean id="ExtractNicknameFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+    
+    <bean id="ExtractNicknameToUpdateFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" 
+        p:noCredentialNicknameEventId="EmptyNickname"/>
 
     <bean id="ExtractKeyRemovalInformationFromFormRequest" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest"
@@ -186,6 +195,20 @@
             <bean class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RegistrationContextCredentialRemovalConsumer"/>
         </property> 
      </bean>
+     
+     <bean id="ExtractKeyUpdateInformationFromFormRequest" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+        <property name="contextSettingConsumer">
+            <bean class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RegistrationContextCredentialToModifyConsumer"/>
+        </property> 
+     </bean>
+     
+     <bean id="UpdateCredentialNickname" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.UpdateCredentialNickname" 
+        p:populateAuditContextAction="#{%{idp.authn.webauthn.registration.audit.enabled:false} ? getObject('RegistrationOperationPopulateAuditContext') : null}"
+        p:writeAuditLogAction="#{%{idp.authn.webauthn.registration.audit.enabled:false} ? getObject('WriteAdminAuditLog') : null}"
+        p:auditContextCreationStrategy-ref="AdminAuditContextLookup"/>   
 
     <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential" 
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 a195509..b43227e 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
@@ -126,6 +126,7 @@
        <transition on="finish" to="RegistrationComplete" />
        <transition on="resume" to="RegistrationCompleteResumeFlow" />
        <transition on="addKey" to="AddKey" />
+       <transition on="updateNickname" to="UpdateNickname" />
        <transition on="deleteKey" to="DeleteKey" />
        <on-exit>
          <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationInformationContext)).reset()"/>
@@ -133,8 +134,24 @@
        </on-exit>       
     </view-state>
     
+    <action-state id="UpdateNickname">
+        <evaluate expression="ExtractNicknameToUpdateFromFormRequest"/>
+        <evaluate expression="ExtractKeyUpdateInformationFromFormRequest"/>
+        <evaluate expression="UpdateCredentialNickname"/>
+        <evaluate expression="'proceed'" />
+        
+        <transition on="EmptyNickname" to="DoClientStorageSaveContext">
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationErrorContext)).addClassifiedError('EmptyNickname')"/>        
+        </transition>
+    
+        <transition on="proceed" to="DoClientStorageSaveContext">
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationInformationContext)).addClassifiedMessage('NicknameUpdated')"/>
+        </transition>
+    </action-state>
+    
    <action-state id="AddKey">
         <evaluate expression="ExtractPublicKeyCredentialAttestationFromFormRequest"/>
+        <evaluate expression="ExtractNicknameFromFormRequest"/>
         <evaluate expression="CheckRegistrationPolicy"/>        
         <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
         <evaluate expression="StorePublicKeyCredential"/> 
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
index 8034b7b..e7b6ec3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
@@ -101,4 +101,35 @@ tr:hover {
 
 .info {
     background-color: #2196F3;
+}
+
+.update-btn {
+    display: inline-flex;
+    align-items: center;
+    font-size: 10px;
+    padding: 2px 6px;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    gap: 4px;
+}
+
+.nickname-input {
+    padding: 4px 6px;
+    font-size: 14px;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    width: 100px;
+}
+
+.update-nickname-form {
+    display: flex;
+    gap: 1px;
+}
+
+.update-icon {
+    width: 15px;
+    height: 25px;
 }
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
index d70e5a0..16ad014 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
@@ -38,6 +38,7 @@ idp.webauthn.register.submit = Submit registration
 idp.webauthn.register.username.explain = Please enter your username below.
 idp.webauthn.register.username.proceed = Next
 idp.webauthn.register.unsupported = Your browser is not WebAuthn compatible
+idp.webauthn.register.credential.nickname.update.button.text = Update Nickname
 
 idp.webauthn.admin.search.title = Search for user
 idp.webauthn.admin.header = Registered keys for
@@ -65,6 +66,8 @@ idp.webauthn.debug.register.request = Registration options
 InvalidRegistration = Key registration unsuccessful
 ValidRegistration = Key was registered successfully
 KeyRemoved = Key was removed successfully
+NicknameUpdated = Nickname updated
+EmptyNickname = Nickname can not be empty
 
 # Messages to report back to the admin during key management
 SearchUsernameNotFoundAfterC14N = Error determining username to search for
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
index 6d20db7..4dfb1a2 100644
--- 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
@@ -65,9 +65,9 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                <th>#springMessageText("idp.webauthn.admin.table.header.authenticatorIcon", "Icon")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.labels", "Labels")</th>
                                #if ($lastUsed == 'true')
-                                  <th>#springMessageText("idp.webauthn.admin.table.header.lastUsedTime", "Last Used")</th>
+                                <th>#springMessageText("idp.webauthn.admin.table.header.lastUsedTime", "Last Used")</th>
                                #else
-                                  <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+                                <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
                                #end
                                <th>#springMessageText("idp.webauthn.admin.table.header.hasMetadata", "Metadata?")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
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 0e1d062..a483604 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
@@ -141,33 +141,44 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                <th>#springMessageText("idp.webauthn.register.table.header.action", "Action")</th>
                             </tr>
                             #foreach($cred in $webauthnRegContext.existingCredentials)
-                            <tr>
-                               <td>$encoder.encodeForHTML($cred.credentialRecord.nickname)</td>                              
-                               #if ($cred.authenticatorDescription)
-                                   <td>$encoder.encodeForHTML($cred.authenticatorDescription)</td>
-                               #else
-                                    <td>#springMessageText("idp.webauthn.register.table.unknownCredential", "unknown")</td>
-                               #end
-                               #if ($cred.icon)
-                                    <td> <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon)" alt="$encoder.encodeForHTML($cred.credentialRecord.nickname)-authenticator-icon"/></td> 
-                               #else
-                                    <td></td>
-                               #end
-                               <td>
-                                #foreach($label in $cred.labels)
-                                    <span class="label info">$encoder.encodeForHTML($label)</span>
-                                #end  
-                               </td>                             
-                               <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
-                               <td>
-                                  <form id="delete_key_form" action="$flowExecutionUrl" method="post">
-                                     #parse("csrf/csrf.vm")
-                                     <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
-                                     <button class="webauthn-table-button" onclick="$areYouSure" id="removeButton" type="submit" name="_eventId_deleteKey">
-                                    #springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
-                                  </form>
-                               </td>
-                            </tr>
+                                <tr>
+                                   <td>
+                                       <form id="update_nickname_form" action="$flowExecutionUrl" method="post" class="update-nickname-form">
+                                             #parse("csrf/csrf.vm")
+                                             <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
+                                             <input name="credentialNickname" class="nickname-input" value="$encoder.encodeForHTML($cred.credentialRecord.nickname)"/>     
+                                             <button title="#springMessageText("idp.webauthn.register.credential.nickname.update.button.text", "Update Nickname")" class="update-btn" onclick="$areYouSure" id="updateNicknameButton" type="submit" name="_eventId_updateNickname">
+                                                 <svg class="update-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+                                                    <path d="M17.65 6.40A7.95 8.95 0 0 0 12 4.6V1L7 7l5 5V7a6 5 0 1 1-6 6h-2a8 8 0 1 0 13.65-6.65z"/>
+                                                  </svg>
+                                             </button>                                                                             
+                                       </form>                              
+                                   </td>                              
+                                   #if ($cred.authenticatorDescription)
+                                       <td>$encoder.encodeForHTML($cred.authenticatorDescription)</td>
+                                   #else
+                                        <td>#springMessageText("idp.webauthn.register.table.unknownCredential", "unknown")</td>
+                                   #end
+                                   #if ($cred.icon)
+                                        <td> <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon)" alt="$encoder.encodeForHTML($cred.credentialRecord.nickname)-authenticator-icon"/></td> 
+                                   #else
+                                        <td></td>
+                                   #end
+                                   <td>
+                                    #foreach($label in $cred.labels)
+                                        <span class="label info">$encoder.encodeForHTML($label)</span>
+                                    #end  
+                                   </td>                             
+                                   <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
+                                   <td>
+                                      <form id="delete_key_form" action="$flowExecutionUrl" method="post">
+                                         #parse("csrf/csrf.vm")
+                                         <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
+                                         <button class="webauthn-table-button" onclick="$areYouSure" id="removeButton" type="submit" name="_eventId_deleteKey">
+                                        #springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
+                                      </form>
+                                   </td>
+                                </tr>
                              #end 
                          </table>                                        
                      #else
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java
new file mode 100644
index 0000000..b54896e
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.testing.ConstantSupplier;
+
+/**
+ * Tests for {@link ExtractKeyInformationFromFormRequest}
+ */
+public class ExtractKeyInformationFromFormRequestTest extends AbstractWebAuthnTest {
+    
+    private ExtractKeyInformationFromFormRequest action;
+    
+    private WebAuthnRegistrationContext context;
+    
+    private MockHttpServletRequest request;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();
+        
+        request = new MockHttpServletRequest();
+        
+        action = new ExtractKeyInformationFromFormRequest();
+    } 
+    
+    @Test
+    public void testExtraction() throws Exception {
+        final SimpleContext contextToUpdate = new SimpleContext();
+        action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+        
+        final byte[] credentialIdBytes = generateRandomBytes(16);
+        final String credentialIdb64 = Base64Support.encodeURLSafe(credentialIdBytes);
+        
+        request.addParameter(ExtractKeyInformationFromFormRequest.DEFAULT_PARAMETER_NAME, 
+                new String[]{credentialIdb64});
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertNull(result);
+        assertNotNull(contextToUpdate);
+        assert contextToUpdate != null;
+        assertNotNull(contextToUpdate.getCredentialIdToRemove());
+        
+    }
+    
+    @Test
+    public void testExtraction_NoCredentialIdFound() throws Exception {
+        final SimpleContext contextToUpdate = new SimpleContext();
+        action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertFailure(result,  WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
+        
+    }
+    
+    @Test
+    public void testExtraction_CredentialIdCouldNotBeBase64Decoded() throws Exception {
+        final SimpleContext contextToUpdate = new SimpleContext();
+        action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+
+        request.addParameter(ExtractKeyInformationFromFormRequest.DEFAULT_PARAMETER_NAME, 
+                new String[]{"not-encoded"});
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertFailure(result,  WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
+        
+    }
+    
+    /** Simple context to store the update.*/
+    private class SimpleContext extends BaseContext {
+        
+        private byte[] credentialIdToRemove;
+        
+        public SimpleContext() {
+        }
+        
+        public void setCredentialIdToRemove(final byte[] credId) {
+            this.credentialIdToRemove = credId;
+        }
+
+        public byte[] getCredentialIdToRemove() {
+            return credentialIdToRemove;
+        }
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java
new file mode 100644
index 0000000..9e85daa
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.testing.ConstantSupplier;
+
+/**
+ * Tests for {@link ExtractNicknameFromFormRequest}
+ */
+public class ExtractNicknameFromFormRequestTest extends AbstractWebAuthnTest {
+    
+    private ExtractNicknameFromFormRequest action;
+    
+    private WebAuthnRegistrationContext context;
+    
+    private MockHttpServletRequest request;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();        
+        request = new MockHttpServletRequest();        
+        action = new ExtractNicknameFromFormRequest();
+        action.setWebAuthnClient(client);
+        action.setCredentialRepository(credentialRepo);
+        context.setNicknameRequired(true);
+    } 
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testExtraction() throws Exception {
+        
+        request.addParameter(ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, 
+                "nickname");
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertNull(result);
+        assertNotNull(context.getCredentialNickname()); 
+    }
+
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testExtraction_NoNicknameInResponse_IsRequired() throws Exception {
+
+        context.setNicknameRequired(true);
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertFailure(result, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);  
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testExtraction_NoNicknameInResponse_NotRequired() throws Exception {
+        
+        context.setNicknameRequired(false);
+        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+        action.initialize();
+        
+        final Event result = action.execute(src);
+        assertNull(result);
+        assertNull(context.getCredentialNickname());
+    }
+    
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
index 966133f..e306097 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
@@ -53,7 +53,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
         action = new ExtractPublicKeyCredentialAttestationFromFormRequest();
         action.setWebAuthnClient(client);
         action.setCredentialRepository(credentialRepo);
-        context.setNicknameRequired(true);
     } 
     
     @SuppressWarnings("null")
@@ -67,16 +66,13 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
         
         request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME, 
                 attestationResponseJson);
-        request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, 
-                "nickname");
+
         action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
         action.initialize();
         
         final Event result = action.execute(src);
         assertNull(result);
-        assertNotNull(context.getCredentialNickname());
         assertNotNull(context.getPublicKeyCredentialAttestationResponse());
-        assertEquals(context.getCredentialNickname(),"nickname");
         assertEquals(context.getPublicKeyCredentialAttestationResponse().getId(), attestationResponse.getId()); 
     }
     
@@ -86,8 +82,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
         
         request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME, 
                 "this-is-not-good");
-        request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, 
-                "nickname");
         action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
         action.initialize();
         
@@ -99,57 +93,11 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
     @Test
     public void testExtraction_NoCredentialInResponse() throws Exception {
         
-        request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, 
-                "nickname");
         action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
         action.initialize();
         
         final Event result = action.execute(src);
         assertFailure(result, AuthnEventIds.NO_CREDENTIALS);        
-    }
-    
-    @SuppressWarnings("null")
-    @Test
-    public void testExtraction_NoNicknameInResponse_IsRequired() throws Exception {
-        
-        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
-            attestationResponse = createAttestationReponse();
- 
-        final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
-        
-        request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME, 
-                attestationResponseJson);
-
-        context.setNicknameRequired(true);
-        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
-        action.initialize();
-        
-        final Event result = action.execute(src);
-        assertFailure(result, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);  
-    }
-    
-    @SuppressWarnings("null")
-    @Test
-    public void testExtraction_NoNicknameInResponse_NotRequired() throws Exception {
-        
-        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
-            attestationResponse = createAttestationReponse();
- 
-        final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
-        
-        request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME, 
-                attestationResponseJson);
-
-        context.setNicknameRequired(false);
-        action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
-        action.initialize();
-        
-        final Event result = action.execute(src);
-        assertNull(result);
-        assertNull(context.getCredentialNickname());
-        assertNotNull(context.getPublicKeyCredentialAttestationResponse());
-        assertEquals(context.getPublicKeyCredentialAttestationResponse().getId(), attestationResponse.getId()) ;
-    }
-    
+    }    
 
 }
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
index 96dd034..8479448 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
@@ -39,6 +39,7 @@ import com.yubico.webauthn.data.RegistrationExtensionInputs;
 import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest;
 import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractPublicKeyCredentialAttestationFromFormRequest;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
@@ -185,7 +186,7 @@ public class TestRegistrationFlow extends AbstractWebAuthnFlowTest{
         final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
         setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
                 attestationResponseJson, 
-                ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
+                ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
         externalContext.setEventId("addKey");
         result.getSecond().setCurrentState("DisplayWebAuthnRegistrationView");
         result.getSecond().resume(externalContext);
@@ -275,7 +276,7 @@ public class TestRegistrationFlow extends AbstractWebAuthnFlowTest{
         final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
         setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
                 attestationResponseJson, 
-                ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
+                ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
         externalContext.setEventId("addKey");
         result.getSecond().setCurrentState("DisplayWebAuthnRegistrationView");
         result.getSecond().resume(externalContext);

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


More information about the commits mailing list