[java-idp-plugin-webauthn] branch main updated: Add credential labeller

Phil Smart philip.smart at jisc.ac.uk
Tue Oct 29 16:59:36 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=513daf5ece761e8cdf9fde2496a146845c6e30e0

The following commit(s) were added to refs/heads/main by this push:
     new 513daf5  Add credential labeller
513daf5 is described below

commit 513daf5ece761e8cdf9fde2496a146845c6e30e0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Oct 29 16:59:33 2024 +0000

    Add credential labeller
    
     - Allow for configurable, customisable, sets of credential labellers
     - Labels are for display purposes only e.g. to display 'this is a
    passkey', or 'this is only allowed for 2fa' type labels in both the
    registration and management views.
---
 .../webauthn/context/BaseWebAuthnContext.java      |   2 +-
 .../webauthn/storage/EnhancedCredentialRecord.java | 193 +++++++--------------
 .../impl/AuthenticatorCapabilitiesLabeller.java    |  89 ++++++++++
 .../admin/impl/ChainingCredentialLabeller.java     |  69 ++++++++
 .../CreatePublicKeyCredentialCreationOptions.java  |   4 +-
 .../admin/impl/LabelAdminCredentialRecords.java    |  90 ++++++++++
 .../admin/impl/LabelCredentialRecords.java         |  89 ++++++++++
 .../admin/impl/LookupCredentialsForUser.java       |  10 +-
 .../admin/impl/PasskeyCredentialLabeller.java      |  60 +++++++
 .../CreatePublicKeyCredentialRequestOptions.java   |   2 +-
 .../webauthn/impl/LookupRegisteredCredentials.java |  11 +-
 .../webauthn-management-beans.xml                  |  12 +-
 .../webauthn-management-flow.xml                   |   5 +-
 .../webauthn-registration-beans.xml                |  11 +-
 .../webauthn-registration-flow.xml                 |   3 +-
 .../conf/authn/webauthn-management-config.xml      |  25 +++
 .../conf/authn/webauthn-registration-config.xml    |  25 +++
 .../idp/plugin/authn/webauthn/css/webauthn.css     |  15 +-
 .../idp/plugin/authn/webauthn/messages.properties  |   9 +-
 .../authn/webauthn/views/webauthn-management.vm    |  28 +--
 .../authn/webauthn/views/webauthn-register.vm      |  16 +-
 ...eatePublicKeyCredentialCreationOptionsTest.java |   6 +-
 .../impl/LookupRegisteredCredentialsTest.java      |  13 +-
 .../authn/webauthn/test-beans-registration.xml     |  19 ++
 24 files changed, 629 insertions(+), 177 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
index c2a2025..63b0261 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
@@ -95,7 +95,7 @@ public class BaseWebAuthnContext extends BaseContext {
     }
 
     /**
-     * Set user's credentials that have already been registered with the credential repository.
+     * Set the user's credentials that have already been registered with the credential repository.
      * 
      * @param credentials the set of credentials
      * 
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/EnhancedCredentialRecord.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/EnhancedCredentialRecord.java
index 2206abc..53a7f2c 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/EnhancedCredentialRecord.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/EnhancedCredentialRecord.java
@@ -14,6 +14,7 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.storage;
 
+import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -25,7 +26,6 @@ import javax.annotation.Nullable;
 import com.yubico.fido.metadata.MetadataBLOBPayloadEntry;
 
 import net.shibboleth.idp.plugin.authn.webauthn.metadata.AaguidEntry;
-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.annotation.constraint.Unmodifiable;
@@ -36,30 +36,33 @@ import net.shibboleth.shared.logic.Constraint;
  * An ephemeral wrapper class that holds a {@link CredentialRecord} and any associated metadata. Created and
  * used during registration or authentication and then discarded. This is not meant to be serialised or stored.
  */
-public final class EnhancedCredentialRecord {
+public class EnhancedCredentialRecord {
     
-    /** The wrapped credential registration.*/
-    @Nonnull private final CredentialRecord credentialRegistration;
+    /** The wrapped credential record.*/
+    @Nonnull private final CredentialRecord credentialRecord;
     
     /** Optional metadata about the authenticator. Will be an empty set if not used. */
-    @Nonnull @Unmodifiable @NonnullElements @NotLive private final Set<MetadataBLOBPayloadEntry> authenticatorMetadata;
+    @Nonnull @Unmodifiable @NonnullElements @NotLive private Set<MetadataBLOBPayloadEntry> authenticatorMetadata;
     
     /** 
-     * An AAGUID entry provided by the supplementary JSON file that describes authenticators not in the MDS feed e.g. 
+     * An AAGUID entry provided by a supplementary JSON file that describes authenticators not in the MDS feed e.g. 
      * for software providers. Supplies a description and icon for UI only.
      */
-    @Nullable private final AaguidEntry aaguidMetadata;
-
+    @Nullable private AaguidEntry aaguidMetadata;
+    
+    /** Labels that can be set about the credential or authenticator that created it. For display only.*/
+    @Nonnull @Unmodifiable @NonnullElements @NotLive private List<String> labels;
+    
     /**
      * 
      * Constructor.
      *
-     * @param builder the builder to construct this instance from
+     * @param credential the credential to wrap.
      */
-    private EnhancedCredentialRecord(final Builder builder) {
-        this.credentialRegistration = builder.credentialRecord;
-        this.authenticatorMetadata = builder.authenticatorMetadata;
-        this.aaguidMetadata = builder.aaguidMetadata;
+    public EnhancedCredentialRecord(@Nonnull final CredentialRecord credential) {
+        credentialRecord = Constraint.isNotNull(credential, "Credential can not be null");
+        authenticatorMetadata = CollectionSupport.emptySet();
+        labels = CollectionSupport.emptyList();
     }
     
     /**
@@ -67,8 +70,20 @@ public final class EnhancedCredentialRecord {
      * 
      * @return the wrapped credential record.
      */
-    public CredentialRecord getCredentialRegistration() {
-        return credentialRegistration;
+    @Nonnull public CredentialRecord getCredentialRecord() {
+        return credentialRecord;
+    }
+    
+    /**
+     * Set the authenticator metadata.
+     * 
+     * @param metadata The authenticator metadata to set.
+     */
+    public void setAuthenticatorMetadata(@Nullable final Set<MetadataBLOBPayloadEntry> metadata) {
+        if (metadata != null) {
+            authenticatorMetadata = CollectionSupport.copyToSet(metadata.stream().filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toSet())).get());
+        }
     }
         
     /**
@@ -77,15 +92,46 @@ public final class EnhancedCredentialRecord {
      * @return Returns the attestationMetadata.
      */
     @Nonnull @Unmodifiable @NonnullElements @NotLive
-    public synchronized Set<MetadataBLOBPayloadEntry> getAuthenticatorMetadata() {
+    public Set<MetadataBLOBPayloadEntry> getAuthenticatorMetadata() {
         return authenticatorMetadata;
     }
     
+    
+    /**
+     * Set the AAGUID entry that describes the authenticator. Typically set for those authenticators that are not
+     * in the FIDO MDS feed.
+     * 
+     * @param metadata The aaguid metadata to set.
+     */
+    public void setAaguidMetadata(@Nullable final AaguidEntry metadata) {
+        if (metadata != null) {
+            aaguidMetadata = metadata;
+        }
+    }
+    
+    /**
+     * Set labels about the credential or authenticator that created it. For display only.
+     * 
+     * @param labels The labels to set.
+     */
+    public void setLabels(@Nonnull final List<String> labelsIn) {
+        labels = CollectionSupport.copyToList(Constraint.isNotNull(labelsIn, "labels can not be null"));
+    }
+    
+    /**
+     * Get labels about the credential or the authenticator that created it. For display only.
+     *
+     * @return the labels.
+     */
+    @Nonnull @Unmodifiable @NonnullElements @NotLive public List<String> getLabels() {
+        return labels;
+    }
+    
     /**
      * Get the human-readable, short description of the authenticator (in English) iff either; the attestation metadata 
      * exists or aaguid metadata exists (preferencing the FIDO2 attestation metadata).
      * 
-     * <p>If there is more than one metadata entry, it picks the first it can find with a description.</p>
+     * <p>If there is more than one metadata entry, pick the first it can find with a description.</p>
      * 
      * @return the  human-readable, short description of the authenticator (in English), or <code>null</code>.
      */
@@ -110,7 +156,7 @@ public final class EnhancedCredentialRecord {
     /** 
      * Get a <code>data:</code> URL encoded PNG icon for the authenticator.
      * 
-     * <p>If there is more than one metadata entry, it picks the first it can find with an icon. If the 
+     * <p>If there is more than one metadata entry, pick the first it can find with an icon. If the 
      * FIDO2 attestation metadata does not exist, the icon is looked up in the aaguid metadata.</p>
      * 
      * @return the icon encoded as a PNG <code>data:</code> URL 
@@ -134,115 +180,4 @@ public final class EnhancedCredentialRecord {
         
     }
 
-    /**
-     * The builder.
-     * 
-     * @return the next stage
-     */
-    public static ICredentialRegistrationStage builder() {
-        return new Builder();
-    }
-
-    /**
-     * A builder stage.
-     */
-    public interface ICredentialRegistrationStage {
-        /**
-         * Set the credential record.
-         * 
-         * @param credentialRecord the record
-         * 
-         * @return the next stage
-         */
-        public IBuildStage withCredentialRecord(@Nonnull final CredentialRecord credentialRecord);
-    }
-
-    /**
-     * A builder stage.
-     */
-    public interface IBuildStage {
-        /**
-         * Set the metadata for the authenticator that created the credential.
-         * 
-         * @param authenticatorMetadata the metadata
-         * 
-         * @return the next stage
-         */
-        public IBuildStage withAuthenticatorMetadata(
-                @Nullable final Set<MetadataBLOBPayloadEntry> authenticatorMetadata);
-        
-        /**
-         * Set the metadata for the authenticator from additional sources e.g. for those that do not exist in the
-         * MDS feed such as most software providers.
-         * 
-         * @param aaguidMetadata the metadata
-         * 
-         * @return the next stage
-         */
-        public IBuildStage withAaguidMetadata(
-                @Nullable final AaguidEntry aaguidMetadata);
-
-        /** 
-         * Build this object.
-         * 
-         * @return an instance of this object
-         */
-        public EnhancedCredentialRecord build();
-    }
-
-    /**
-     * The builder.
-     */
-    public static final class Builder implements ICredentialRegistrationStage, IBuildStage {
-        /** The wrapped credential record.*/
-        @NonnullAfterInit private CredentialRecord credentialRecord;        
-        /** Optional metadata about the authenticator. Will be an empty set if not used. */
-        @Nonnull @Unmodifiable @NonnullElements @NotLive 
-        private Set<MetadataBLOBPayloadEntry> authenticatorMetadata;
-        /** 
-         * An entry provided by the supplementary JSON file that describes authenticators not in the MDS feed e.g. for
-         * software providers. Supplies a description and icon for UI only.
-         */
-        @Nullable private AaguidEntry aaguidMetadata;
-
-        /** Constructor.*/
-        private Builder() {
-            authenticatorMetadata = CollectionSupport.emptySet();
-        }
-
-        @Override
-        public IBuildStage withCredentialRecord(
-                @Nonnull final CredentialRecord record) {
-            credentialRecord = Constraint.isNotNull(record, "Credential record can not be null");
-            return this;
-        }
-
-        @Override
-        public IBuildStage withAuthenticatorMetadata(@Nullable final Set<MetadataBLOBPayloadEntry> metadata) {
-            if (metadata != null) {
-                authenticatorMetadata = CollectionSupport.copyToSet(metadata.stream().filter(Objects::nonNull)
-                        .collect(CollectionSupport.nonnullCollector(Collectors.toSet())).get());
-            }
-            return this;
-        }
-        
-
-        /** {@inheritDoc} */
-        @Override
-        public IBuildStage withAaguidMetadata(@Nullable final AaguidEntry metadata) {
-            aaguidMetadata = metadata;
-            return this;
-        }
-
-        @Override
-        public EnhancedCredentialRecord build() {
-            return new EnhancedCredentialRecord(this);
-        }
-
-    }
-
-
-
-    
-
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AuthenticatorCapabilitiesLabeller.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AuthenticatorCapabilitiesLabeller.java
new file mode 100644
index 0000000..f1667e3
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AuthenticatorCapabilitiesLabeller.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+
+/**
+ * An authenticator capabilities labeller function that maps authenticator capabilities to labels for display.
+ */
+ at ThreadSafe
+public class AuthenticatorCapabilitiesLabeller 
+    implements BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> {
+    
+    /** Map authenticator capabilities to labels to display in the registration pages.*/
+    @Nonnull @NonnullElements @Unmodifiable @NotLive private Map<Pair<String,String>,List<String>> capabilityToLabelMap;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param map the authenticator to capabilities map. The key is a {@link Pair} where the first entry is the
+     * capability and the second entry is the value of that capability to match on. The Value of the map is the label
+     * to set.
+     */
+    public AuthenticatorCapabilitiesLabeller(
+            @Nullable @ParameterName(name="capabilityToLabelMap") final Map<Pair<String,String>,List<String>> map) {
+        if (map != null) {
+            capabilityToLabelMap = CollectionSupport.copyToMap(map);
+        } else {
+            capabilityToLabelMap = CollectionSupport.emptyMap();
+        }
+        
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<String> apply(@Nullable final EnhancedCredentialRecord cred, 
+            @Nullable final ProfileRequestContext prc) {
+        
+        if (cred == null) {
+            return CollectionSupport.emptyList();
+        }
+        final List<String> labels = new ArrayList<>();
+        final CredentialRecord credential = cred.getCredentialRecord();
+
+        final Map<String, String> capabilities = credential.getAuthenticatorCapabilities();
+        for (final Entry<Pair<String,String>, List<String>> entry : capabilityToLabelMap.entrySet()) {
+            if (capabilities.containsKey(entry.getKey().getFirst())) {
+                final String value = capabilities.get(entry.getKey().getFirst());
+                if (value != null && value.equals(entry.getKey().getSecond())) {
+                    labels.addAll(entry.getValue());
+                }
+                
+            }
+        }
+        return labels;
+    }
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ChainingCredentialLabeller.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ChainingCredentialLabeller.java
new file mode 100644
index 0000000..a82346c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ChainingCredentialLabeller.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A credential labeller that labels credentials based on a chain of other labellers. All labellers are iterated over
+ * to produce the final list of labels.
+ */
+public class ChainingCredentialLabeller 
+    implements BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>>{
+    
+    /** The chain of credential labellers to use. All are iterated over to produce the final set of labels.*/
+    private final List<BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>>> labellers;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param labellersIn The chain of credential labellers to use.
+     */
+    public ChainingCredentialLabeller(@Nullable @ParameterName(name="labellers") 
+        final List<BiFunction<EnhancedCredentialRecord, ProfileRequestContext, 
+            List<String>>> labellersIn) {
+        
+        if (labellersIn != null) {
+            labellers = CollectionSupport.copyToList(labellersIn);
+        } else {
+            labellers = CollectionSupport.emptyList();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<String> apply(final EnhancedCredentialRecord credential, final ProfileRequestContext prc) {
+        final List<String> labels = new ArrayList<>();
+        for (final BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> labeller : labellers) {
+            final List<String> newLabels = labeller.apply(credential, prc);
+            if (newLabels != null) {
+                labels.addAll(newLabels);
+            }
+        }
+        return labels;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
index 46fe5a5..f5a09a3 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -110,8 +110,10 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAc
 
         try {       
             
+            @SuppressWarnings("null")
             final Set<PublicKeyCredentialDescriptor> existingCredentialDescriptors  = context.getExistingCredentials()
-                    .stream().map(cred -> cred.getCredentialRegistration())
+                    .stream().map(cred -> cred.getCredentialRecord())
+                        .filter(Objects::nonNull)
                         .map(cred -> cred.toPublicKeyCredentialDescriptor())
                         .filter(Objects::nonNull)
                         .collect(Collectors.toSet());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelAdminCredentialRecords.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelAdminCredentialRecords.java
new file mode 100644
index 0000000..ca2c315
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelAdminCredentialRecords.java
@@ -0,0 +1,90 @@
+/*
+ * 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 java.util.List;
+import java.util.function.BiFunction;
+
+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.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Use the labelling function to add labels to each {@link EnhancedCredentialRecord} found in the context. The labels
+ * can then be displayed in the registration view.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @post Add labels to each enhanced credential record in the context
+ */
+// TODO another admin function which is almost identical to the registration variant
+public class LabelAdminCredentialRecords extends AbstractWebAuthnAction<WebAuthnManagementContext> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(LabelAdminCredentialRecords.class);
+    
+    /** The labeller used to generate labels for the credential, for display only.*/
+    @Nonnull @NotLive private BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> labeller;
+    
+    /**
+     * Constructor.
+     *
+     * @param defaultStrategy
+     */
+    protected LabelAdminCredentialRecords() {
+        super(new ChildContextLookup<>(WebAuthnManagementContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class)));
+        labeller = (cred, prc) -> CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Set the labeller used to generate labels for the credential, for display only.
+     * 
+     * @param labeller The labeller to set.
+     */
+    public void setLabeller(@Nonnull 
+            final BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> labellerIn) {
+        checkSetterPreconditions();
+        labeller = Constraint.isNotNull(labellerIn, "labeller can not be null");
+    }
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnManagementContext context) {
+        
+        final Collection<EnhancedCredentialRecord> credentials = context.getFoundCredentials();
+        
+        log.trace("{} Labelling '{}' credentials",getLogPrefix(), credentials.size());
+        for (final EnhancedCredentialRecord credential : credentials) {
+            final List<String> labels = labeller.apply(credential, profileRequestContext);
+            if (labels != null) {
+                log.trace("{} Added labels '{}' for credential '{}'", 
+                        getLogPrefix(), labels, credential.getCredentialRecord().getCredentialIdBase64Url());
+                credential.setLabels(labels);
+            }
+        }    
+    }
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelCredentialRecords.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelCredentialRecords.java
new file mode 100644
index 0000000..14f71a5
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LabelCredentialRecords.java
@@ -0,0 +1,89 @@
+/*
+ * 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 java.util.List;
+import java.util.function.BiFunction;
+
+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.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Use the labelling function to add labels to each {@link EnhancedCredentialRecord} found in the context. The labels
+ * can then be displayed in the registration view.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @post Add labels to each enhanced credential record in the context
+ */
+public class LabelCredentialRecords extends AbstractWebAuthnAction<BaseWebAuthnContext> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(LabelCredentialRecords.class);
+    
+    /** The labeller used to generate labels for the credential, for display only.*/
+    @Nonnull @NotLive private BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> labeller;
+    
+    /**
+     * Constructor.
+     *
+     * @param defaultStrategy
+     */
+    protected LabelCredentialRecords() {
+        super(new ChildContextLookup<>(BaseWebAuthnContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class)));
+        labeller = (cred, prc) -> CollectionSupport.emptyList();
+    }
+    
+    /**
+     * Set the labeller used to generate labels for the credential, for display only.
+     * 
+     * @param labeller The labeller to set.
+     */
+    public void setLabeller(@Nonnull 
+            final BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> labellerIn) {
+        checkSetterPreconditions();
+        labeller = Constraint.isNotNull(labellerIn, "labeller can not be null");
+    }
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final BaseWebAuthnContext context) {
+        
+        final Collection<EnhancedCredentialRecord> credentials = context.getExistingCredentials();
+        
+        log.trace("{} Labelling '{}' credentials",getLogPrefix(), credentials.size());
+        for (final EnhancedCredentialRecord credential : credentials) {
+            final List<String> labels = labeller.apply(credential, profileRequestContext);
+            if (labels != null) {
+                log.trace("{} Added labels '{}' for credential '{}'", 
+                        getLogPrefix(), labels, credential.getCredentialRecord().getCredentialIdBase64Url());
+                credential.setLabels(labels);
+            }
+        }    
+    }
+}
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
index 530841e..58a3b03 100644
--- 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
@@ -32,7 +32,6 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContex
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord.IBuildStage;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.collection.CollectionSupport;
@@ -91,13 +90,14 @@ public class LookupCredentialsForUser extends AbstractWebAuthnAction<WebAuthnMan
                     new HashSet<>(credentials.size());
             
             credentials.stream().filter(Objects::nonNull).forEach(cred -> {
-                final IBuildStage builder = EnhancedCredentialRecord.builder().withCredentialRecord(cred);
+                assert cred != null;
+                final EnhancedCredentialRecord enhancedRecord = new EnhancedCredentialRecord(cred);                 
                 final byte[] aaguid = cred.getAaguid();
                 if (aaguid != null && aaguid.length == 16) {
-                    builder.withAuthenticatorMetadata(getAuthenticatorMetadata(new ByteArray(aaguid)));
-                    builder.withAaguidMetadata(getAaguidMetadata(new AAGUID(new ByteArray(aaguid))));
+                    enhancedRecord.setAuthenticatorMetadata(getAuthenticatorMetadata(new ByteArray(aaguid)));
+                    enhancedRecord.setAaguidMetadata(getAaguidMetadata(new AAGUID(new ByteArray(aaguid))));
                 }
-                enhancedCredentialRegistrations.add(builder.build());
+                enhancedCredentialRegistrations.add(enhancedRecord);
             });
             
             context.setFoundCredentials(enhancedCredentialRegistrations);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PasskeyCredentialLabeller.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PasskeyCredentialLabeller.java
new file mode 100644
index 0000000..65b9894
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PasskeyCredentialLabeller.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A credential labeller function that labels credentials as 'Passkey' if they are discoverable.
+ */
+ at ThreadSafe
+public class PasskeyCredentialLabeller 
+    implements BiFunction<EnhancedCredentialRecord, ProfileRequestContext, List<String>> {
+    
+    /**
+     * 
+     * Constructor.
+    **/
+    public PasskeyCredentialLabeller() {
+        super();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public List<String> apply(@Nullable final EnhancedCredentialRecord cred, 
+            @Nullable final ProfileRequestContext prc) {
+        
+        if (cred == null) {
+            return CollectionSupport.emptyList();
+        }
+        final List<String> labels = new ArrayList<>();
+        final CredentialRecord credential = cred.getCredentialRecord();
+        if (credential.isDiscoverable().isPresent() && credential.isDiscoverable().get()==Boolean.TRUE) {
+            labels.add("Passkey");
+        }        
+        return labels;
+    }
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
index 72cc0b9..060ebc6 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -81,7 +81,7 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAct
         try {
             final Collection<EnhancedCredentialRecord> existingCredentials = context.getExistingCredentials();
             final List<PublicKeyCredentialDescriptor> existingCredentialDescriptors  = existingCredentials.stream()
-                .map(cred -> cred.getCredentialRegistration())
+                .map(cred -> cred.getCredentialRecord())
                 .map(cred -> cred.toPublicKeyCredentialDescriptor())
                 .filter(Objects::nonNull)
                 .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
index 284c620..4b98a4f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
@@ -37,7 +37,6 @@ import net.shibboleth.idp.plugin.authn.webauthn.authn.WebAuthnAuthenticationEven
 import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.EnhancedCredentialRecord.IBuildStage;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
@@ -179,12 +178,14 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnAction<BaseWebA
                 new HashSet<>(credentials.size());
         
         credentials.stream().filter(Objects::nonNull).forEach(cred -> {
-            final IBuildStage builder = EnhancedCredentialRecord.builder().withCredentialRecord(cred);
+            assert cred != null;
+            final EnhancedCredentialRecord enhancedRecord = new EnhancedCredentialRecord(cred);            
+            final byte[] aaguid = cred.getAaguid();
             if (cred.getAaguid() != null) {
-                builder.withAuthenticatorMetadata(getAuthenticatorMetadata(new ByteArray(cred.getAaguid())));
-                builder.withAaguidMetadata(getAaguidMetadata(new AAGUID(new ByteArray(cred.getAaguid()))));
+                enhancedRecord.setAuthenticatorMetadata(getAuthenticatorMetadata(new ByteArray(aaguid)));
+                enhancedRecord.setAaguidMetadata(getAaguidMetadata(new AAGUID(new ByteArray(aaguid))));
             }
-            enhancedCredentialRegistrations.add(builder.build());
+            enhancedCredentialRegistrations.add(enhancedRecord);
         });
         context.setExistingCredentials(enhancedCredentialRegistrations);
         
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
index 891e252..ed54578 100644
--- 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
@@ -66,13 +66,21 @@
     
     <bean id="UpdateAdminSearchUsernameWithC14nPrincipal" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.UpdateAdminContextWithC14nPrincipal"
-        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext"/>   
-        
+        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext"/>           
     
     <bean id="LookupCredentialsForUser" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LookupCredentialsForUser"
         p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext" />
         
+    <bean id="LabelCredentialRecords" parent="AbstractWebAuthnBaseAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LabelAdminCredentialRecords"
+        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext" 
+        p:labeller="#{getObject('%{idp.authn.webauthn.registration.labeller:shibboleth.authn.WebAuthn.ChainingCredentialLabeller}')}"/>
+        
+    <bean id="shibboleth.authn.WebAuthn.ChainingCredentialLabeller" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ChainingCredentialLabeller"
+        c:labellers="#{getObject('shibboleth.authn.WebAuthn.CredentialLabellerList')}"/>
+        
     <bean id="ExtractKeyRemovalInformationFromFormRequest" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
index 98192cc..8cc5a5b 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
@@ -68,11 +68,12 @@
         <evaluate expression="UpdateAdminSearchUsernameWithC14nPrincipal"/>
         
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="LookupCredentials" />      
+        <transition on="proceed" to="LookupCredentials" />  
     </action-state>
     
     <action-state id="LookupCredentials">       
-       <evaluate expression="LookupCredentialsForUser"/>   
+       <evaluate expression="LookupCredentialsForUser"/>  
+       <evaluate expression="LabelCredentialRecords"/> 
       
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ManagementView" />    
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 1a8be50..ce929a9 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
@@ -132,7 +132,16 @@
     <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
         p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
-
+        
+    <bean id="LabelCredentialRecords" parent="AbstractWebAuthnBaseAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LabelCredentialRecords"
+        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" 
+        p:labeller="#{getObject('%{idp.authn.webauthn.registration.labeller:shibboleth.authn.WebAuthn.ChainingCredentialLabeller}')}"/>
+        
+    <bean id="shibboleth.authn.WebAuthn.ChainingCredentialLabeller" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ChainingCredentialLabeller"
+        c:labellers="#{getObject('shibboleth.authn.WebAuthn.CredentialLabellerList')}"/>
+    
     <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
         p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" 
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 c729ece..cc6c46a 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
@@ -92,7 +92,8 @@
     <!-- Recreate a registration context from the result of authentication, and create WebAuthn GET creation options -->
      <action-state id="GeneratePublicKeyCredentialCreationOptions">
         <evaluate expression="PopulateWebAuthnRegistrationContext"/>
-        <evaluate expression="LookupRegisteredCredentials"/>      
+        <evaluate expression="LookupRegisteredCredentials"/>
+        <evaluate expression="LabelCredentialRecords"/>
         <evaluate expression="GenerateServerChallenge"/>
         <evaluate expression="AddUserName"/>
         <evaluate expression="AddUserId"/>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-management-config.xml b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-management-config.xml
index 92416d5..07605bd 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-management-config.xml
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-management-config.xml
@@ -8,6 +8,31 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
 
     default-init-method="initialize" default-destroy-method="destroy" default-lazy-init="true">
+    
+    <!-- 
+    The default credential labeller list
+    -->
+    <util:list id="shibboleth.authn.WebAuthn.CredentialLabellerList">
+    
+        <bean id="CapabilitiesCredentialLabeller" scope="prototype"
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AuthenticatorCapabilitiesLabeller"
+            c:_0-ref="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap"/>
+        
+        <bean id="PasskeyCredentialLabeller" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PasskeyCredentialLabeller"/>
+    </util:list>
+    
+    <!-- 
+    The default capabilities to labels map. The capabilities are first determined by the Inspectors.
+    -->
+    <util:map id="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap">
+        <entry>
+            <key>
+                <bean parent="shibboleth.Pair"
+                    p:first="2faOnly" p:second="true"/>
+           </key>
+           <value>SecondFactorOnly</value> 
+        </entry>
+    </util:map>
 
 
 </beans>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-registration-config.xml b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-registration-config.xml
index 9202e2c..ce177f3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-registration-config.xml
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn-registration-config.xml
@@ -28,5 +28,30 @@
             class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.SecondFactorOnlyAuthenticatorInspector"
             p:secondFactorOnlyAuthenticators="%{idp.authn.webauthn.registration.authenticator.inspector.secondFactorOnlyAuthenticators:null}"/>
     </util:list>
+    
+    <!-- 
+    The default credential labeller list
+    -->
+    <util:list id="shibboleth.authn.WebAuthn.CredentialLabellerList">
+    
+        <bean id="CapabilitiesCredentialLabeller" scope="prototype"
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AuthenticatorCapabilitiesLabeller"
+            c:_0-ref="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap"/>
+        
+        <bean id="PasskeyCredentialLabeller" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PasskeyCredentialLabeller"/>
+    </util:list>
+    
+    <!-- 
+    The default capabilities to labels map. The capabilities are first determined by the Inspectors.
+    -->
+    <util:map id="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap">
+        <entry>
+            <key>
+                <bean parent="shibboleth.Pair"
+                    p:first="2faOnly" p:second="true"/>
+           </key>
+           <value>SecondFactorOnly</value> 
+        </entry>
+    </util:map>
 
 </beans>
\ No newline at end of file
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 719675a..8034b7b 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
@@ -20,7 +20,7 @@ tr:hover {
 }
 
 .inline {
-    display:inline;
+    display: inline;
 }
 
 
@@ -89,5 +89,16 @@ tr:hover {
 
 .authenticator-logo {
     vertical-align: middle;
-    max-width:2em;
+    max-width: 2em;
+
+}
+
+.label {
+    color: white;
+    padding: 5px;
+    font-size: 75%;
+}
+
+.info {
+    background-color: #2196F3;
 }
\ 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 e84d3e1..7208933 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
@@ -23,8 +23,8 @@ idp.webauthn.register.credential.nickname = Credential nickname
 idp.webauthn.register.credential.remove.confirm = Are you sure
 idp.webauthn.register.table.header.keyName = Key Name
 idp.webauthn.register.table.header.authenticatorDescription = Authenticator
-idp.webauthn.register.table.header.transports = Transports
-idp.webauthn.register.table.header.passkey = Passkey?
+idp.webauthn.register.table.header.authenticatorIcon = Icon
+idp.webauthn.register.table.header.labels = Labels
 idp.webauthn.register.table.header.registrationTime = Registration Time
 idp.webauthn.register.table.header.action = Action
 idp.webauthn.register.table.unknownCredential = unknown
@@ -43,11 +43,10 @@ idp.webauthn.admin.header = Registered keys for
 idp.webauthn.admin.table.header.userName = User
 idp.webauthn.admin.table.header.keyName = Key Name
 idp.webauthn.admin.table.header.authenticatorDescription = Authenticator
-idp.webauthn.admin.table.header.transports = Transports
-idp.webauthn.admin.table.header.passkey = Passkey?
+idp.webauthn.admin.table.header.authenticatorIcon = Icon
 idp.webauthn.admin.table.header.registrationTime = Registration Time
 idp.webauthn.admin.table.header.action = Action
-idp.webauthn.admin.noKeys  = There are no registered keys
+idp.webauthn.admin.noKeys = There are no registered keys
 idp.webauthn.admin.unsupported = Your browser is not WebAuthn compatible
 idp.webauthn.admin.table.hasMetadata = Yes
 idp.webauthn.admin.table.noMetadata = No
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 2f56a90..0f81268 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
@@ -60,27 +60,31 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                             <tr>
                                <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.authenticatorIcon", "Icon")</th>
+                               <th>#springMessageText("idp.webauthn.admin.table.header.labels", "Labels")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.hasMetadata", "Metadata?")</th>
                                <th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
                             </tr>
                             #foreach($cred in $webAuthnManContext.foundCredentials)
                                 <tr>
-                                   <td>$encoder.encodeForHTML($cred.credentialRegistration.nickname)</td>
+                                   <td>$encoder.encodeForHTML($cred.credentialRecord.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>
+                                       <td>$encoder.encodeForHTML($cred.authenticatorDescription)</td>
                                    #else
                                         <td>#springMessageText("idp.webauthn.admin.table.unknownCredential", "unknown")</td>
                                    #end
-                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatTransports($cred.credentialRegistration.transports))</td>
-                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatDiscoverable($cred.credentialRegistration.isDiscoverable()))</td>
-                                   <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.credentialRegistration.registrationTime))</td>
+                                   #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.formatInstant($cred.credentialRecord.registrationTime))</td>
                                    <td>
                                         #if ($webAuthnEncoder.isAuthenticatorMetadataAttached($cred))
                                            #springMessageText("idp.webauthn.admin.table.hasMetadata", "Yes") 
@@ -91,7 +95,7 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                    <td>
                                       <form id="delete_key_form" action="$flowExecutionUrl" method="post">
                                          #parse("csrf/csrf.vm")
-                                         <input type="hidden" name="credentialId" value="$cred.credentialRegistration.credentialIdBase64Url"/>
+                                         <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.admin.credential.remove", "Remove")</button>
                                       </form>                                      
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 f5bb507..8047f54 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
@@ -129,29 +129,33 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
                                <th>#springMessageText("idp.webauthn.register.table.header.keyName", "Key Name")</th>                               
                                <th>#springMessageText("idp.webauthn.register.table.header.authenticatorDescription", "Authenticator")</th>
                                <th>#springMessageText("idp.webauthn.register.table.header.authenticatorIcon", "Icon")</th> 
-                               <th>#springMessageText("idp.webauthn.register.table.header.passkey", "Passkey?")</th>
+                               <th>#springMessageText("idp.webauthn.register.table.header.labels", "Labels")</th>
                                <th>#springMessageText("idp.webauthn.register.table.header.registrationTime", "Registration Time")</th>
                                <th>#springMessageText("idp.webauthn.register.table.header.action", "Action")</th>
                             </tr>
                             #foreach($cred in $webauthnRegContext.existingCredentials)
                             <tr>
-                               <td>$encoder.encodeForHTML($cred.credentialRegistration.nickname)</td>                              
+                               <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.credentialRegistration.nickname)-authenticator-icon"/></td> 
+                                    <td> <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon)" alt="$encoder.encodeForHTML($cred.credentialRecord.nickname)-authenticator-icon"/></td> 
                                #else
                                     <td></td>
                                #end
-                               <td>$encoder.encodeForHTML($webAuthnEncoder.formatDiscoverable($cred.credentialRegistration.isDiscoverable()))</td>
-                               <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.credentialRegistration.registrationTime))</td>
+                               <td>
+                                #foreach($label in $cred.labels)
+                                    <span class="label info">$encoder.encodeForHTML($label)</span>
+                                #end  
+                               </td>                             
+                               <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($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.credentialRegistration.credentialIdBase64Url"/>
+                                     <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>
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptionsTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptionsTest.java
index 532b764..3242272 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptionsTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptionsTest.java
@@ -96,9 +96,9 @@ public class CreatePublicKeyCredentialCreationOptionsTest extends AbstractWebAut
     public void testCreateOptions_WithExcludeCredentials() throws Exception {
         context.setServerChallenge(generateRandomBytes(17));
         action.initialize();
-
-        context.setExistingCredentials(CollectionSupport.setOf(EnhancedCredentialRecord.builder()
-                .withCredentialRecord(createCredentialRegistration()).build()));
+        
+        context.setExistingCredentials(CollectionSupport.setOf(
+                new EnhancedCredentialRecord(createCredentialRegistration())));
         
         final Event result = action.execute(src);
         assertNull(result);
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsTest.java
index bfabb3d..030947a 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsTest.java
@@ -128,11 +128,22 @@ public class LookupRegisteredCredentialsTest extends AbstractWebAuthnTest {
     
     @SuppressWarnings("null")
     @Test
-    public void testLookup_NoUsername() throws ComponentInitializationException {        
+    public void testLookup_NoUsernameRequired() throws ComponentInitializationException {  
+        action.setUsernameRequired(true);
         action.initialize();        
         final Event event = action.execute(src);
         assertNotNull(event);
         assertEquals(event.getId(),  EventIds.INVALID_PROFILE_CTX);
     }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testLookup_NoUsernameNotRequired() throws ComponentInitializationException {       
+        action.setUsernameRequired(false);
+        action.initialize();        
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertEquals(context.getExistingCredentials().size(), 0);
+    }
 
 }
diff --git a/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-registration.xml b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-registration.xml
index d48f123..0dd2022 100644
--- a/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-registration.xml
+++ b/webauthn-impl/src/test/resources/net/shibboleth/idp/plugin/authn/webauthn/test-beans-registration.xml
@@ -42,6 +42,25 @@
             class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.SecondFactorOnlyAuthenticatorInspector"
             p:secondFactorOnlyAuthenticators="%{idp.authn.webauthn.registration.authenticator.inspector.secondFactorOnlyAuthenticators:null}"/>
     </util:list>
+    
+    <util:list id="shibboleth.authn.WebAuthn.CredentialLabellerList">
+    
+        <bean id="CapabilitiesCredentialLabeller" scope="prototype"
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AuthenticatorCapabilitiesLabeller"
+            c:_0-ref="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap"/>
+        
+        <bean id="PasskeyCredentialLabeller" class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PasskeyCredentialLabeller"/>
+    </util:list>
+    
+    <util:map id="shibboleth.authn.WebAuthn.DefaultCapabilitiesToLabelsMap">
+        <entry>
+            <key>
+                <bean parent="shibboleth.Pair"
+                    p:first="2faOnly" p:second="true"/>
+           </key>
+           <value>SecondFactorOnly</value> 
+        </entry>
+    </util:map>
    
 
 </beans>

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


More information about the commits mailing list