[java-idp-plugin-webauthn] branch main updated: Add authentication and registration request param actions and builders

Phil Smart philip.smart at jisc.ac.uk
Thu Jan 18 12:00: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=59583e6c44f3ecefa299b88f179f0950334c131e

The following commit(s) were added to refs/heads/main by this push:
     new 59583e6  Add authentication and registration request param actions and builders
59583e6 is described below

commit 59583e6c44f3ecefa299b88f179f0950334c131e
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Jan 18 12:00:09 2024 +0000

    Add authentication and registration request param actions and builders
    
     - Cleanup interfaces
     - Cleanup contexts
---
 .../admin/CredentialCreationOptionsParameters.java | 268 +++++++++++++++++++++
 .../webauthn/authn/BaseOptionsParameters.java      |  69 ++++++
 .../authn/CredentialRequestOptionsParameters.java  | 137 +++++++++++
 .../client/WebAuthnAuthenticationClient.java       |  38 +--
 .../webauthn/context/BaseWebAuthnContext.java      |  37 ++-
 .../context/WebAuthnAuthenticationContext.java     |  17 +-
 .../context/WebAuthnRegistrationContext.java       |  75 +++++-
 .../logic/IsDiscoverableCredentialRequired.java    |   2 +-
 ...CredentialRequired.java => IsSecondFactor.java} |  27 +--
 .../CredentialCreationOptionsParametersTest.java   |  92 +++++++
 .../CredentialRequestOptionsParametersTest.java    |  40 +++
 .../AddAuthenticatorAttachmentRequirement.java     |  83 +++++++
 .../admin/impl/AddResidentKeyRequirement.java      |  76 ++++++
 .../CreatePublicKeyCredentialCreationOptions.java  |  55 ++++-
 .../impl/YubicoWebauthnAuthenticationClient.java   |  57 ++---
 .../impl/AddUserVerificationRequirement.java       |  77 ++++++
 .../CreatePublicKeyCredentialRequestOptions.java   |  26 +-
 .../impl/EnsureAllowedCredentialsIsEmpty.java      |  45 ++++
 .../webauthn-registration-beans.xml                |  45 ++--
 .../webauthn-registration-flow.xml                 |   3 +
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  49 ++--
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |  23 +-
 .../authn/webauthn/conf/authn/webauthn.properties  |   9 +-
 .../plugin/authn/webauthn/views/webauthn-authn.vm  |   8 +-
 .../authn/webauthn/views/webauthn-register.vm      |  22 +-
 .../views/webauthn-registration-outcomes.vm        |  14 +-
 .../authn/webauthn/views/webauthn-selector.vm      |   2 +-
 .../webauthn/views/webauthn-username-entry.vm      | 124 ++++++++++
 28 files changed, 1340 insertions(+), 180 deletions(-)

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
new file mode 100644
index 0000000..8029a59
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
@@ -0,0 +1,268 @@
+/*
+ * 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;
+
+import java.util.Collections;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.authn.BaseOptionsParameters;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class to hold the parameters required to build a PublicKeyCredentialRequestOptions.
+ */
+public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
+    
+    /** 
+     * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
+     * duplicate credentials during registration.
+     */
+    @Nonnull @NonnullElements final Set<PublicKeyCredentialDescriptor> excludeCredentials;
+    
+    /** 
+     * The username of the user that is the subject of this authentication.
+     */
+    @Nonnull @NotEmpty final String username;
+    
+    /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+    @Nullable final AuthenticatorAttachment authenticatorAttachment;
+    
+    /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+    @Nonnull final ResidentKeyRequirement residentKeyRequirement;    
+    
+    /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+    @Nonnull final byte[] userHandle;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param builder the builder to extract parameters from
+     */
+    @SuppressWarnings("null")
+    private CredentialCreationOptionsParameters(final Builder builder) {
+        super(builder.userVerificationRequirement, builder.challenge);
+        this.excludeCredentials = Constraint.isNotNull(builder.excludeCredentials,
+                "Exclude credentails can not be null");
+        this.username = Constraint.isNotEmpty(builder.username, "Username can not be null or empty");
+        this.residentKeyRequirement =  Constraint.isNotNull(builder.residentKeyRequirement,
+                "Resident key requirement can not be null");
+        this.userHandle = Constraint.isNotNull(builder.userHandle, "UserHandle can not be null");
+        this.authenticatorAttachment = builder.authenticatorAttachment;
+    }
+
+    /**
+     * Get the set of credentials that have already been registered with the IdP. 
+     * 
+     * @return the excludeCredentials.
+     */
+    @Nonnull @NonnullElements public final Set<PublicKeyCredentialDescriptor> getExcludeCredentials() {
+        return excludeCredentials;
+    }
+
+
+    /**
+     * Get the username
+     * 
+     * @return the username.
+     */
+    @Nonnull @NotEmpty public final String getUsername() {
+        return username;
+    }
+
+
+    /**
+     * Get the  authenticator attachment requirement. 
+     * 
+     * @return the authenticatorAttachment.
+     */
+    @Nullable public final AuthenticatorAttachment getAuthenticatorAttachment() {
+        return authenticatorAttachment;
+    }
+
+
+    /**
+     * Get the resident key requirement. 
+     * 
+     * @return the residentKeyRequirement.
+     */
+    @Nonnull public final ResidentKeyRequirement getResidentKeyRequirement() {
+        return residentKeyRequirement;
+    }
+
+
+    /**
+     * Get the user handle.
+     * 
+     * @return the userHandle
+     */
+    @Nonnull public final byte[] getUserHandle() {
+        return userHandle;
+    }
+
+
+    /** Get a new builder.*/
+    public static IUserVerificationRequirementStage builder() {
+        return new Builder();
+    }
+
+
+    /** Stage interface.*/
+    public interface IUserVerificationRequirementStage {
+        /** Does the authentication/registration require user verification.*/
+        public IChallengeStage withUserVerificationRequirement(
+                @Nonnull final UserVerificationRequirement userVerificationRequirement);
+    }
+
+
+    /** Stage interface.*/
+    public interface IChallengeStage {
+        /** The challenge sent to the authenticator in registration ceremonies.*/
+        public IExcludeCredentialsStage withChallenge(@Nonnull final byte[] challenge);
+    }
+
+
+    /** Stage interface.*/
+    public interface IExcludeCredentialsStage {
+        /** Credentials that have already been registered with the IdP.*/
+        public IUsernameStage withExcludeCredentials(
+                @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials);
+    }
+
+
+    /** Stage interface.*/
+    public interface IUsernameStage {
+        /** The username of the user that is the subject of this authentication.*/
+        public IResidentKeyRequirementStage withUsername(@Nonnull @NotEmpty final String username);
+    }
+
+
+    /** Stage interface.*/
+    public interface IResidentKeyRequirementStage {
+        /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+        public IUserHandleStage withResidentKeyRequirement(
+                @Nonnull final ResidentKeyRequirement residentKeyRequirement);
+    }
+
+
+    /** Stage interface.*/
+    public interface IUserHandleStage {
+        /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+        public IBuildStage withUserHandle(@Nonnull final byte[] userHandle);
+    }
+
+
+    /** Stage interface.*/
+    public interface IBuildStage {
+        /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+        public IBuildStage withAuthenticatorAttachment(@Nullable AuthenticatorAttachment authenticatorAttachment);
+
+        /** Build the options.*/
+        public CredentialCreationOptionsParameters build();
+    }
+
+
+    /** Builder class.*/
+    public static final class Builder implements IUserVerificationRequirementStage, IChallengeStage,
+            IExcludeCredentialsStage, IUsernameStage, IResidentKeyRequirementStage, IUserHandleStage, IBuildStage {
+        
+        /** Does the authentication/registration require user verification.*/
+        private UserVerificationRequirement userVerificationRequirement;
+        
+        /** The challenge sent to the authenticator in registration ceremonies.*/
+        private byte[] challenge;
+        
+        /** Credentials that have already been registered with the IdP.*/
+        private Set<PublicKeyCredentialDescriptor> excludeCredentials = Collections.emptySet();
+        
+        /** The username of the user that is the subject of this authentication.*/
+        private String username;
+        
+        /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+        private ResidentKeyRequirement residentKeyRequirement;
+        
+        /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+        private byte[] userHandle;
+        
+        /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+        private AuthenticatorAttachment authenticatorAttachment;
+
+        /** Constructor.*/
+        private Builder() {
+        }
+
+        @Override
+        public IChallengeStage withUserVerificationRequirement(
+                @Nonnull final UserVerificationRequirement userVerificationRequirement) {
+            this.userVerificationRequirement = userVerificationRequirement;
+            return this;
+        }
+
+        @Override
+        public IExcludeCredentialsStage withChallenge(@Nonnull final byte[] challenge) {
+            this.challenge = challenge;
+            return this;
+        }
+
+        @Override
+        public IUsernameStage withExcludeCredentials(
+                @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials) {
+            this.excludeCredentials = excludeCredentials;
+            return this;
+        }
+
+        @Override
+        public IResidentKeyRequirementStage withUsername(@Nonnull final String username) {
+            this.username = username;
+            return this;
+        }
+
+        @Override
+        public IUserHandleStage withResidentKeyRequirement(
+                @Nonnull final ResidentKeyRequirement residentKeyRequirement) {
+            this.residentKeyRequirement = residentKeyRequirement;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withUserHandle(@Nonnull final byte[] userHandle) {
+            this.userHandle = userHandle;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withAuthenticatorAttachment(
+                @Nullable final AuthenticatorAttachment authenticatorAttachment) {
+            this.authenticatorAttachment = authenticatorAttachment;
+            return this;
+        }
+
+        @Override
+        public CredentialCreationOptionsParameters build() {
+            return new CredentialCreationOptionsParameters(this);
+        }
+    }
+   
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.java
new file mode 100644
index 0000000..22d823b
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.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.authn;
+
+import javax.annotation.Nonnull;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract options parameters class. Common to credential create or request options. 
+ */
+public abstract class BaseOptionsParameters {    
+    
+    /** Does the authentication/registration require user verification.*/
+    @Nonnull protected final UserVerificationRequirement userVerificationRequirement;
+    
+    /** The challenge sent to the authenticator in authentication or registration ceremonies.*/
+    @Nonnull protected final byte[] challenge;
+
+    /**
+     * Constructor.
+     *
+     * @param userVerificationRequirement the user verification requirement
+     * @param challenge the challenge
+     */
+    protected BaseOptionsParameters(@Nonnull final UserVerificationRequirement userVerificationRequirement, 
+            @Nonnull final byte[] challenge) {
+        super();
+        this.userVerificationRequirement = 
+                Constraint.isNotNull(userVerificationRequirement, "UserVerificationRequirement can not be null");
+        this.challenge = Constraint.isNotNull(challenge, "Challenge can not be null");
+    }    
+
+    /**
+     * Get the user verification requirement. 
+     * 
+     * @return the userVerificationRequirement.
+     */
+    @Nonnull public final UserVerificationRequirement getUserVerificationRequirement() {
+        return userVerificationRequirement;
+    }
+
+
+
+    /**
+     * Get the challenge.
+     * 
+     * @return the challenge.
+     */
+    @Nonnull public final byte[] getChallenge() {
+        return challenge;
+    }
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java
new file mode 100644
index 0000000..86972f5
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java
@@ -0,0 +1,137 @@
+/*
+ * 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.authn;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class to hold the parameters required to build a PublicKeyCredentialRequestOptions.
+ */
+public class CredentialRequestOptionsParameters extends BaseOptionsParameters {
+    
+    /** 
+     * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
+     * duplicate credentials during registration.
+     */
+    @Nonnull @NonnullElements private final List<PublicKeyCredentialDescriptor> allowCredentials;
+
+    
+    private CredentialRequestOptionsParameters(final Builder builder) {
+        super(builder.userVerificationRequirement, builder.challenge);
+        Constraint.isNotNull(builder.allowCredentials, "AllowCredentials can not be null");
+        allowCredentials = 
+                builder.allowCredentials.stream()
+                    .filter(Objects::nonNull)
+                    .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        
+    }
+    
+    /**
+     * Get the set of credentials that are acceptable to the IdP. 
+     * 
+     * @return the allowable credentials.
+     */
+    @Nonnull @NonnullElements public final List<PublicKeyCredentialDescriptor> getAllowCredentials() {
+        return allowCredentials;
+    }
+
+    /** Create a new builder. */
+    public static IUserVerificationRequirementStage builder() {
+        return new Builder();
+    }
+
+    /** Stage interface.*/
+    public interface IUserVerificationRequirementStage {
+        /** Does the authentication/registration require user verification.*/
+        public IChallengeStage withUserVerificationRequirement(
+                @Nonnull final UserVerificationRequirement userVerificationRequirement);
+    }
+
+    /** Stage interface.*/
+    public interface IChallengeStage {
+        /** The challenge sent to the authenticator in authentication ceremonies.*/
+        public IAllowCredentialsStage withChallenge(@Nonnull final byte[] challenge);
+    }
+
+    /** Stage interface.*/
+    public interface IAllowCredentialsStage {
+        /** Credentials that have already been registered with the IdP.**/
+        public IBuildStage withAllowCredentials(@Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials);
+    }
+
+    /** Build the options.*/
+    public interface IBuildStage {
+        public CredentialRequestOptionsParameters build();
+    }
+
+    /** Builder class.*/
+    public static final class Builder
+            implements IUserVerificationRequirementStage, IChallengeStage, IAllowCredentialsStage, IBuildStage {
+        
+        /** Does the authentication/registration require user verification.*/
+        private UserVerificationRequirement userVerificationRequirement;
+        
+        /** The challenge sent to the authenticator in authentication ceremonies.*/
+        private byte[] challenge;
+        
+        /** Credentials that have already been registered with the IdP.**/
+        private List<PublicKeyCredentialDescriptor> allowCredentials = CollectionSupport.emptyList();
+
+        /** Constructor.*/
+        private Builder() {
+        }
+
+        @Override
+        public IChallengeStage withUserVerificationRequirement(
+                @Nonnull final UserVerificationRequirement userVerificationRequirement) {
+            this.userVerificationRequirement = userVerificationRequirement;
+            return this;
+        }
+
+        @Override
+        public IAllowCredentialsStage withChallenge(@Nonnull final byte[] challenge) {
+            this.challenge = challenge;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withAllowCredentials(@Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials) {
+            this.allowCredentials = allowCredentials;
+            return this;
+        }
+
+        @Override
+        public CredentialRequestOptionsParameters build() {
+            return new CredentialRequestOptionsParameters(this);
+        }
+    }
+
+    
+    
+   
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
index 701a4d2..ed95c88 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
@@ -1,8 +1,5 @@
 package net.shibboleth.idp.plugin.authn.webauthn.client;
 
-import java.util.List;
-import java.util.Set;
-
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
@@ -15,15 +12,16 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
 import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
 
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
 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.exception.WebAuthnAuthenticationClientException;
 
 /**
- * A client that manages the entire webauthn authentication and registration ceremony.
+ * A client that manages a WebAuthn authentication or registration ceremony.
  * 
  * <p>Tied to the Yubico library data model, other clients will need to be adapted to support this model, and provide
  * translations between their internal representation and the types required.</p>
@@ -36,41 +34,30 @@ public interface WebAuthnAuthenticationClient {
     
      /**
       * Create a PublicKeyCredentialRequestOptions for the WebAuthn 'get' call to generate an authentication assertion.
+      *          
+      * @param requestParams the options that should be present in the authentication request. 
       * 
-      * @param username the username of the username that has been pre-identified. Can be null
-      *                 if no user has been identified, and the IdP is requesting the client discover
-      *                 the credential.  
-      * @param allowCredentials the set of public key credentials acceptable to the IdP in descending order of 
-      *                 preference
-      * @param challenge the challenge the authenticator signs along with other data to produce an assertion
-      * 
-      * @return a PublicKeyCredentialRequestOptions object
+      * @return a PublicKeyCredentialRequestOptions object to supply the WebAuthn 'get' call
       * 
       * @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
       *         
       */
-     @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username, 
-             @Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials, @Nonnull final byte[] challenge) 
+     @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(
+             @Nonnull final CredentialRequestOptionsParameters requestParams) 
                      throws WebAuthnAuthenticationClientException;
      
      /**
       * Create a PublicKeyCredentialCreationOptions for the WebAuthn 'create' call to generate a registration 
       * attestation.
       * 
-      * @param username the username of the username that has been pre-identified. Can be {@code null}
-      *                 if no user has been identified, and the IdP is requesting the client discover
-      *                 the credential.  
-      * @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
-      * @param the challenge to sign when creating new credentials
+      * @param creationOptions the options that should be present in the registration request. 
       * 
-      * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call. 
+      * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call 
       *         
       * @throws WebAuthnAuthenticationClientException if there is an error generating the creation request
       */
-     @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(
-             @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials, @Nullable final String username, 
-             @Nullable final byte[] userHandle, @Nonnull final byte[] challenge) 
-                     throws WebAuthnAuthenticationClientException;
+     @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(@Nonnull final
+             CredentialCreationOptionsParameters creationOptions) throws WebAuthnAuthenticationClientException;
      
      
      /**
@@ -85,6 +72,7 @@ public interface WebAuthnAuthenticationClient {
       * 
       * @throws WebAuthnAuthenticationClientException if the assertion is not valid
       */
+     //TODO would need our own AssertionResult to make this usuable beyond Yubico.
      AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username, 
              @Nullable final byte[] userHandle, 
              @Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions, 
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 ff00804..fc63106 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
@@ -21,6 +21,8 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
 
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.annotation.constraint.Unmodifiable;
@@ -51,7 +53,10 @@ public class BaseWebAuthnContext extends BaseContext {
     @Nullable private byte[] serverChallenge;  
     
     /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
-    @Nullable private byte[] userHandle;
+    @Nullable private byte[] userHandle;    
+    
+    /** Does the authentication/registration require user verification.*/
+    @Nullable private UserVerificationRequirement userVerificationRequirement;
     
     /**
      * Gets the username.
@@ -79,9 +84,10 @@ public class BaseWebAuthnContext extends BaseContext {
      * 
      * @param credentials the set of credentials
      */
-    public void setExistingCredentials(@Nullable final Collection<CredentialRegistration> credentials) {
+    @Nonnull public BaseWebAuthnContext setExistingCredentials(
+            @Nullable final Collection<CredentialRegistration> credentials) {
         existingCredentials = credentials;
-        
+        return this;
     }
     
     /**
@@ -132,7 +138,7 @@ public class BaseWebAuthnContext extends BaseContext {
      * 
      * @param handle The userHandle to set.
      */
-    public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
+    @Nonnull public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
         Constraint.isNotEmpty(handle,"UserHandle can not be null or empty");
         Constraint.isLessThan(65, handle.length, "UserHandle must be maximum 64 bytes");
         userHandle = handle;
@@ -144,8 +150,29 @@ public class BaseWebAuthnContext extends BaseContext {
      * 
      * @return the userHandle.
      */
-    public byte[] getUserHandle() {
+    @Nullable public byte[] getUserHandle() {
         return userHandle;
     }
+    
+    
+    /**
+     * Set the user verification requirement. 
+     * 
+     * @param requirement The user verification requirement to set.
+     */
+    @Nonnull public BaseWebAuthnContext setUserVerificationRequirement(
+            @Nullable final UserVerificationRequirement requirement) {
+        userVerificationRequirement = requirement;
+        return this;
+    }
+    
+    /**
+     * Get the user verification requirement. 
+     * 
+     * @return the user verification requirement.
+     */
+    @Nullable public UserVerificationRequirement getUserVerificationRequirement() {
+        return userVerificationRequirement;
+    }
 
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
index 61c57f1..a6b95a6 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -15,10 +15,7 @@ import net.shibboleth.shared.logic.Constraint;
 /** Authentication context for processing WebAuthn Authentication Ceremonies. */
 @NotThreadSafe
 public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
-        
-    /** The  credential public key encoded in COSE_Key format.*/
-    @Nullable private byte[] publicKey;
-    
+            
     /** The credential identifier generated by the authenticator.*/
     @Nullable private byte[] credentialId;
     
@@ -33,17 +30,6 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
     /** The public key credential request options for authentication represented as a JSON string.*/ 
     @Nullable private String publicKeyCredentialRequestOptionsJSON;
 
-    
-    /**
-     * Set the public key, as a byte array, in COSE_Key format.
-     * 
-     * @param key the public key in COSE_Key format.
-     */
-    public WebAuthnAuthenticationContext setPublicKey(@Nonnull final byte[] key) {
-        publicKey = Constraint.isNotNull(key, "Public key can not be null");
-        return this;
-    }
-
     /**
      * Get the credential Id.
      * 
@@ -122,4 +108,5 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
         publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
         return this;
     }
+
 }
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 6c7c8f1..aee6d36 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
@@ -1,13 +1,16 @@
 package net.shibboleth.idp.plugin.authn.webauthn.context;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
 import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAttachment;
 import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
 
 
 /** 
@@ -49,11 +52,38 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
 
     /** The ID of the credential that is going to be removed.*/
     @Nullable private byte[] credentialIdToRemove;    
+    
+    /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+    @Nullable private ResidentKeyRequirement residentKeyRequirement;
+    
+    /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+    @Nullable private AuthenticatorAttachment authenticatorAttachmentRequirement;
+    
+    /**
+     * Set the AuthenticatorAttachment requirement. {@code null} would represent either possibility.
+     * 
+     * @param authenticatorAttachmentRequirement The authenticatorAttachmentRequirement to set.
+     */
+    public WebAuthnRegistrationContext setAuthenticatorAttachmentRequirement(@Nullable final AuthenticatorAttachment requirement) {
+        authenticatorAttachmentRequirement = requirement;
+        return this;
+    }
+    
+    /** 
+     * 
+     * Get the AuthenticatorAttachment requirement. {@code null} would represent either possibility.
+     * 
+     * @return the authenticator attachment requirement.
+     */
+    public AuthenticatorAttachment getAuthenticatorAttachmentRequirement() {
+        return authenticatorAttachmentRequirement;
+    }
+    
 
     /**
      * Get the attestation response as a result of creating a new credential.
      * 
-     * @return Returns the authenticator attestation response.
+     * @return the authenticator attestation response.
      */
     @Nullable public PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> 
             getAuthenticatorAttestationResponse() {
@@ -65,7 +95,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
      */
-    public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(
+    @Nonnull public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(
             @Nullable final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> 
             attestation) {
         authenticatorAttestationResponse = attestation;
@@ -77,7 +107,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @param options The publicKeyCredentialCreationOptions to set.
      */
-    public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
+    @Nonnull public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
             @Nullable final PublicKeyCredentialCreationOptions options) {
         publicKeyCredentialCreationOptions = options;
         return this;
@@ -98,7 +128,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @param registrationResult The registrationResult to set.
      */
-    public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
+    @Nonnull public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
         registrationResult = result;
         return this;
     }
@@ -109,21 +139,25 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @return Returns the registrationResult.
      */
-    public RegistrationResult getRegistrationResult() {
+    @Nullable public RegistrationResult getRegistrationResult() {
         return registrationResult;
     }
     
     /**
-     * @return Returns the publicKeyCredentialCreationOptionsJSON.
+     * Get the credential public key registration options in JSON format.
+     * 
+     * @return the publicKeyCredentialCreationOptions in JSON format.
      */
-    public String getPublicKeyCredentialCreationOptionsJSON() {
+    @Nullable public String getPublicKeyCredentialCreationOptionsJSON() {
         return publicKeyCredentialCreationOptionsJSON;
     }
     
     /**
-     * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptionsJSON to set.
+     * Set the credential public key registration options in JSON format.
+     * 
+     * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptions in JSON to set.
      */
-    public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
+    @Nonnull public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
         publicKeyCredentialCreationOptionsJSON = optionsJSON;
         return this;
     }
@@ -134,7 +168,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @param nickname The credential nickname to set.
      */
-    public WebAuthnRegistrationContext setCredentialNickname(@Nullable final String nickname) {
+    @Nonnull public WebAuthnRegistrationContext setCredentialNickname(@Nullable final String nickname) {
         credentialNickname = nickname;
         return this;
     }
@@ -153,7 +187,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
      * 
      * @param credentialId the credential identifier
      */
-    public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
+    @Nonnull public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
         credentialIdToRemove = id;
         return this;
     }
@@ -167,4 +201,23 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
         return credentialIdToRemove;
     }
 
+    /**
+     * Set the ResidentKey requirement. Also know as a discoverable credential.
+     * 
+     * @param requirement The resident key requirement to set.
+     */
+    @Nonnull public WebAuthnRegistrationContext setResidentKeyRequirement(
+            @Nullable final ResidentKeyRequirement requirement) {
+        residentKeyRequirement = requirement;
+        return this;
+    }
+    
+    /**
+     * Get the ResidentKey requirement. Also know as a discoverable credential.
+     * 
+     * @return the requirement.
+     */
+    @Nullable public ResidentKeyRequirement getResidentKeyRequirement() {
+        return residentKeyRequirement;
+    }
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
index 3db25ca..d8b3800 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
@@ -28,7 +28,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 
 /** 
  * A predicate that determines if the authentication ceremony requires a discoverable credential supplied by the 
- * authenticator, or if we have a username to determine which credentials to use on from the IdP. 
+ * authenticator, or if we have a username to determine which credentials to use from the IdP. 
  */
 public class IsDiscoverableCredentialRequired implements Predicate<ProfileRequestContext> {
     
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
similarity index 58%
copy from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
copy to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
index 3db25ca..b680f82 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
@@ -23,17 +23,16 @@ 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.WebAuthnAuthenticationContext;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /** 
- * A predicate that determines if the authentication ceremony requires a discoverable credential supplied by the 
- * authenticator, or if we have a username to determine which credentials to use on from the IdP. 
+ * A predicate that determines if the authentication flow is being used as a second factor of authentication, and not
+ * a first (and possibly only) factor. Returns true if second factor use, or false if passwordless/first factor.
  */
-public class IsDiscoverableCredentialRequired implements Predicate<ProfileRequestContext> {
+public class IsSecondFactor implements Predicate<ProfileRequestContext> {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(IsDiscoverableCredentialRequired.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(IsSecondFactor.class);
 
     @Override
     public boolean test(@Nullable final ProfileRequestContext input) {
@@ -46,16 +45,14 @@ public class IsDiscoverableCredentialRequired implements Predicate<ProfileReques
             log.trace("Authentication context was null, can not determine if discoverable credentials are required");
             return false;
         }
-        final WebAuthnAuthenticationContext webauthnContext = 
-                authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
-        if (webauthnContext == null) {
-            log.trace("WebAuthn authentication context was null, can not determine if discoverable credentials "
-                    + "are required");
-            return false;
+        
+        //TODO no decision here yet. 
+        final boolean secondFactor = false;
+        if (secondFactor) {
+            log.debug("Request contained a previous factor, assuming second factor");
+            return true;
         }
-        final boolean discoverableCredentialRequired = webauthnContext.getUsername() == null;
-        log.debug("{}", discoverableCredentialRequired ? "Usernameless authentication required" : 
-            "Passwordless authentication required for '"+webauthnContext.getUsername()+"'");
-        return discoverableCredentialRequired;
+        log.debug("Request did not contain a previous factor");
+        return false;
     }
 }
diff --git a/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java
new file mode 100644
index 0000000..eb0980a
--- /dev/null
+++ b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link CredentialCreationOptionsParameters}.
+ */
+public class CredentialCreationOptionsParametersTest {
+    
+    @Test
+    public void testBuilder() {
+        final var options = CredentialCreationOptionsParameters.builder()
+                .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+                .withChallenge(new byte[0])
+                .withExcludeCredentials(CollectionSupport.emptySet())
+                .withUsername("username")
+                .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+                .withUserHandle(new byte[0])
+                .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+                .build();
+        assertNotNull(options);
+        assertEquals(options.getUsername(), "username");
+    }
+    
+    @SuppressWarnings("null")
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testBuilderNullUsername() {
+        CredentialCreationOptionsParameters.builder()
+            .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+            .withChallenge(new byte[0])
+            .withExcludeCredentials(CollectionSupport.emptySet())
+            .withUsername(null)
+            .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+            .withUserHandle(new byte[0])
+            .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+            .build();       
+    }
+    
+    @SuppressWarnings("null")
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testBuilderNullUserHandle() {
+        CredentialCreationOptionsParameters.builder()
+            .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+            .withChallenge(new byte[0])
+            .withExcludeCredentials(CollectionSupport.emptySet())
+            .withUsername("username")
+            .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+            .withUserHandle(null)
+            .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+            .build();   
+    }
+    
+    @SuppressWarnings("null")
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testBuilderNullExcludeCredentials() {
+        CredentialCreationOptionsParameters.builder()
+            .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+            .withChallenge(new byte[0])
+            .withExcludeCredentials(null)
+            .withUsername("username")
+            .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+            .withUserHandle(new byte[0])
+            .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+            .build();
+
+    }
+
+}
diff --git a/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java
new file mode 100644
index 0000000..467ab7e
--- /dev/null
+++ b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.authn;
+
+import static org.testng.Assert.assertNotNull;
+
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Test for {@link CredentialRequestOptionsParameters}
+ */
+public class CredentialRequestOptionsParametersTest {
+    
+    @Test
+    public void CredentialRequestOptionsParameters() {
+        final var options = CredentialRequestOptionsParameters.builder()
+        .withUserVerificationRequirement(UserVerificationRequirement.PREFERRED)
+        .withChallenge(new byte[0])
+        .withAllowCredentials(CollectionSupport.emptyList())
+        .build();
+        assertNotNull(options);
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java
new file mode 100644
index 0000000..c47e986
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java
@@ -0,0 +1,83 @@
+/*
+ * 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.stream.Stream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a ResidentKey requirement to the {@link WebAuthnRegistrationContext context}.
+ */
+public class AddAuthenticatorAttachmentRequirement extends AbstractWebAuthnRegistrationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddAuthenticatorAttachmentRequirement.class);
+    
+    /** Set the AuthenticatorAttachment requirement. Default is null, so any.*/
+    @Nullable private AuthenticatorAttachment authenticatorAttachmentRequirement;
+    
+    /** Constructor.*/
+    public AddAuthenticatorAttachmentRequirement() {
+        authenticatorAttachmentRequirement = null;
+    }
+    
+    /**
+     * Set the AuthenticatorAttachment requirement. 
+     * 
+     * @param requirement The AuthenticatorAttachment requirement to set.
+     */
+    public void setAuthenticatorAttachmentRequirement(@Nonnull @NotEmpty final String requirement) {
+        checkSetterPreconditions();
+        Constraint.isNotEmpty(requirement, "AuthenticatorAttachment requirement can not be null or empty");
+        
+        if ("any".equals(requirement)) {
+            authenticatorAttachmentRequirement = null;
+        } else {        
+            final AuthenticatorAttachment aaRequirement = 
+                    Stream.of(AuthenticatorAttachment.values())
+                    .filter(aa -> aa.getValue().equals(requirement))
+                    .findAny()
+                    .orElseThrow(() -> 
+                    new ConstraintViolationException("AuthenticatorAttachment requirement "+requirement+" unknown"));
+            assert aaRequirement != null;
+            authenticatorAttachmentRequirement = aaRequirement;
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(final ProfileRequestContext profileRequestContext, 
+            final WebAuthnRegistrationContext context) {
+
+        log.debug("{} AuthenticatorAttachment is '{}'",getLogPrefix(), authenticatorAttachmentRequirement != null ?
+                authenticatorAttachmentRequirement : "ANY");
+        context.setAuthenticatorAttachmentRequirement(authenticatorAttachmentRequirement);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java
new file mode 100644
index 0000000..e22d9d5
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java
@@ -0,0 +1,76 @@
+/*
+ * 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.stream.Stream;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a ResidentKey requirement to the {@link WebAuthnRegistrationContext context}.
+ */
+public class AddResidentKeyRequirement extends AbstractWebAuthnRegistrationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddResidentKeyRequirement.class);
+    
+    /** Set the ResidentKey requirement. Default is PREFERRED.*/
+    @Nonnull private ResidentKeyRequirement residentKeyRequirement;
+    
+    /** Constructor.*/
+    public AddResidentKeyRequirement() {
+        residentKeyRequirement = ResidentKeyRequirement.PREFERRED;
+    }
+    
+    /**
+     * Set the ResidentKey requirement. 
+     * 
+     * @param requirement The ResidentKey requirement to set.
+     */
+    public void setResidentKeyRequirement(@Nonnull @NotEmpty final String requirement) {
+        checkSetterPreconditions();
+        Constraint.isNotEmpty(requirement, "ResidentKey requirement can not be null or empty");
+        
+        final ResidentKeyRequirement uvRequirement = 
+                Stream.of(ResidentKeyRequirement.values())
+                .filter(rk -> rk.getValue().equals(requirement))
+                .findAny()
+                .orElseThrow(() -> new ConstraintViolationException("ResidentKey requirement "+requirement+" unknown"));
+        assert uvRequirement != null;
+        residentKeyRequirement = uvRequirement;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(final ProfileRequestContext profileRequestContext, 
+            final WebAuthnRegistrationContext context) {
+
+        log.debug("{} ResidentKey is '{}'",getLogPrefix(), residentKeyRequirement);
+        context.setResidentKeyRequirement(residentKeyRequirement);
+        
+    }
+
+}
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 b225147..e08b9ee 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
@@ -30,13 +30,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
 import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
 
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
 import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -81,12 +85,37 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
 
         final byte[] challenge = context.getServerChallenge();
         if (challenge == null) {
-            log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
+            log.error("{} WebAuthn challenge is null",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
-        
-        try {         
+        final ResidentKeyRequirement residentKeyRequirement = context.getResidentKeyRequirement();
+        if (residentKeyRequirement == null) {
+            log.error("{} ResidentKeyRequirement is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final UserVerificationRequirement uvRequirement = context.getUserVerificationRequirement();
+        if (uvRequirement == null) {
+            log.error("{} UserVerificationRequirement is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final String username = context.getUsername();
+        if (username == null) {
+            log.error("{} Username is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        final byte[] userHandle = context.getUserHandle();
+        if (userHandle == null) {
+            log.error("{} UserHandle is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+
+        try {       
+            
             final Set<PublicKeyCredentialDescriptor> existingCredentialDescriptors  = context.getExistingCredentials()
                     .stream()
                         .map(cred -> cred.toPublicKeyCredentialDescriptor())
@@ -94,21 +123,31 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
                         .collect(Collectors.toSet());
             
             assert null != existingCredentialDescriptors;
-            
+
+            final CredentialCreationOptionsParameters creationOptions = CredentialCreationOptionsParameters.builder()
+                    .withUserVerificationRequirement(uvRequirement)
+                    .withChallenge(challenge)
+                    .withExcludeCredentials(existingCredentialDescriptors)
+                    .withUsername(username)
+                    .withResidentKeyRequirement(residentKeyRequirement)
+                    .withUserHandle(userHandle).withAuthenticatorAttachment(
+                            context.getAuthenticatorAttachmentRequirement())
+                    .build();
+
+            assert creationOptions != null;
             final PublicKeyCredentialCreationOptions pkCredCreationOptions = 
-                    getWebAuthnClient().createRegistrationRequest(existingCredentialDescriptors, context.getUsername(), 
-                            context.getUserHandle(), challenge);
+                    getWebAuthnClient().createRegistrationRequest(creationOptions);
             
             context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
             //convert to JSON for the JS API to use
             context.setPublicKeyCredentialCreationOptionsJSON(objectMapper.writeValueAsString(pkCredCreationOptions));
             
             log.debug("{} Created PublicKeyCredentialCreationOptions '{}'",getLogPrefix(), pkCredCreationOptions);
-        } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
+        } catch (final WebAuthnAuthenticationClientException | JsonProcessingException | ConstraintViolationException e) {
             log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
             return;
-        }        
+        } 
     }
 
 }
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 28b4cdf..25e0f61 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
@@ -18,7 +18,6 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.Set;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -41,14 +40,13 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
 import com.yubico.webauthn.data.PublicKeyCredentialParameters;
 import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
-import com.yubico.webauthn.data.ResidentKeyRequirement;
 import com.yubico.webauthn.data.UserIdentity;
-import com.yubico.webauthn.data.UserVerificationRequirement;
 import com.yubico.webauthn.exception.RegistrationFailedException;
 
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
 import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
@@ -99,26 +97,19 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
     }
 
     @Override
-    public PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username, 
-            @Nullable final List<PublicKeyCredentialDescriptor> allowCredentials, @Nonnull final byte[] challenge) 
-                    throws WebAuthnAuthenticationClientException {
- 
-        //set default to preferred.
-        UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
-        if (username == null) {
-            //then require user verification if no username is known. 
-            userVerificationRequirement = UserVerificationRequirement.REQUIRED;
-        }        
+    public PublicKeyCredentialRequestOptions createAuthenticationRequest(
+            @Nonnull final CredentialRequestOptionsParameters requestParams) 
+                    throws WebAuthnAuthenticationClientException { 
         
         final PublicKeyCredentialRequestOptions request = PublicKeyCredentialRequestOptions.builder()
-                    .challenge(new ByteArray(challenge))
+                    .challenge(new ByteArray(requestParams.getChallenge()))
                     .rpId(rp.getIdentity().getId())
-                    .allowCredentials(Optional.ofNullable(allowCredentials))
+                    .allowCredentials(Optional.ofNullable(requestParams.getAllowCredentials()))
 //                        .extensions(
 //                            startAssertionOptions
 //                                .getExtensions()
 //                                .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
-                    .userVerification(userVerificationRequirement)
+                    .userVerification(requestParams.getUserVerificationRequirement())
                     .timeout(Optional.of(60000l))
                     .build();
         if (request == null) {
@@ -131,35 +122,27 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
     /** {@inheritDoc} */
     @Override
     public PublicKeyCredentialCreationOptions createRegistrationRequest(
-            @Nullable final Set<PublicKeyCredentialDescriptor> excludeCredentials, @Nullable final String username, 
-            final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
-       
-        //set default to preferred.
-        ResidentKeyRequirement residentKeyRquirement = ResidentKeyRequirement.PREFERRED;        
-        if (username == null) {
-            // FIXME Fix this
-            //then require a resident key, but this can not happen during registration
-            residentKeyRquirement = ResidentKeyRequirement.REQUIRED;
+            @Nonnull final CredentialCreationOptionsParameters creationOptions) 
+                    throws WebAuthnAuthenticationClientException {
 
-        }
-        
-        // If U2F we do not need UV. So conditional required.
-        final UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
         
         final UserIdentity identity = 
-                UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
+                UserIdentity.builder().name(creationOptions.getUsername())
+                    .displayName(creationOptions.getUsername())
+                    .id(new ByteArray(creationOptions.getUserHandle()))
+                    .build();
         
         final PublicKeyCredentialCreationOptions creation = PublicKeyCredentialCreationOptions.builder()
                     .rp(rp.getIdentity())
                     .user(identity)
-                    .challenge(new ByteArray(challenge))
+                    .challenge(new ByteArray(creationOptions.getChallenge()))
                     .pubKeyCredParams(preferredPublickeyParams)
-                    .excludeCredentials(excludeCredentials)
+                    .excludeCredentials(creationOptions.getExcludeCredentials())
                     .authenticatorSelection(AuthenticatorSelectionCriteria.builder()
-                            .userVerification(userVerificationRequirement)
-                            .residentKey(residentKeyRquirement)
+                            .userVerification(creationOptions.getUserVerificationRequirement())
+                            .residentKey(creationOptions.getResidentKeyRequirement())
+                            .authenticatorAttachment(creationOptions.getAuthenticatorAttachment())
                             .build())
-//                        .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection())
 //                        .extensions(
 //                            startRegistrationOptions
 //                                .getExtensions()
@@ -197,7 +180,7 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
                 log.debug("Attempting validation of credential with known username '{}' and userHandle '{}'",
                         username, userHandle);
             }
-            
+
             final AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
                     .request(requestAssertion)
                     .response(authenticatorAssertionResponse)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java
new file mode 100644
index 0000000..01e09ad
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.stream.Stream;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a UserVerification requirement to the {@link WebAuthnAuthenticationContext context}.
+ */
+public class AddUserVerificationRequirement extends AbstractWebAuthnBaseAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddUserVerificationRequirement.class);
+    
+    /** Set the user verification requirement.*/
+    @Nonnull private UserVerificationRequirement userVerificationRequirement;
+    
+    /** Constructor.*/
+    public AddUserVerificationRequirement() {
+        userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+    }
+    
+    /**
+     * Set the UserVerification requirement. 
+     * 
+     * @param requirement The userVerificationRequirement to set.
+     */
+    public void setUserVerificationRequirement(@Nonnull @NotEmpty final String requirement) {
+        checkSetterPreconditions();
+        Constraint.isNotEmpty(requirement, "userVerificationRequirement can not be null or empty");
+        
+        final UserVerificationRequirement uvRequirement = 
+                Stream.of(UserVerificationRequirement.values())
+                .filter(uv -> uv.getValue().equals(requirement))
+                .findAny()
+                .orElseThrow(() -> new ConstraintViolationException("UserVerification requirement "+requirement+" unknown"));
+        assert uvRequirement != null;
+        userVerificationRequirement = uvRequirement;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final BaseWebAuthnContext context) {
+
+        log.debug("{} UserVerification is '{}'",getLogPrefix(), userVerificationRequirement);
+        context.setUserVerificationRequirement(userVerificationRequirement);
+        
+    }
+
+}
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 72129a9..e8da523 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
@@ -18,6 +18,7 @@ package net.shibboleth.idp.plugin.authn.webauthn.impl;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 
@@ -29,14 +30,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
 import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+import com.yubico.webauthn.data.UserVerificationRequirement;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
 import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -84,7 +88,13 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
         
         final byte[] challenge = context.getServerChallenge();
         if (challenge == null) {
-            log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
+            log.error("{} WebAuthn challenge is null",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        final UserVerificationRequirement uvRequirement = context.getUserVerificationRequirement();
+        if (uvRequirement == null) {
+            log.error("{} User verification requirement is null",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
             return;
         }
@@ -94,13 +104,19 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
             final List<PublicKeyCredentialDescriptor> existingCredentialDescriptors  = existingCredentials.stream()
                 .map(cred -> cred.toPublicKeyCredentialDescriptor())
                 .filter(Objects::nonNull)
-                .toList();
+                .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+            
+            final CredentialRequestOptionsParameters requestParams = CredentialRequestOptionsParameters.builder()
+                .withUserVerificationRequirement(uvRequirement)
+                .withChallenge(challenge)
+                .withAllowCredentials(existingCredentialDescriptors)
+                .build();
             
             final PublicKeyCredentialRequestOptions pkCredRequestOptions = 
-                    client.createAuthenticationRequest(context.getUsername(), existingCredentialDescriptors, 
-                            challenge);
+                    client.createAuthenticationRequest(requestParams);
+            
             context.setPublicKeyCredentialRequestOptions(pkCredRequestOptions);
-            // Convert to JSON for the view. TODO maybe that should be converted by velocity
+            // Convert to JSON for the view. TODO maybe that should be converted by velocity etc.
             context.setPublicKeyCredentialRequestOptionsJSON(objectMapper.writeValueAsString(pkCredRequestOptions));
             
             log.debug("{} Created PublicKeyCredentialRequestOptions: '{}'",getLogPrefix(), pkCredRequestOptions);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java
new file mode 100644
index 0000000..909c86a
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that blanks any existing registered credentials in the WebAuthn context.
+ */
+//TODO should never be necessary given the flow, but just to make it explicit?
+public class EnsureAllowedCredentialsIsEmpty extends AbstractWebAuthnAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(EnsureAllowedCredentialsIsEmpty.class);
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {        
+        
+        log.trace("{} Removing any existing credentials found from the authentication request", getLogPrefix());
+        context.setExistingCredentials(null); 
+    }
+
+}
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 8695eb7..0933b6c 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
@@ -12,6 +12,12 @@
     <bean id="shibboleth.AdminProfileId" class="java.lang.String"
         c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
 
+    <!-- Flow Functions -->
+
+    <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
+
     <!-- TODO Should this been populating an authentication context for an admin flow? -->
     <bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
@@ -20,38 +26,45 @@
                 class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy" />
         </property>
     </bean>
-    
-    <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
-        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
-        c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
-        
+
+    <bean id="AddResidentKeyRequirement" scope="prototype" parent="AbstractWebAuthnRegistrationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddResidentKeyRequirement"
+        p:residentKeyRequirement="%{idp.authn.webauthn.registration.residentKey:preferred}" />
+
+    <bean id="AddAuthenticatorAttachmentRequirement" scope="prototype" parent="AbstractWebAuthnRegistrationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddAuthenticatorAttachmentRequirement"
+        p:authenticatorAttachmentRequirement="%{idp.authn.webauthn.registration.authenticatorAttachment:any}" />
+
+    <bean id="AddUserVerificationRequired" parent="AbstractWebAuthnBaseAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+        p:userVerificationRequirement="%{idp.authn.webauthn.registration.userVerification:discouraged}" />
+
     <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
-        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
 
     <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
-        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
-
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
 
     <bean id="GenerateUserHandle" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.GenerateUserHandle" />
 
     <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions" 
-        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"/>
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
 
     <bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractAuthenticatorAttestationFromFormRequest"
-        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
-        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" /> 
-        
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
     <bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyRemovalInformationFromFormRequest"
-        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>       
-        
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
     <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential"/>
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential" />
 
     <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
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 b6b95ad..f2517d3 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
@@ -28,6 +28,9 @@
         <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="GenerateServerChallenge"/>
         <evaluate expression="GenerateUserHandle"/>
+        <evaluate expression="AddResidentKeyRequirement"/>
+        <evaluate expression="AddAuthenticatorAttachmentRequirement"/>
+        <evaluate expression="AddUserVerificationRequired"/>
         <evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="DisplayWebAuthnView" />    
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 5d3ac72..8b8ff30 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -9,42 +9,59 @@
 
     default-init-method="initialize" default-destroy-method="destroy">
 
-     <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype" 
+    <!-- Functions -->
+    <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+        parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
+        c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+
+
+    <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
         p:usernameRequiredPredicate="false">
     </bean>
-    
-     <bean id="IsDiscoverableCredentialRequired" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsDiscoverableCredentialRequired"/>
-    
+
+    <bean id="IsSecondFactor" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsSecondFactor" />
+
+    <bean id="IsDiscoverableCredentialRequired" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsDiscoverableCredentialRequired" />
+
     <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext) }" />
+    
+    <bean id="EnsureAllowedCredentialsIsEmpty" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
+    class="net.shibboleth.idp.plugin.authn.webauthn.impl.EnsureAllowedCredentialsIsEmpty"/>
+
+    <bean id="AddUserVerificationRequired" parent="AbstractWebAuthnBaseAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+        p:userVerificationRequirement="required" />
+
+    <bean id="AddUserVerificationNotRequired" parent="AbstractWebAuthnAuthenticationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+        p:userVerificationRequirement="discouraged" />
 
-    <bean id ="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
-            parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
-            c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
-            
     <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
-        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"/>
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
 
     <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
-        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"/> 
-    
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
+
     <bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"
-        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"/>        
-        
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
     <bean id="ExtractAuthenticatorAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractAuthenticatorAssertionFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
         p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
 
     <bean id="ValidateWebAuthnAssertion" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion" 
-        p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>        
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion"
+        p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}" />
 
 
 </beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index 9a134ce..27aca88 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -5,23 +5,42 @@
     <action-state id="PopulateWebauthnContext">
         <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="DetermineUsernamelessOrPasswordlessLogin" />    
+        <transition on="proceed" to="DetermineSecondFactorLogin" />    
     </action-state>
     
+    <!-- Test if we are operating as a 2FA -->
+     <decision-state id="DetermineSecondFactorLogin">
+        <if test="IsSecondFactor.test(opensamlProfileRequestContext)"
+            then="SecondFactorLogin" 
+            else="DetermineUsernamelessOrPasswordlessLogin" />        
+    </decision-state>
+    
+    <!-- Test if we are operating as a first (and possibly only) factor -->
     <decision-state id="DetermineUsernamelessOrPasswordlessLogin">
         <if test="IsDiscoverableCredentialRequired.test(opensamlProfileRequestContext)"
             then="UsernamelessLogin" 
             else="PasswordlessLogin" />        
     </decision-state>
     
-    <!-- If passwordless and not usernameless we need a different flow here. Also User verification not required for 2FA-->
+    <!-- If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials-->
     <action-state id="UsernamelessLogin">
+        <evaluate expression="AddUserVerificationRequired"/>
+        <evaluate expression="EnsureAllowedCredentialsIsEmpty"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
     </action-state>
     
+    <!-- If Passwordless: do not require ResidentKey, require UV, UP, and provide previous credentials based on username -->
     <action-state id="PasswordlessLogin">
         <evaluate expression="LookupRegisteredCredentials"/>
+        <evaluate expression="AddUserVerificationRequired"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
+    </action-state>
+    
+    <!-- If we are running after a first factor, perform 2FA only -->
+    <action-state id="SecondFactorLogin">
+        <evaluate expression="AddUserVerificationNotRequired"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
     </action-state>
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 c746019..5892659 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
@@ -7,4 +7,11 @@ idp.authn.webauthn.allowOriginPort = true
 idp.authn.webauthn.allowOriginSubdomain = false
 
 ## Display debug information about the registration and authentication ceremony on their respective views?
-#idp.authn.webauthn.ui.debug = false
\ No newline at end of file
+#idp.authn.webauthn.ui.debug = false
+
+## Registration properties
+# idp.authn.webauthn.registration.residentKey = preferred
+### The authenticatorAttachment requirement. One-of 'any', 'cross-platform', or 'platform'. 
+# idp.authn.webauthn.registration.authenticatorAttachment = any
+### Require User Verification
+# idp.authn.webauthn.registration.userVerification = discouraged
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
index b462f3b..5ccd4b3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -76,17 +76,17 @@
                     <form id="authn-form" action="$flowExecutionUrl" method="post">
                         #parse("csrf/csrf.vm")
                         <input type="hidden" id="publicKeyAssertion" name="publicKeyAssertion"/>
-                        <button class="hidden" id="authenticationSubmit" type="submit" name="_eventId_proceed">Submit Authentication</button>
+                        <button class="hidden" id="authenticationSubmit" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.authn.submit", "Submit Authentication")</button>
                     </form>
                     #if($debug == "true")
                         <div class="centre">
-                            <button id="authenticate" class="form-element form-button">Authenticate</button>
+                            <button id="authenticate" class="form-element form-button">#springMessageText("idp.webauthn.authn.authenticate", "Authenticate")</button>
                         </div>                                           
                         <hr/>
-                        <button type="button" class="collapsible">Debugging</button> 
+                        <button type="button" class="collapsible">#springMessageText("idp.webauthn.debug.title", "Debugging")</button> 
                         
                         <div class="debug" id="debug-div">
-                          <label for="publicKeyCredentialCreation">Request Options</label>
+                          <label for="publicKeyCredentialCreation">#springMessageText("idp.webauthn.debug.request", "Request Options")</label>
                           <textarea id="publicKeyCredentialRequestOptions" name="publicKeyCredentialRequestOptions" rows="20" cols="50">
                             $webauthnContext.publicKeyCredentialRequestOptions</textarea>
                         </div>
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 c4f6992..093890e 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
@@ -82,11 +82,11 @@
                  #if ($webauthnRegContext.existingCredentials)
                     <table>
                         <tr>
-                            <th>Key Name</th>
-                            <th>Transports</th>
-                            <th>Is Passkey</th>
-                            <th>Registration Time</th>
-                            <th>Action</th>
+                            <th>#springMessageText("idp.webauthn.register.table.keyName", "Key Name")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.transports", "Transports")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.passkey", "Passkey?")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.registrationTime", "Registration Time")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.action", "Action")</th>
                         </tr>
                          #foreach($cred in $webauthnRegContext.existingCredentials)
                             <tr>
@@ -98,35 +98,35 @@
                                 <form id="deleteKeyForm" action="$flowExecutionUrl" method="post">
                                     #parse("csrf/csrf.vm")
                                     <input type="hidden" name="credentialId" value="$cred.credentialIdBase64Url"/>
-                                    <button class="webauthn-table-button" id="removeButton" type="submit" name="_eventId_deleteKey">Remove</button>
+                                    <button class="webauthn-table-button" id="removeButton" type="submit" name="_eventId_deleteKey">#springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
                                 </form>
                                 </td>
                             </tr>
                          #end   
                     </table>
                  #else
-                    <div><span>You have no registered keys</span></div>                 
+                    <div><span>#springMessageText("idp.webauthn.register.registered.noKeys", "You have no registered keys")</span></div>                 
                  #end
                  <br/>
                  <div>
-                    <button class="form-element form-button" id="registerButton">Add Key</button>
+                    <button class="form-element form-button" id="registerButton">#springMessageText("idp.webauthn.register.addKey", "Add Key")</button>
                  </div>
                  
                   <form id="authenticatorAttestationForm" action="$flowExecutionUrl" method="post">
                       #parse("csrf/csrf.vm")
                       <input type="hidden" id="credentialNickname" name="credentialNickname"/>
                       <input type="hidden" id="authenticatorAttestation" name="authenticatorAttestation"/>
-                      <button class="hidden" id="registrationSubmit" type="submit" name="_eventId_addKey">Submit Registration</button>
+                      <button class="hidden" id="registrationSubmit" type="submit" name="_eventId_addKey">#springMessageText("idp.webauthn.register.submit", "Submit Registration")</button>
                   </form>
             </div>
             
             #if($debug == "true")
                 <hr/>
                
-                <button type="button" class="collapsible">Debugging</button> 
+                <button type="button" class="collapsible">#springMessageText("idp.webauthn.register.debug.title", "Debugging")</button> 
                 
                 <div class="debug" id="debug-div">
-                    <label for="publicKeyCredentialCreation">Registration Options</label>
+                    <label for="publicKeyCredentialCreation">#springMessageText("idp.webauthn.register.debug.registration", "Registration Options")</label>
                     <textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
                     $webauthnRegContext.publicKeyCredentialCreationOptions</textarea>                   
                 </div>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
index cb7e06d..a43f1d3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
@@ -55,11 +55,11 @@
                     <p>Registered Keys</p>
                     <table>
                         <tr>
-                            <th>Key Name</th>
-                            <th>Transports</th>
-                            <th>Is Passkey</th>
-                            <th>User Verified On Registration</th>
-                            <th>Registration Time</th>
+                            <th>#springMessageText("idp.webauthn.register.table.keyName", "Key Name")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.transports", "Transports")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.passkey", "Passkey?")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.userVerifiedOnRegistration", "User Verified On Registration")</th>
+                            <th>#springMessageText("idp.webauthn.register.table.registrationTime", "Registration Time")</th>
                         </tr>
                         #foreach($cred in $webauthnRegContext.existingCredentials)
                         <tr>
@@ -75,9 +75,9 @@
                     <div><span>You have no registered keys</span></div>
                 #end
                 <br />
-                <form id="doneButtonForm" action="$flowExecutionUrl" method="post">
+                <form id="finish_button_form" action="$flowExecutionUrl" method="post">
                     #parse("csrf/csrf.vm")
-                    <button id="doneButton" type="submit" name="_eventId_proceed">Done</button>
+                    <button id="finish_button" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.register.finish", "Done")</button>
                 </form>
             </div>
 
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
index a7099dc..1a2784d 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
@@ -53,7 +53,7 @@
                     </div>
                  </form>
   
-            </section>
+             </section>
         </main>
         <footer class="footer">
             <div class="cc">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm
new file mode 100644
index 0000000..c3965ff
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm
@@ -0,0 +1,124 @@
+##
+## Velocity Template for collection of username for Duo Passwordless use
+##
+## 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
+## passwordlessContext - context with Duo username and enrollment status
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+## cspNonce - Calculates secure nonces (call generateIdentifier)
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.profile.context.RelyingPartyContext'))
+#set ($eventCtx = $profileRequestContext.getSubcontext('org.opensaml.profile.context.EventContext'))
+#if ($eventCtx)
+#set ($eventId = $eventCtx.getEvent())
+#end
+#set ($nonce = $cspNonce.generateIdentifier())
+$response.addHeader("Content-Security-Policy", "script-src-elem 'nonce-$nonce'")
+#set ($onClick = "document.forms.password.j_username.value = document.forms.passwordless.j_username.value")
+$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onClick)'")
+##
+<!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 onLoad="$onLoad">
+        <main class="main">
+            <header>
+                <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+                
+                #set ($serviceName = $rpUIContext.serviceName)
+                #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+                    <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+                #end
+            </header>
+            
+            <section>
+                #*
+                //
+                //    SP Description & Logo (optional)
+                //    These idpui lines will display added information (if available
+                //    in the metadata) about the Service Provider (SP) that requested
+                //    authentication. These idpui lines are "active" in this example
+                //    (not commented out) - this extra SP info will be displayed.
+                //    Remove or comment out these lines to stop the display of the
+                //    added SP information.
+                //
+                *#
+                #set ($logo = $rpUIContext.getLogo())
+                #if ($logo)
+                    <img class="service-logo" src= "$encoder.encodeForHTMLAttribute($logo)" alt="$encoder.encodeForHTMLAttribute($serviceName)">
+                #end
+                #set ($desc = $rpUIContext.getServiceDescription())
+                #if ($desc)
+                    <p>$encoder.encodeForHTML($desc)</p>
+                #end
+
+                <blockquote>#springMessageText("idp.duo.passwordless.explain", "If you've enrolled a passkey or device/token for passwordless login,
+                please enter your username below and press the corresponding button. To bypass this option, just press the alternate button
+                to perform a traditional login.")</blockquote>
+
+
+                
+                #if ($eventId == "RequestUnsupported")
+                    <p class="output-message output--error">$encoder.encodeForHTML("#springMessageText('idp.duo.passwordless.unsupported', 'You have not enrolled a qualifying device for Passwordless use.')")</p>
+                #end
+                        
+                <form name="username-entry" action="$flowExecutionUrl" method="post">
+                    #parse("csrf/csrf.vm")
+                    
+                    <label for="username">#springMessageText("idp.login.username", "Username")</label>
+                    <input id="j_username" name="j_username" type="text"
+                        value="#if($username)$encoder.encodeForHTML($username)#end" />
+                        
+                    <input type="checkbox" name="donotcache" value="1" id="donotcache" />
+                    <label for="donotcache">#springMessageText("idp.login.donotcache", "Don't Remember Login")</label>
+
+                    <input id="_shib_idp_revokeConsent" type="checkbox" name="_shib_idp_revokeConsent" value="true" />
+                    <label for="_shib_idp_revokeConsent">#springMessageText("idp.attribute-release.revoke", "Clear prior granting of permission for release of your information to this service.")</label>
+    
+                    <div class="grid">
+                        <div class="grid-item">
+                            <button type="submit" name="_eventId_proceed"
+                                >#springMessageText("idp.webauthn.passwordless.proceed", "Login with Passkey or Device")</button>
+                        </div>
+                    </div>
+                </form>
+    
+                <ul>
+                    <li><a href="#springMessageText('idp.duo.enrollment.url', 'idp/profile/admin/webauthn-registration')">#springMessageText("idp.duo.enrollment", "Enroll New Devices")</a></li>
+                    <li><a href="#springMessageText('idp.url.helpdesk', '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+                </ul>
+            </section>
+        </main>
+        <footer class="footer">
+            <div class="cc">
+                <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+            </div>
+        </footer>
+        
+        <script #if ($nonce)nonce="$nonce"#end>
+        <!--
+        const input = document.getElementById('j_username');
+        const end = input.value.length;
+        input.setSelectionRange(end, end);
+        input.focus();
+        // -->
+        </script>
+        
+     </body>
+</html>
\ No newline at end of file

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


More information about the commits mailing list