[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-11 - Pull identity information from the AttributeResolver during registration

Phil Smart philip.smart at jisc.ac.uk
Wed Apr 24 08:45:11 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=f9523aa9a5e56e92111b8539dcbd58193353fd53

The following commit(s) were added to refs/heads/main by this push:
     new f9523aa  JWEBAUTHN-11 - Pull identity information from the AttributeResolver during registration
f9523aa is described below

commit f9523aa9a5e56e92111b8539dcbd58193353fd53
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Apr 24 09:45:08 2024 +0100

    JWEBAUTHN-11 - Pull identity information from the AttributeResolver
    during registration
    
     - Both user.id and user.displayName can be generated or looked up.
     - Attributes from the attribute context are supported if configured
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-11
---
 webauthn-api/pom.xml                               |  11 ++
 .../admin/CredentialCreationOptionsParameters.java |  49 +++++++-
 .../webauthn/context/BaseWebAuthnContext.java      |  14 +--
 .../context/WebAuthnRegistrationContext.java       |  29 +++++
 ...stractAttributeContextUserIdentityStrategy.java | 127 ++++++++++++++++++++
 .../AttributeContextByteArrayLookupStrategy.java   |  71 +++++++++++
 .../AttributeContextStringLookupStrategy.java      |  69 +++++++++++
 ...ttributeContextByteArrayLookupStrategyTest.java | 130 +++++++++++++++++++++
 .../authn/webauthn/admin/impl/AddDisplayName.java  | 116 ++++++++++++++++++
 .../authn/webauthn/admin/impl/AddUserId.java       |  72 +++++-------
 .../CreatePublicKeyCredentialCreationOptions.java  |  12 +-
 .../webauthn/admin/impl/RandomUserIdGenerator.java |  49 ++++++++
 .../impl/YubicoWebAuthnAuthenticationClient.java   |   2 +-
 .../webauthn-registration-beans.xml                |  21 +++-
 .../webauthn-registration-flow.xml                 |  17 ++-
 .../authn/webauthn/conf/authn/webauthn.properties  |  10 ++
 .../idp/plugin/authn/webauthn/module.properties    |   3 +
 .../authn/webauthn/views/webauthn-register-end.vm  |  48 ++++++++
 .../authn/webauthn/admin/impl/AddUserIdTest.java   |  27 +++--
 .../admin/impl/DeletePublicKeyCredentialTest.java  |   2 +-
 .../YubicoWebauthnAuthenticationClientTest.java    |   2 +-
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  |   1 +
 .../impl/LookupRegisteredCredentialsTest.java      |   1 +
 .../impl/ValidateWebAuthnAssertionTest.java        |   1 +
 .../impl/CredentialRegistrationSerializerTest.java |   1 -
 ...IdPStorageServiceCredentialRespositoryTest.java |   1 -
 .../{ => storage}/impl/MockAuthenticator.java      |   2 +-
 27 files changed, 812 insertions(+), 76 deletions(-)

diff --git a/webauthn-api/pom.xml b/webauthn-api/pom.xml
index 136a2c0..9fa1ac1 100644
--- a/webauthn-api/pom.xml
+++ b/webauthn-api/pom.xml
@@ -95,6 +95,17 @@
             <artifactId>shib-support</artifactId>
             <scope>provided</scope>
         </dependency>
+        <!-- Test dependencies -->        
+         <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-profile-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
index 20b65af..2a25f40 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
@@ -47,6 +47,12 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
      */
     @Nonnull @NotEmpty final String username;
     
+    /**
+     * The user.displayName supplied to the authenticator during registration. A human-palatable name for the user 
+     * account, intended only for display
+     */
+    @Nonnull @NotEmpty final String displayName;
+    
     /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
     @Nullable final AuthenticatorAttachment authenticatorAttachment;
     
@@ -81,6 +87,7 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
         this.attestationConveyancePreference = Constraint.isNotNull(builder.attestationConveyancePreference, 
                 "AttestationConveyancePreference can not be null");
         this.enableCredProperties = builder.enableCredProperties;
+        this.displayName = builder.displayName;
     }
 
     /**
@@ -132,6 +139,17 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
         return userId;
     }
     
+
+
+    /**
+     * Get the user.displayName.
+     * 
+     * @return the user's display name.
+     */
+    @Nonnull public String getDisplayName() {
+        return displayName;
+    }
+    
     /**
      * Is the credential properties extension enabled?
      * 
@@ -207,7 +225,19 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
          * @param username the username
          * @return the next stage
          */
-        public IResidentKeyRequirementStage withUsername(@Nonnull @NotEmpty final String username);
+        public IDisplayNameStage withUsername(@Nonnull @NotEmpty final String username);
+    }
+    
+    /** Stage interface.*/
+    public interface IDisplayNameStage {
+        /**
+         * The user.displayName supplied to the authenticator during registration. A human-palatable name for the user 
+         * account, intended only for display
+         * 
+         * @param name the display name
+         * @return the next stage
+         */
+        public IResidentKeyRequirementStage withDisplayName(@Nonnull @NotEmpty final String name);
     }
 
 
@@ -280,7 +310,7 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
 
     /** Builder class.*/
     public static final class Builder implements IUserVerificationRequirementStage, IChallengeStage,
-            IExcludeCredentialsStage, IUsernameStage, IResidentKeyRequirementStage, IUserHandleStage, 
+            IExcludeCredentialsStage, IUsernameStage, IDisplayNameStage, IResidentKeyRequirementStage, IUserHandleStage, 
             IAttestationConveyancePreferenceStage, IBuildStage {
         
         /** Does the authentication/registration require user verification.*/
@@ -307,6 +337,12 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
         /** The Attestation conveyance preference.*/
         private AttestationConveyancePreference attestationConveyancePreference;
         
+        /**
+         * The user.displayName supplied to the authenticator during registration. A human-palatable name for the user 
+         * account, intended only for display
+         */
+        private String displayName;
+        
         /** 
          * Enable the credential properties extenions. For example, to report discoverable credentials i.e. 
          * 'passkeys'.
@@ -339,10 +375,16 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
         }
 
         @Override
-        public IResidentKeyRequirementStage withUsername(@Nonnull final String username) {
+        public IDisplayNameStage withUsername(@Nonnull final String username) {
             this.username = username;
             return this;
         }
+        
+        @Override
+        public IResidentKeyRequirementStage withDisplayName(@Nonnull final String name) {
+            displayName = name;
+            return this;
+        }
 
         @Override
         public IUserHandleStage withResidentKeyRequirement(
@@ -386,5 +428,6 @@ public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
 
 
     }
+
    
 }
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 f724601..3802a7b 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
@@ -37,9 +37,8 @@ import net.shibboleth.shared.logic.Constraint;
 public class BaseWebAuthnContext extends BaseContext {
     
     /** 
-     * The username of the user that is the subject of this authentication. In the authentication ceremony, if 
-     * {@code null} we can not determine the userHandle (and hence public key) to use, and the flow will require a 
-     * discoverable credential.
+     * The user.name of the user. If {@code null} in the authentication ceremony if we can not determine the 
+     * userHandle (and hence public key) to use, and the flow will require a discoverable credential.
      */
     @Nullable private String username;    
     
@@ -52,9 +51,9 @@ public class BaseWebAuthnContext extends BaseContext {
     /** The challenge sent to the authenticator in both registration and authentication ceremonies.*/
     @Nullable private byte[] serverChallenge;  
     
-    /** The user.id supplied to the authenticator as a userHandle during registration. Generated by the IdP.*/
+    /** The user.id supplied to the authenticator during registration. Generated by the IdP.*/
     @Nullable private byte[] userId;    
-    
+        
     /** Does the authentication/registration require user verification.*/
     @Nullable private UserVerificationRequirement userVerificationRequirement;
     
@@ -68,9 +67,9 @@ public class BaseWebAuthnContext extends BaseContext {
     }
     
     /**
-     * Gets the username.
+     * Gets the user.name.
      * 
-     * @return the username
+     * @return the user.name
      */
     @Nullable public String getUsername() {
         return username;
@@ -175,7 +174,6 @@ public class BaseWebAuthnContext extends BaseContext {
         return userId;
     }
     
-    
     /**
      * Set the user verification requirement. 
      * 
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 5051c25..e19be11 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
@@ -13,6 +13,8 @@ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
 import com.yubico.webauthn.data.ResidentKeyRequirement;
 
 import net.shibboleth.idp.plugin.authn.webauthn.authn.RegistrationResult;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
 
 
 /** 
@@ -53,6 +55,12 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
     /** The attestation preference.*/
     @Nullable private AttestationConveyancePreference attestationConveyancePreference;
     
+    /** 
+     * The user.displayName supplied to the authenticator during registration. A human-palatable name for the user 
+     * account, intended only for display.
+     */
+    @Nullable private String displayName;
+    
     /**
      * Set the AuthenticatorAttachment requirement. {@code null} would represent either possibility.
      * 
@@ -232,4 +240,25 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
     @Nullable public AttestationConveyancePreference getAttestationConveyancePreference() {
         return attestationConveyancePreference;
     }
+    
+    
+    /**
+     * Set the human-palatable name for the user account.
+     * 
+     * @param name The display name to set.
+     */
+    @Nonnull public BaseWebAuthnContext setDisplayName(@Nonnull @NotEmpty final String name) {
+        displayName = Constraint.isNotEmpty(name, "The Display Name can not be null");
+        return this;
+    }
+    
+    /**
+     * Get the human-palatable name for the user account.
+     * 
+     * @return the displayName.
+     */
+    @Nullable public String getDisplayName() {
+        return displayName;
+    }
+    
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AbstractAttributeContextUserIdentityStrategy.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AbstractAttributeContextUserIdentityStrategy.java
new file mode 100644
index 0000000..545752a
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AbstractAttributeContextUserIdentityStrategy.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A base class for functions that retrieve a single attribute value from the 
+ * {@link AttributeContext attribute context}.
+ * 
+ * @param <T> the type of attribute value to return
+ */
+public abstract class AbstractAttributeContextUserIdentityStrategy<T> extends AbstractIdentifiableInitializableComponent 
+        implements Function<ProfileRequestContext, T> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAttributeContextUserIdentityStrategy.class);
+            
+    /**
+     * Strategy used to locate the {@link AttributeContext} associated with a given {@link ProfileRequestContext}.
+     */
+    @Nonnull private Function<ProfileRequestContext,AttributeContext> attributeContextLookupStrategy;
+    
+    /** The attribute Id to extract the value from.*/
+    @Nullable @NotEmpty private String attributeId;
+    
+    /** Constructor.*/
+    protected AbstractAttributeContextUserIdentityStrategy() {
+        final Function<ProfileRequestContext,AttributeContext> acls =
+                new ChildContextLookup<>(AttributeContext.class).compose(
+                        new ChildContextLookup<>(RelyingPartyContext.class));
+        assert acls !=null;
+        attributeContextLookupStrategy = acls;
+        
+    }
+    
+    /**
+     * Set the attribute Id to extract the value from.
+     *  
+     * @param id the attributeId.
+     */
+    public void setAttributeId(@Nullable final String id) {
+        checkSetterPreconditions();
+        attributeId = id;
+    }
+    
+    /**
+     * Get the attributeId.
+     * 
+     * @return the attributeId.
+     */
+    @Nullable @NotEmpty protected String getAttributeId() {
+        return attributeId;
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link AttributeContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy strategy used to locate the {@link AttributeContext} associated with a given
+     *            {@link ProfileRequestContext}
+     */
+    public void setAttributeContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, AttributeContext> strategy) {
+        checkSetterPreconditions();
+        attributeContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AttributeContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Get the attribute with the given attribute Id from the AttributeContext (if it exists).
+     * 
+     * @param profileRequestContext the profile request context to locate the attribute context and the attribute from
+     * 
+     * @return the IdPAttribute, or <code>null</code> if not found.
+     */
+    @Nullable protected IdPAttribute getAttribute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        checkComponentActive();
+        
+        final AttributeContext attrContext = attributeContextLookupStrategy.apply(profileRequestContext);
+
+        if (attrContext != null && attributeId != null) {
+            for (final IdPAttribute attribute : attrContext.getIdPAttributes().values()) {
+                
+                if (attribute != null && !attribute.getValues().isEmpty() && 
+                        attribute.getId().equals(attributeId)) {                    
+                    return attribute;                 
+                }
+            }
+        }
+        log.trace("{}: Attribute '{}' could not be found", getId(), attributeId);
+        return null;
+        
+    }
+   
+
+    
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategy.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategy.java
new file mode 100644
index 0000000..8ac2971
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategy.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context.navigate;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link AbstractAttributeContextUserIdentityStrategy} that pulls out an {@link StringAttributeValue} from the 
+ * attribute context and represents it as a UTF-8 encoded byte array. Will return an empty array if the attribute can 
+ * not be found, or if there is more than one attribute value.
+ */
+public class AttributeContextByteArrayLookupStrategy extends AbstractAttributeContextUserIdentityStrategy<byte[]> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AttributeContextByteArrayLookupStrategy.class);
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public byte[] apply(final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+        
+        if (profileRequestContext == null) {
+            return new byte[0];
+        }
+        
+        final IdPAttribute attribute = getAttribute(profileRequestContext);
+        if (attribute == null) {
+            return new byte[0];
+        }       
+                    
+        final List<IdPAttributeValue> values = attribute.getValues();
+        if (values.size() != 1) {
+            log.warn("{}: Attribute '{}' has more than one value", getId(), getAttributeId());
+            return new byte[0];
+        }
+        final IdPAttributeValue value = values.get(0);
+        if (value instanceof final StringAttributeValue strValue) {
+            log.debug("{}: Found attribute '{}' with value '{}'", getId(), attribute.getId(), strValue.getValue());
+            final String valueAsString = strValue.getValue();
+            final byte[] valueAsBytes = valueAsString.getBytes(StandardCharsets.UTF_8);
+            return valueAsBytes != null ? valueAsBytes : new byte[0];
+        }                 
+
+        log.warn("{}: Attribute '{}' could not be found", getId(), getAttributeId());
+        return new byte[0];
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextStringLookupStrategy.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextStringLookupStrategy.java
new file mode 100644
index 0000000..39dcae0
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextStringLookupStrategy.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.context.navigate;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.IdPAttributeValue;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link AbstractAttributeContextUserIdentityStrategy} that pulls out an {@link StringAttributeValue} from the 
+ * attribute context. Will return {@code null} if the attribute can not be found, or if there is more than one 
+ * attribute value.
+ */
+public class AttributeContextStringLookupStrategy extends AbstractAttributeContextUserIdentityStrategy<String> {
+        
+        /** Class logger. */
+        @Nonnull private final Logger log = LoggerFactory.getLogger(AttributeContextStringLookupStrategy.class);
+        
+        /** {@inheritDoc} */
+        @Override
+        @Nullable public String apply(final ProfileRequestContext profileRequestContext) {
+            checkComponentActive();
+            
+            if (profileRequestContext == null) {
+                return null;
+            }
+            
+            final IdPAttribute attribute = getAttribute(profileRequestContext);
+            if (attribute == null) {
+                return null;
+            }       
+                        
+            final List<IdPAttributeValue> values = attribute.getValues();
+            if (values.size() != 1) {
+                log.warn("{}: Attribute '{}' has more than one value", getId(), getAttributeId());
+                return null;
+            }
+            final IdPAttributeValue value = values.get(0);
+            if (value instanceof final StringAttributeValue strValue) {
+                log.debug("{}: Found attribute '{}' with value '{}'", getId(), attribute.getId(), strValue.getValue());
+                return strValue.getValue();
+            }                 
+
+            log.warn("{}: Attribute '{}' could not be found", getId(), getAttributeId());
+            return null;
+        }
+
+}
diff --git a/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategyTest.java b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategyTest.java
new file mode 100644
index 0000000..9248e60
--- /dev/null
+++ b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/AttributeContextByteArrayLookupStrategyTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context.navigate;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import java.nio.charset.StandardCharsets;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.attribute.IdPAttribute;
+import net.shibboleth.idp.attribute.StringAttributeValue;
+import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link AttributeContextByteArrayLookupStrategy}.
+ */
+public class AttributeContextByteArrayLookupStrategyTest {
+    
+    private AttributeContextByteArrayLookupStrategy strategy;
+    
+    /** The profile request context to use.*/
+    protected ProfileRequestContext prc;
+    
+    /** The request context to use.*/
+    protected RequestContext src;
+    
+    @BeforeMethod
+    public void setup() throws Exception {
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);  
+        strategy = new AttributeContextByteArrayLookupStrategy();
+        strategy.setId("Test AttributeContextByteArrayLookupStrategy");
+        strategy.setAttributeId("attributeOne");
+    }
+    
+    @Test
+    public void testAttributeLookup() throws ComponentInitializationException {
+        strategy.initialize();
+        
+        final AttributeContext attrCtx = prc.ensureSubcontext(RelyingPartyContext.class)
+                .ensureSubcontext(AttributeContext.class);
+        final var idpAttribute = new IdPAttribute("attributeOne");
+        idpAttribute.setValues(CollectionSupport.listOf(new StringAttributeValue("value")));
+
+        attrCtx.setIdPAttributes(CollectionSupport.listOf(idpAttribute));
+        
+        final byte[] attributeValue = strategy.apply(prc);
+        assertEquals(new String(attributeValue, StandardCharsets.UTF_8), "value");
+        
+    }
+    
+    @Test
+    public void testAttributeLookup_Attribute_NotFound() throws ComponentInitializationException {
+        strategy.initialize();
+        
+        final AttributeContext attrCtx = prc.ensureSubcontext(RelyingPartyContext.class)
+                .ensureSubcontext(AttributeContext.class);
+        final var idpAttribute = new IdPAttribute("attributeNotFound");
+        idpAttribute.setValues(CollectionSupport.listOf(new StringAttributeValue("value")));
+
+        attrCtx.setIdPAttributes(CollectionSupport.listOf(idpAttribute));
+        
+        final byte[] attributeValue = strategy.apply(prc);
+        assertNotNull(attributeValue);
+        assertEquals(attributeValue.length, 0);
+        
+    }
+    
+    @Test
+    public void testAttributeLookup_AttributeHasTooManyValues() throws ComponentInitializationException {
+        strategy.initialize();
+        
+        final AttributeContext attrCtx = prc.ensureSubcontext(RelyingPartyContext.class)
+                .ensureSubcontext(AttributeContext.class);
+        final var idpAttribute = new IdPAttribute("attributeOne");
+        idpAttribute.setValues(CollectionSupport.listOf(new StringAttributeValue("value"), 
+                new StringAttributeValue("valueTwo")));
+
+        attrCtx.setIdPAttributes(CollectionSupport.listOf(idpAttribute));
+        
+        final byte[] attributeValue = strategy.apply(prc);
+        assertNotNull(attributeValue);
+        assertEquals(attributeValue.length, 0);
+        
+    }
+    
+    @Test
+    public void testAttributeLookup_NoAttributeContext() throws ComponentInitializationException {
+        strategy.initialize();
+        
+        final AttributeContext attrCtx = prc.ensureSubcontext(RelyingPartyContext.class)
+                .ensureSubcontext(AttributeContext.class);
+        
+        attrCtx.removeFromParent();
+        
+        final var idpAttribute = new IdPAttribute("attributeOne");
+        idpAttribute.setValues(CollectionSupport.listOf(new StringAttributeValue("value"), 
+                new StringAttributeValue("valueTwo")));
+
+        attrCtx.setIdPAttributes(CollectionSupport.listOf(idpAttribute));
+        
+        final byte[] attributeValue = strategy.apply(prc);
+        assertNotNull(attributeValue);
+        assertEquals(attributeValue.length, 0);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddDisplayName.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddDisplayName.java
new file mode 100644
index 0000000..17a7be7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddDisplayName.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action to generate or lookup a user.displayName. The display name is for display purposes only. 
+ * 
+ * @event {@link WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post a displayName is added to the registration context
+ */
+public class AddDisplayName extends AbstractWebAuthnRegistrationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddDisplayName.class);
+   
+    /** Strategy used to lookup the user.displayName. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> displayNameLookupStrategy;
+    
+    /** The stashed username.*/
+    @NonnullBeforeExec private String username;
+        
+    /**
+     * Set the strategy used to generate the user.displayName.
+     * 
+     * @param strategy the strategy
+     */
+    public void setDisplayNameLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        displayNameLookupStrategy =
+                Constraint.isNotNull(strategy, "Challenge Generator cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (displayNameLookupStrategy == null) {
+            throw new ComponentInitializationException("DisplayName generation strategy can not be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+        
+        if (!super.doPreExecute(profileRequestContext, context)) {
+            return false;
+        }
+       
+        username = context.getUsername();
+        if (username == null) {
+            log.error("{} Username not available in registration context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+             
+                    
+        // else we build a user.display from the lookup function
+        final String displayName = displayNameLookupStrategy.apply(profileRequestContext);
+        if (displayName == null) {
+            log.trace("{} DisplayName was null for user '{}'", getLogPrefix(), username);
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            return;
+        }
+        if (displayName.getBytes(StandardCharsets.UTF_8).length > 64) {
+            log.warn("{} DisplayName exceeds 64 bytes and might get truncated by the authenticator", getLogPrefix());
+        }
+        log.trace("{} Populating DisplayName '{}'",getLogPrefix(),displayName);
+        context.setDisplayName(displayName); 
+             
+    }        
+    
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
index 7886376..1ed0a45 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
@@ -17,13 +17,10 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
 import java.util.Optional;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -38,13 +35,14 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * An action to generate or lookup a user.id used as a userHandle. This is used by the IdP to map a public key 
- * credential to a users session map of public keys, and by the Authenticator to map the IdP's ID (RelyingParty ID)
- * and the userHandle to a public key credential source (which contains the private key).
+ * An action to generate or lookup a user.id (stored by an authenticator as the userHandle). This is used by the IdP to 
+ * map a public key credential to a users session map of public keys, and by the authenticator to map the IdP's ID 
+ * (RelyingParty ID) and the userHandle to a public key credential source (which contains the private key).
  * 
- * <p>Note, the same user should use the same UserHandle.</p>
+ * <p>The user.id could contain some form of state if required, but must not contain retrievable PII.</p>
+ * 
+ * <p>The same user should have the same user.id.</p>
  * 
- * TODO: Support a generated unique to username approach, as well as one from the resolver.
  * 
  * @event {@link WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
  * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
@@ -55,7 +53,7 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(AddUserId.class);
    
-    /** Strategy used to generate the UserHandle. */
+    /** Strategy used to generate the user.id. */
     @Nonnull private Function<ProfileRequestContext,byte[]> userIdGeneratorStrategy;
     
     /** The stashed username.*/
@@ -63,7 +61,7 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
     
     /** Constructor. */
     public AddUserId() {
-        userIdGeneratorStrategy = new DefaultUserIdGenerator();
+        userIdGeneratorStrategy = new RandomUserIdGenerator();
     }
     
     /**
@@ -107,44 +105,26 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
         if (existingUserHandle.isPresent()) {
             final byte[] handleAsBytes = existingUserHandle.get().getBytes();
             assert handleAsBytes != null;
+            log.trace("{} Found user.id '{}'",getLogPrefix(),handleAsBytes);
             context.setUserId(handleAsBytes); 
-        } else {            
-            final byte[] userId = userIdGeneratorStrategy.apply(profileRequestContext);
-            if (userId == null) {
-                log.trace("{} Generated UserID was null", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
-                return;
-            }
-            log.trace("{} Generated UserID '{}'",getLogPrefix(),userId);
-            context.setUserId(userId);   
-        }
+            return;
+        }            
+                    
+        // else we build a user.id from the generator function
+        final byte[] userId = userIdGeneratorStrategy.apply(profileRequestContext);
+        if (userId == null || userId.length == 0) {
+            log.trace("{} Generated user.id was empty or null", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            return;
+        }    
+        if (userId.length > 64) {
+            log.warn("{}: User.id is larger than 64 bytes", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            return;
+        }        
+        log.trace("{} Generated user.id '{}'",getLogPrefix(),userId);
+        context.setUserId(userId);       
              
     }    
     
-    /**
-     * Default user.id generator that generates a 64 byte randomized user.id (must be at least 32 bytes long). 
-     * Returns {@code null} iff one can not be generated.
-     * 
-     * <p>This could contain some form of state if required, but must not contain retrievable PII.</p>
-     */
-    private static final class DefaultUserIdGenerator implements Function<ProfileRequestContext, byte[]>{
-        
-        /** Class logger. */
-        @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultUserIdGenerator.class);
-
-        /** {@inheritDoc} */
-        @Override
-        @Nullable public byte[] apply(final ProfileRequestContext input) {           
-            try {
-                final byte[] bytes = new byte[64];
-                SecureRandom.getInstanceStrong().nextBytes(bytes);
-                return bytes;
-            } catch (final NoSuchAlgorithmException e) {
-                log.error("Unable to generate UserHandle", e);
-                return null;
-            }            
-        }      
-    }
-    
-    
 }
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 35a9666..01b5edf 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
@@ -78,13 +78,19 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
         }
         final String username = context.getUsername();
         if (username == null) {
-            log.error("{} Username is null",getLogPrefix());
+            log.error("{} user.name is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            return;
+        }
+        final String displayName = context.getDisplayName();
+        if (displayName == null) {
+            log.error("{} user.displayName is null",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
             return;
         }
         final byte[] userId = context.getUserId();
         if (userId == null) {
-            log.error("{} UserID is null",getLogPrefix());
+            log.error("{} user.id is null",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
             return;
         }
@@ -105,11 +111,13 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
             
             assert null != existingCredentialDescriptors;
 
+            //TODO helper?
             final CredentialCreationOptionsParameters creationOptions = CredentialCreationOptionsParameters.builder()
                     .withUserVerificationRequirement(uvRequirement)
                     .withChallenge(challenge)
                     .withExcludeCredentials(existingCredentialDescriptors)
                     .withUsername(username)
+                    .withDisplayName(displayName)
                     .withResidentKeyRequirement(residentKeyRequirement)
                     .withUserId(userId)
                     .withAttestationConveyancePreference(attestationPreference)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RandomUserIdGenerator.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RandomUserIdGenerator.java
new file mode 100644
index 0000000..18d9e05
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RandomUserIdGenerator.java
@@ -0,0 +1,49 @@
+/*
+ * 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.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A user.id generator that generates a 64 byte randomized user.id. Returns {@code null} iff one can not be generated.
+ */
+public final class RandomUserIdGenerator implements Function<ProfileRequestContext, byte[]>{
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RandomUserIdGenerator.class);
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable public byte[] apply(final ProfileRequestContext input) {           
+        try {
+            final byte[] bytes = new byte[64];
+            SecureRandom.getInstanceStrong().nextBytes(bytes);
+            return bytes;
+        } catch (final NoSuchAlgorithmException e) {
+            log.error("Unable to generate user.id", e);
+            return null;
+        }            
+    }      
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
index 84e826b..38b0fec 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
@@ -119,7 +119,7 @@ public class YubicoWebAuthnAuthenticationClient implements WebAuthnAuthenticatio
         
         final UserIdentity identity = 
                 UserIdentity.builder().name(creationOptions.getUsername())
-                    .displayName(creationOptions.getUsername())
+                    .displayName(creationOptions.getDisplayName())
                     .id(new ByteArray(creationOptions.getUserId()))
                     .build();
         
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 716933c..aa5f3d4 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
@@ -72,7 +72,26 @@
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
 
     <bean id="AddUserId" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddUserId" />
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddUserId"
+        p:userIdGeneratorStrategy="#{getObject('%{idp.authn.webauthn.registration.userid.strategy:shibboleth.authn.webauthn.RandomUserIdGenerator}')}"/>
+    
+    <bean id="AddDisplayName" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddDisplayName"
+        p:displayNameLookupStrategy="#{getObject('%{idp.authn.webauthn.registration.displayname.strategy:shibboleth.authn.webauthn.SubjectDisplayNameLookupStrategy}')}"/>
+    
+    <bean id="shibboleth.authn.webauthn.SubjectDisplayNameLookupStrategy" lazy-init="true"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromSubjectContext"/>
+        
+        <bean id="shibboleth.authn.webauthn.AttributeContextDisplayNameLookupStrategy" lazy-init="true"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.AttributeContextStringLookupStrategy"
+        p:attributeId="%{idp.authn.webauthn.registration.displayname.attributeId:#{null}}"/>
+    
+    <bean id="shibboleth.authn.webauthn.AttributeContextUserIdLookupStrategy" scope="singleton" lazy-init="true"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.AttributeContextByteArrayLookupStrategy"
+        p:attributeId="%{idp.authn.webauthn.registration.userid.attributeId:#{null}}"/>
+        
+    <bean id="shibboleth.authn.webauthn.RandomUserIdGenerator" scope="singleton" lazy-init="true"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RandomUserIdGenerator"/>
 
     <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"/>
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 9131091..fe13bc0 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
@@ -53,6 +53,7 @@
         <evaluate expression="LookupRegisteredCredentials"/>      
         <evaluate expression="GenerateServerChallenge"/>
         <evaluate expression="AddUserId"/>
+        <evaluate expression="AddDisplayName"/>
         <evaluate expression="AddResidentKeyRequirement"/>
         <evaluate expression="AddAuthenticatorAttachmentRequirement"/>
          <evaluate expression="AddAttestationConveyancePreference"/>
@@ -64,7 +65,6 @@
     
     <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-register">
         <on-render>
-
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
@@ -109,7 +109,20 @@
         </transition>
     </action-state> 
 
-    <end-state id="RegistrationComplete"/>
+
+    <end-state id="RegistrationComplete" view="webauthn/webauthn-register-end">
+         <on-entry>
+            <evaluate expression="environment" result="requestScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="requestScope.webauthnRegContext" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />   
+            <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="requestScope.encoder" />
+            <evaluate expression="T(net.shibboleth.idp.plugin.authn.webauthn.impl.WebAuthnEncoder)" result="requestScope.webAuthnEncoder"/>
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="requestScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="requestScope.response" />
+        </on-entry>
+    </end-state>
     
     <bean-import resource="webauthn-registration-beans.xml" />
     <bean-import resource="../../authn/WebAuthn/webauthn-abstract-beans.xml" />
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
index 1d010fb..13ba04e 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
@@ -44,6 +44,16 @@ idp.authn.webauthn.supportedPrincipals = \
 # Should user.id and display name be resolve from the attribute resolver? else the user.id will be generated by the plugin
 #idp.authn.webauthn.admin.registration.resolveIdentityAttributes = true;
 
+# The bean name of the user.id generator or lookup strategy
+#idp.authn.webauthn.registration.userid.strategy = shibboleth.authn.webauthn.RandomUserIdGenerator
+# If using the attribute context lookup strategy, which attribute should the user.id be taken from
+#idp.authn.webauthn.registration.userid.attributeId = WebAuthnUserID
+
+# The bean name of the user.displayName lookup strategy
+#idp.authn.webauthn.registration.displayname.strategy = shibboleth.authn.webauthn.SubjectDisplayNameLookupStrategy
+# If using the attribute context lookup strategy, which attribute should the user.displayName be taken from
+#idp.authn.webauthn.registration.displayname.attributeId = mail
+
 # Enable FIDO Alliance metadata? If you want to use the metadata service, you *must* download and set the trust root and the legal blob file headers
 #idp.authn.webauthn.metadata.enabled = false
 #idp.authn.webauthn.metadata.trustRootFile =
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
index 93b0eea..6e944e2 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
@@ -40,4 +40,7 @@ idp.authn.WebAuthn.8.dest = edit-webapp/js/webauthn/webauthn-json.browser-ponyfi
 idp.authn.WebAuthn.9.src = /net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-support.js
 idp.authn.WebAuthn.9.dest = edit-webapp/js/webauthn/webauthn-support.js
 
+idp.authn.WebAuthn.10.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
+idp.authn.WebAuthn.10.dest = views/webauthn/webauthn-register-end.vm
+
 
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
new file mode 100644
index 0000000..937d077
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
@@ -0,0 +1,48 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## webAuthnEncoder - WebAuthnEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>#springMessageText("idp.title", "Web Login Service")</title>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+        <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+    </head>
+    <body>
+        <main class="main">
+        <header>
+            <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+        </header>
+        <section>           
+             #springMessageText("idp.webauthn.authn.register.ended", "Your registration session has ended")                    
+        </section>
+        <footer>
+            <div class="container container-footer">
+                <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+            </div>
+        </footer>
+        </div>    
+    </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
index 8fb4ad3..93e43a8 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
@@ -39,12 +39,11 @@ import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
-import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.ConstraintViolationException;
 
 /**
  * Tests for {@link AddUserId}.
@@ -78,7 +77,13 @@ public class AddUserIdTest extends AbstractWebAuthnTest {
     
     @Test
     public void testNewUserId_CustomGenerator() throws ComponentInitializationException {
-        addAction.setUserIdGeneratorStrategy(input -> new byte[] {(byte)0xFF});
+        addAction.setUserIdGeneratorStrategy(input -> {
+            final byte[] bytes = new byte[34];
+            for (int i = 0; i <34; i++) {
+                bytes[i] = (byte)0xFF;
+            }
+            return bytes;
+        });
         addAction.initialize();
         
         context.setUsername("jdoe");
@@ -88,7 +93,8 @@ public class AddUserIdTest extends AbstractWebAuthnTest {
         assertNotNull(context.getUserId());
         final byte[] userId = context.getUserId();
         assert userId != null;
-        assertEquals(userId.length,1);
+        assertEquals(userId.length,34);
+        // just check the first byte, the rest should be the same
         assertEquals(userId[0], (byte)0xFF);
     }
     
@@ -172,7 +178,7 @@ public class AddUserIdTest extends AbstractWebAuthnTest {
         assertEquals(result.getId(), "InvalidRegistrationContext");
     }
     
-    @Test(expectedExceptions = ConstraintViolationException.class)
+    @Test
     public void testNewUserId_MoreThan64Bytes() throws ComponentInitializationException {
         addAction.setUserIdGeneratorStrategy(input -> {
             final Random random = new Random();
@@ -180,9 +186,14 @@ public class AddUserIdTest extends AbstractWebAuthnTest {
             random.nextBytes(byteArray);
             return byteArray;
         });
-        addAction.initialize();        
-        context.setUsername("jdoe");        
-        addAction.execute(src);
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNotNull(result);
+        assert result != null;
+        assertEquals(result.getId(), "InvalidRegistrationContext");
     }
+    
+    
+
 
 }
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredentialTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredentialTest.java
index 495e6e8..6a14df1 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredentialTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredentialTest.java
@@ -39,8 +39,8 @@ import com.yubico.webauthn.data.UserIdentity;
 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.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
index eaf1eda..3d69350 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
@@ -45,9 +45,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.authn.RegistrationResult;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
-import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index e4ca99f..cedd238 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -49,6 +49,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationCo
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.idp.profile.testing.RequestContextBuilder;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
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 0ab06b7..4ed0f21 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
@@ -39,6 +39,7 @@ import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
index 7643cc3..1dee29f 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertionTest.java
@@ -38,6 +38,7 @@ import com.yubico.webauthn.data.UserVerificationRequirement;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.authn.webauthn.client.impl.MockWebAuthnClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.MockAuthenticator;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.DecodingException;
 
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
index 123bb02..3536138 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
@@ -35,7 +35,6 @@ import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
-import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
index e1d182c..87cb832 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
@@ -43,7 +43,6 @@ import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
-import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.shared.collection.CollectionSupport;
 
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
similarity index 99%
rename from webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
rename to webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
index 0c01fa8..b9ee8d7 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/MockAuthenticator.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
 
 import static org.testng.Assert.assertEquals;
 

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


More information about the commits mailing list