[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-12 - Add an access control predicate for guarding against username changes

Phil Smart philip.smart at jisc.ac.uk
Fri May 10 11:27:17 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=fb8227f3bee9bfddb8e41ceaeed1b1521ca3d283

The following commit(s) were added to refs/heads/main by this push:
     new fb8227f  JWEBAUTHN-12 - Add an access control predicate for guarding against username changes
fb8227f is described below

commit fb8227f3bee9bfddb8e41ceaeed1b1521ca3d283
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri May 10 12:27:14 2024 +0100

    JWEBAUTHN-12 - Add an access control predicate for guarding against
    username changes
    
     - Added a plugable access control guard that checks the principal name
    in the subject context is the same as the username in the registration
    context.
      - exposed options to transform the username when entered into the
    registration page or the passwordless page.
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-12
---
 .../client/WebAuthnAuthenticationClient.java       |   2 +
 .../impl/AllowCurrentUserAccessPredicate.java      | 171 +++++++++++++++++++++
 .../impl/ExtractUsernameFromRegistrationForm.java} |  87 ++++-------
 .../impl/PopulateWebAuthnRegistrationContext.java  |  45 +++++-
 .../impl/YubicoWebAuthnAuthenticationClient.java   |   2 +-
 .../impl/AbstractWebAuthnExtractionAction.java     | 166 ++++++++++++++++++++
 .../authn/webauthn/impl/BaseWebAuthnAction.java    |   5 +-
 .../webauthn/impl/ExtractUsernameFromForm.java     |  21 ++-
 .../LookupRegisteredCredentialsFromUserHandle.java |   2 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   2 +-
 .../webauthn-registration-beans.xml                |  22 ++-
 .../webauthn-registration-flow.xml                 |   1 +
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |   7 +-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |   5 +-
 .../authn/webauthn/conf/authn/webauthn.properties  |  12 +-
 .../plugin/authn/webauthn/views/webauthn-authn.vm  |   4 +
 .../webauthn/views/webauthn-register-username.vm   |   3 -
 .../impl/AllowCurrentUserAccessPredicateTest.java  |  98 ++++++++++++
 18 files changed, 554 insertions(+), 101 deletions(-)

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 35adacb..54cd9c4 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
@@ -26,6 +26,8 @@ import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthentication
  * <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>
  * 
+ * TODO: should be go all in for Yubico data model here, otherwise we need our own variant of PublicKeyCredential etc.
+ * 
  * <p>Implementations must be thread-safe</p>
  */
 @ThreadSafe
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java
new file mode 100644
index 0000000..accffa0
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An access control predicate that should implement comparison logic to allow access only to authenticated users who 
+ * initiate the WebAuthn registration process. If the authenticated user is not the same as the user who started the 
+ * registration process (as determined by the initial username collection step), access should be denied. 
+ * 
+ * <p>It is important to prevent users from changing their username between the registration and authentication 
+ * flows. If this is allowed, it would create a loophole in the WebAuthn authentication process, which could 
+ * allow a user to bypass it. For instance, a user who starts the registration of a new WebAuthn credential, without any 
+ * previous WebAuthn credentials, could then login with an inferior authentication mechanism as another user who 
+ * should only be using WebAuthn.</p>
+ * 
+ * <p>More complicated comparison predicates might be needed if either the principal in the subject context and or the
+ * username in the registration context have been transformed into different forms but are still logically 
+ * equivalent.</p> 
+ */
+public class AllowCurrentUserAccessPredicate extends AbstractIdentifiableInitializableComponent 
+        implements Predicate<ProfileRequestContext> {
+    
+    /** Class logger. */
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AllowCurrentUserAccessPredicate.class);
+    
+    /** Lookup strategy to locate the webauthn registration context. */
+    @Nonnull 
+    private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
+    
+    /** Strategy function to lookup SubjectContext. */
+    @Nonnull private Function<ProfileRequestContext,SubjectContext> subjectContextLookupStrategy;
+    
+    /** The predicate that performs the comparison. */
+    @Nonnull private BiPredicate<ProfileRequestContext, Pair<String,String>> comparisonPredicate; 
+
+    /** Constructor.*/
+    public AllowCurrentUserAccessPredicate() {
+        webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+        subjectContextLookupStrategy = new ChildContextLookup<>(SubjectContext.class);
+        comparisonPredicate = new DefaultCurrentUserComparisonPredicate();
+    }
+
+    /**
+     * Set the lookup strategy to use to locate the {@link SubjectContext}.
+     * 
+     * @param strategy lookup function to use
+     */
+    public void setSubjectContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SubjectContext> strategy) {
+        checkSetterPreconditions();
+        subjectContextLookupStrategy = Constraint.isNotNull(strategy, "SubjectContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the predicate that performs the comparison between the principal name from the subject context (as the first 
+     * item in the pair) with the username in the registration context (as the second item in the pair). The profile
+     * request context adds additional information if required.
+     * 
+     * @param predicate The comparison predicate to set.
+     */
+    public void setComparisonPredicate(
+            @Nonnull final BiPredicate<ProfileRequestContext, Pair<String,String>> predicate) {
+        checkSetterPreconditions();
+        comparisonPredicate = Constraint.isNotNull(predicate, "ComparisonPredicate can not be null");
+    }
+    
+    /**
+     * Set the WebAuthn registration context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setWebauthnRegistrationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
+        checkSetterPreconditions();
+
+        webauthnRegistrationContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+    }
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(final ProfileRequestContext profileRequestContext) {
+        
+        final WebAuthnRegistrationContext regContext = 
+                webauthnRegistrationContextLookupStrategy.apply(profileRequestContext);        
+        final String usernameFromRegistrationContext = regContext.getUsername();
+        
+        final SubjectContext subjectContext = subjectContextLookupStrategy.apply(profileRequestContext);
+        if (subjectContext == null) {
+            log.debug("{}: No subject context found, access requires authentication.", getId());
+            return false;
+        }
+        final String usernameFromSubjectContext = subjectContext.getPrincipalName();
+        
+        return comparisonPredicate.test(profileRequestContext, 
+                new Pair<>(usernameFromSubjectContext, usernameFromRegistrationContext));
+
+    }
+    
+    /** Default comparison predicate which just compares the equality of the input strings.*/
+    public static class DefaultCurrentUserComparisonPredicate 
+                            implements BiPredicate<ProfileRequestContext, Pair<String,String>> {
+        
+        /** Class logger. */
+        @Nonnull @NotEmpty 
+        private final Logger log = LoggerFactory.getLogger(DefaultCurrentUserComparisonPredicate.class);
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean test(@Nullable final ProfileRequestContext profileRequestContext, 
+                @Nullable final Pair<String,String> usernamePair) {
+            
+            if (profileRequestContext == null || usernamePair == null) {
+                log.debug("Required context and username information not found, denying access");
+                return false;
+            }
+
+            final String usernameFromSubjectContext = usernamePair.getFirst();
+            final String usernameFromRegistrationContext = usernamePair.getSecond();
+           
+            if (usernameFromRegistrationContext == null) {
+                log.debug("No username in registration context, granting access");
+                return true;
+            }
+            
+            if (usernameFromSubjectContext == null) {
+                log.debug("No username in subject context, access requires authentication");
+                return false;
+            }
+            final boolean match = usernameFromSubjectContext.equals(usernameFromRegistrationContext);
+            log.debug("Username in registration context '{}' {} with the authenticated principal '{}'", 
+                    usernameFromRegistrationContext, match ? "matched" : "did not match",
+                            usernameFromSubjectContext);
+            return match;
+        }
+        
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
similarity index 53%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
index 9f49786..b3ac5e1 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import java.util.function.Function;
 
@@ -25,10 +25,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 import jakarta.servlet.http.HttpServletRequest;
-import net.shibboleth.idp.authn.AbstractExtractionAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnExtractionAction;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
@@ -36,56 +35,50 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.StringSupport;
 
 /**
- * An action to populate a username into the {@link WebAuthnAuthenticationContext}.
- * 
- * TODO FINISH. Really maybe should be similar to CheckPasswordlessEnrollment from Duo
+ * An action to populate a username into the {@link BaseWebAuthnContext}.
  * 
  * @event {@link AuthnEventIds#UNKNOWN_USERNAME}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>WebAuthnAuthenticationContext.getUsername() == null</pre>
+ * @pre <pre>ProfileRequestContext.getSubcontext(BaseWebAuthnContext.class) != null</pre>
+ * @post <pre>BaseWebAuthnContext.getUsername() == null</pre>
  */
-public class ExtractUsernameFromForm extends AbstractExtractionAction {
+public class ExtractUsernameFromRegistrationForm extends AbstractWebAuthnExtractionAction { 
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromForm.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromRegistrationForm.class);
     
-    /** Strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on. */
-    @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webAuthnContextLookupStrategy;
+    /** Strategy used to locate the {@link BaseWebAuthnContext} to operate on. */
+    @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnContextLookupStrategy;
 
     /** Form parameter name to carry username. */
     @Nonnull @NotEmpty private String usernameFieldName;
-
-    /** Parameter name for SSO bypass. */
-    @Nonnull @NotEmpty private String ssoBypassFieldName;
     
     /** Context to operate on. */
-    @NonnullBeforeExec private WebAuthnAuthenticationContext webAuthnContext;
+    @NonnullBeforeExec private BaseWebAuthnContext webAuthnContext;
     
     /** Constructor.*/
-    public ExtractUsernameFromForm() {            
+    public ExtractUsernameFromRegistrationForm() {            
         usernameFieldName = "j_username";
-        ssoBypassFieldName = "donotcache";
 
         webAuthnContextLookupStrategy =
-                new ChildContextLookup<>(WebAuthnAuthenticationContext.class).compose(
-                        new ChildContextLookup<>(AuthenticationContext.class));
+                new ChildContextLookup<>(BaseWebAuthnContext.class).compose(
+                        new ChildContextLookup<>(ProfileRequestContext.class));
     }
     
     /**
-    * Set the strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on.
+    * Set the strategy used to locate the {@link BaseWebAuthnContext} to operate on.
     * 
     * @param strategy lookup strategy
     */
    public void setWebAuthnContextLookupStrategy(
-           @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+           @Nonnull final Function<ProfileRequestContext,BaseWebAuthnContext> strategy) {
        checkSetterPreconditions();
 
        webAuthnContextLookupStrategy =
-               Constraint.isNotNull(strategy, "WebAuthnAuthenticationContext lookup strategy cannot be null");
+               Constraint.isNotNull(strategy, "WebAuthnContext lookup strategy cannot be null");
    }
    
     /**
-     * Sets the name of the form field to carry the username.
+     * Sets the name of the form field that carries the username.
      * 
      * @param name field name
      */
@@ -96,25 +89,11 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
                 "Username form field name cannot be null or empty");
     }
 
-    /**
-     * Set the SSO bypass parameter name.
-     * 
-     * @param fieldName the SSO bypass parameter name
-     */
-    public void setSSOBypassFieldName(@Nonnull @NotEmpty final String fieldName) {
-        checkSetterPreconditions();
-        
-        ssoBypassFieldName = Constraint.isNotNull(
-                StringSupport.trimOrNull(fieldName), "SSO Bypass field name cannot be null or empty.");
-    }
-
-    
     /** {@inheritDoc} */
     @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+        if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
         
@@ -128,10 +107,9 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
 
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {        
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {        
 
-        final String username = getUsernameFromForm(profileRequestContext, authenticationContext);
+        final String username = getUsernameFromForm(profileRequestContext);
         if (username == null) {
             log.warn("{} Unable to find username in HTTP request", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
@@ -145,29 +123,18 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
     /**
      * Gets the username from a form submission.
      * 
-     * <p>Also processes do-not-cache instruction.</p>
-     * 
      * @param profileRequestContext profile request context
-     * @param authenticationContext authentication context
      * 
      * @return submitted username, after applying any configured transforms
      */
-    @Nullable private String getUsernameFromForm(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+    @Nullable private String getUsernameFromForm(@Nonnull final ProfileRequestContext profileRequestContext) {
         
         final HttpServletRequest request = getHttpServletRequest();
-        if (request != null) {
-            // FIXME this will not work atm?
-            final String donotcache = request.getParameter(ssoBypassFieldName);
-            if (donotcache != null && "1".equals(donotcache)) {
-                log.debug("{} Recording do-not-cache instruction in authentication context", getLogPrefix());
-                authenticationContext.setResultCacheable(false);
-            } else {
-                authenticationContext.setResultCacheable(true);
-            }            
+        if (request != null) {                            
             return applyTransforms(request.getParameter(usernameFieldName));            
         }        
         return null;
     }
-    
-}
\ No newline at end of file
+        
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
index 911d065..a465a14 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
@@ -16,6 +16,7 @@
 package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
@@ -27,8 +28,9 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -39,6 +41,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
  * @post a {@link WebAuthnRegistrationContext} is created, and the username has been populated.
  */
+//TODO The population actions are similar, can they be generalised?
 public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
     
     /** Class logger. */
@@ -51,16 +54,40 @@ public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
     /** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
     @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
     
+    /** Is the username required? */
+    @Nonnull private Predicate<ProfileRequestContext> usernameRequiredPredicate;
+    
     /** Constructor.*/
     public PopulateWebAuthnRegistrationContext() {
         // Default creates a WebAuthn registration context under the profile request context.
+        //TODO does this makes sense directly under the PRC?
         webAuthnRegistrationContextCreationStrategy =
                 new ChildContextLookup<>(WebAuthnRegistrationContext.class, true);
-        
-        usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+        usernameRequiredPredicate = PredicateSupport.alwaysFalse();
+        usernameLookupStrategy = FunctionSupport.constant(null);
 
     }
     
+    /**
+     * Set a flag to determine if the username is required or not.
+     * 
+     * @param flag is the username required?
+     */
+    public void setUsernameRequired(final boolean flag) {
+        checkSetterPreconditions();
+        usernameRequiredPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+    
+    /**
+     * Set a strategy to determine if the username is required or not.
+     * 
+     * @param predicate the predicate to set.
+     */
+    public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+        checkSetterPreconditions();
+        usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+    }
+    
     /**
      * Set the strategy used to lookup or create the WebAuthn registration context.
      * 
@@ -97,16 +124,20 @@ public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
-        
         final String username = usernameLookupStrategy.apply(profileRequestContext);
-        if (username == null) {
+        if (usernameRequiredPredicate.test(profileRequestContext) && username == null) {   
             log.error("{} Error creating WebAuthnRegistrationContext, no username found", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
-        context.setUsername(username);
+        if (username != null) {
+            context.setUsername(username);
+            log.debug("{} Created WebAuthn registration context for user '{}'",getLogPrefix(), context.getUsername());
+        } else {
+            log.debug("{} Created WebAuthn registration context",getLogPrefix());
+        }
 
-        log.debug("Created WebAuthn registration context for user '{}'", context.getUsername());
+        
     }
     
     
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 2337097..ab92652 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
@@ -171,7 +171,7 @@ public class YubicoWebAuthnAuthenticationClient implements WebAuthnAuthenticatio
                 log.debug("Attempting validation of assumed discoverable credential with userHandle from response '{}'",
                         authenticatorAssertionResponse.getResponse().getUserHandle());
             } else {
-                log.debug("Attempting validation of credential with known username '{}' and userHandle '{}'",
+                log.debug("Attempting validation of credential with username '{}' and userHandle '{}'",
                         username, userId);
             }
 
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java
new file mode 100644
index 0000000..4f34910
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java
@@ -0,0 +1,166 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AbstractExtractionAction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A base class for WebAuthn actions that extract usernames for subsequent use.
+ * 
+ * <p>The base class adds a common mechanism for applying regular expression transforms to
+ * the username prior to being added to the context tree.</p>
+ *
+ * Similar to {@link AbstractExtractionAction}, but without the requirement of it running inside an authentication flow.
+ */
+public class AbstractWebAuthnExtractionAction extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnExtractionAction.class);
+    
+    /** Match patterns and replacement strings to apply. */
+    @Nonnull private List<Pair<Pattern,String>> transforms;
+
+    /** Convert to uppercase prior to transforms? */
+    private boolean uppercase;
+    
+    /** Convert to lowercase prior to transforms? */
+    private boolean lowercase;
+    
+    /** Trim prior to transforms? */
+    private boolean trim;
+    
+    /** Constructor. */
+    public AbstractWebAuthnExtractionAction() {
+        transforms = CollectionSupport.emptyList();
+        
+        uppercase = false;
+        lowercase = false;
+        trim = false;
+    }
+
+    /**
+     * A collection of regular expression and replacement pairs.
+     * 
+     * @param newTransforms collection of replacement transforms
+     */
+    public void setTransforms(@Nullable final Collection<Pair<String, String>> newTransforms) {
+        checkSetterPreconditions();
+        if (newTransforms != null) {
+            transforms = new ArrayList<>();
+            for (final Pair<String,String> p : newTransforms) {
+                final Pattern pattern = Pattern.compile(StringSupport.trimOrNull(p.getFirst()));
+                transforms.add(new Pair<>(pattern, Constraint.isNotNull(
+                        StringSupport.trimOrNull(p.getSecond()), "Replacement expression cannot be null")));
+            }
+        } else {
+            transforms = CollectionSupport.emptyList();
+        }
+    }
+
+    /**
+     * Controls conversion to uppercase prior to applying any transforms.
+     * 
+     * @param flag  uppercase flag
+     */
+    public void setUppercase(final boolean flag) {
+        checkSetterPreconditions();
+        uppercase = flag;
+    }
+
+    /**
+     * Controls conversion to lowercase prior to applying any transforms.
+     * 
+     * @param flag lowercase flag
+     */
+    public void setLowercase(final boolean flag) {
+        checkSetterPreconditions();
+        lowercase = flag;
+    }
+    
+    /**
+     * Controls whitespace trimming prior to applying any transforms.
+     * 
+     * @param flag trim flag
+     */
+    public void setTrim(final boolean flag) {
+        checkSetterPreconditions();
+        trim = flag;
+    }
+    
+    /**
+     * Apply any configured regular expression replacements to an input value and return the result.
+     * 
+     * @param input the input string
+     * 
+     * @return  the result of applying the expressions
+     */
+    @Nullable @NotEmpty protected String applyTransforms(@Nullable final String input) {
+        
+        if (input == null) {
+            return null;
+        }
+        
+        String s = input;
+        
+        if (trim) {
+            log.debug("{} Trimming whitespace of input string '{}'", getLogPrefix(), s);
+            s = s.trim();
+        }
+        
+        if (lowercase) {
+            log.debug("{} Converting input string '{}' to lowercase", getLogPrefix(), s);
+            s = s.toLowerCase();
+        } else if (uppercase) {
+            log.debug("{} Converting input string '{}' to uppercase", getLogPrefix(), s);
+            s = s.toUpperCase();
+        }
+        
+        if (transforms.isEmpty()) {
+            return s;
+        }
+        
+        for (final Pair<Pattern,String> p : transforms) {
+            final Pattern pattern = p.getFirst();
+            if (pattern != null) {
+                final Matcher m = pattern.matcher(s);
+                log.debug("{} Applying replacement expression '{}' against input '{}'", getLogPrefix(),
+                        pattern.pattern(), s);
+                s = m.replaceAll(p.getSecond());
+                log.debug("{} Result of replacement is '{}'", getLogPrefix(), s);
+            }
+        }
+
+        return s;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
index c4be7c8..754df41 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
@@ -23,7 +23,6 @@ import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationCli
 import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 
@@ -33,7 +32,7 @@ import net.shibboleth.shared.logic.Constraint;
 public abstract class BaseWebAuthnAction extends AbstractProfileAction {
     
     /** The WebAuthn client to use.*/
-    @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+    @NonnullAfterInit private WebAuthnAuthenticationClient webAuthnClient;
     
     /** The credential respository to store valid credentials in.*/
     @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
@@ -69,7 +68,7 @@ public abstract class BaseWebAuthnAction extends AbstractProfileAction {
      * 
      * @return the webAuthnClient.
      */
-    @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
+    @NonnullAfterInit protected WebAuthnAuthenticationClient getWebAuthnClient() {
         checkComponentActive();
         return webAuthnClient;
     }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
index 9f49786..36120f7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
@@ -28,7 +28,7 @@ import jakarta.servlet.http.HttpServletRequest;
 import net.shibboleth.idp.authn.AbstractExtractionAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
@@ -36,21 +36,21 @@ import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.primitive.StringSupport;
 
 /**
- * An action to populate a username into the {@link WebAuthnAuthenticationContext}.
+ * An action to populate a username into the {@link BaseWebAuthnContext}.
  * 
  * TODO FINISH. Really maybe should be similar to CheckPasswordlessEnrollment from Duo
  * 
  * @event {@link AuthnEventIds#UNKNOWN_USERNAME}
  * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>WebAuthnAuthenticationContext.getUsername() == null</pre>
+ * @post <pre>BaseWebAuthnContext.getUsername() == null</pre>
  */
 public class ExtractUsernameFromForm extends AbstractExtractionAction {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromForm.class);
     
-    /** Strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on. */
-    @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webAuthnContextLookupStrategy;
+    /** Strategy used to locate the {@link BaseWebAuthnContext} to operate on. */
+    @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnContextLookupStrategy;
 
     /** Form parameter name to carry username. */
     @Nonnull @NotEmpty private String usernameFieldName;
@@ -59,7 +59,7 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
     @Nonnull @NotEmpty private String ssoBypassFieldName;
     
     /** Context to operate on. */
-    @NonnullBeforeExec private WebAuthnAuthenticationContext webAuthnContext;
+    @NonnullBeforeExec private BaseWebAuthnContext webAuthnContext;
     
     /** Constructor.*/
     public ExtractUsernameFromForm() {            
@@ -67,17 +67,17 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
         ssoBypassFieldName = "donotcache";
 
         webAuthnContextLookupStrategy =
-                new ChildContextLookup<>(WebAuthnAuthenticationContext.class).compose(
+                new ChildContextLookup<>(BaseWebAuthnContext.class).compose(
                         new ChildContextLookup<>(AuthenticationContext.class));
     }
     
     /**
-    * Set the strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on.
+    * Set the strategy used to locate the {@link BaseWebAuthnContext} to operate on.
     * 
     * @param strategy lookup strategy
     */
    public void setWebAuthnContextLookupStrategy(
-           @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+           @Nonnull final Function<ProfileRequestContext,BaseWebAuthnContext> strategy) {
        checkSetterPreconditions();
 
        webAuthnContextLookupStrategy =
@@ -85,7 +85,7 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
    }
    
     /**
-     * Sets the name of the form field to carry the username.
+     * Sets the name of the form field that carries the username.
      * 
      * @param name field name
      */
@@ -157,7 +157,6 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
         
         final HttpServletRequest request = getHttpServletRequest();
         if (request != null) {
-            // FIXME this will not work atm?
             final String donotcache = request.getParameter(ssoBypassFieldName);
             if (donotcache != null && "1".equals(donotcache)) {
                 log.debug("{} Recording do-not-cache instruction in authentication context", getLogPrefix());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
index 039c619..3b608c7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
@@ -152,7 +152,7 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
             }
         }
 
-        if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && credentialsFound == false) {
+        if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && !credentialsFound) {
             log.debug("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
             ActionSupport.buildEvent(profileRequestContext, noCredentialsEventId);
             return;
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 21d5d4c..1c37da8 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -47,7 +47,7 @@
     <bean parent="shibboleth.AdminFlow"
             c:id="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential"
             p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
-            p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByAnyone}"
+            p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByCurrentUser}"
             p:nonBrowserSupported="false"
             p:authenticated="%{idp.authn.webauthn.admin.registration.authenticated:true}"
             p:resolveAttributes="%{idp.authn.webauthn.admin.registration.resolveAttributes:true}">
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 93e9170..e6badd1 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
@@ -29,21 +29,27 @@
     <!-- Flow beans -->
     
     <bean id="PopulateInitialWebAuthnRegistrationContext" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
-         <property name="usernameLookupStrategy">
-            <bean id="usernameFromHttpRequest" scope="prototype"
-                class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromHttpRequest"
-                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
-        </property>
-    </bean>
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext"
+        p:usernameRequired="false">
+    </bean>    
+        
+    <bean id="ExtractUsernameFromForm" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractUsernameFromRegistrationForm"
+        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+        p:uppercase="%{idp.authn.webauthn.registration.username.uppercase:false}"
+        p:lowercase="%{idp.authn.webauthn.registration.username.lowercase:false}"
+        p:trim="%{idp.authn.webauthn.registration.username.trime:false}"
+        p:transforms="#{getObject('shibboleth.authn.webauthn.registration.UsernameTransformations')}"/>
     
     <!-- Important that this gets the username from the subject context, not the initial context that is created -->
     <bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext"
+        p:usernameRequired="true"
         p:usernameLookupStrategy="#{getObject('%{idp.authn.webauthn.registration.usernameLookupStrategy:shibboleth.authn.webauthn.RegistrationUsernameLookupStrategy}')}">
     </bean>
         
-    <!-- Default username comes from previous c14n or session. -->
+    <!-- Default username comes from the subject context. -->
     <bean id="shibboleth.authn.webauthn.RegistrationUsernameLookupStrategy"
         class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromSubjectContext" />
     
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 d193b7f..1afa84e 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
@@ -32,6 +32,7 @@
     <!-- TODO if we want to do this, we would want this to populate a different context -->
     <action-state id="ExtractUsernameAndPopulateContext">
         <evaluate expression="PopulateInitialWebAuthnRegistrationContext"/>
+        <evaluate expression="ExtractUsernameFromForm"/>
         <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="'proceed'" />
         
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 7079a9d..5e3c719 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
@@ -75,7 +75,12 @@
         
     <bean id="ExtractUsernameFromForm" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractUsernameFromForm"
-        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+        p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+        p:uppercase="%{idp.authn.webauthn.passwordless.username.uppercase:false}"
+        p:lowercase="%{idp.authn.webauthn.passwordless.username.lowercase:false}"
+        p:trim="%{idp.authn.webauthn.passwordless.username.trime:false}"
+        p:transforms="#{getObject('shibboleth.authn.webauthn.passwordless.UsernameTransformations')}"/>
     
     <bean id="EnsureAllowedCredentialsIsEmpty" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
     class="net.shibboleth.idp.plugin.authn.webauthn.impl.EnsureAllowedCredentialsIsEmpty"/>
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 0c69d56..510d089 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
@@ -31,10 +31,7 @@
             then="CollectUsernameView" 
             else="PasswordlessLoginProceed" />        
     </decision-state>
-    
-        
-    <!-- need decision state, we might have already collected username, so might not need it again e.g. when part of a registration flow -->
-        
+       
     <view-state id="CollectUsernameView" view="webauthn/webauthn-authn-username">
         <on-render>
             <evaluate expression="environment" result="viewScope.environment" />
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 406450c..ad7bbed 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
@@ -39,7 +39,7 @@ idp.authn.webauthn.supportedPrincipals = \
 # Does the registration page require authentication. Should always be true.
 #idp.authn.webauthn.admin.registration.authenticate = true
 # Access policy for the registration flow
-#idp.authn.webauthn.admin.registration.accessPolicy = AccessByAnyone
+#idp.authn.webauthn.admin.registration.accessPolicy = AccessByCurrentUser
 # Restrict the usable authentication flows
 #idp.authn.webauthn.admin.registration.authenticationFlows =
 # Should user.id and display name be resolve from the attribute resolver?
@@ -88,6 +88,11 @@ idp.authn.webauthn.supportedPrincipals = \
 # State the preference of the IdP during registration to receive an authenticator attestation. One-of 'none', 'indirect', 'direct', or 'enterprise'.
 #idp.authn.webauthn.registration.attestationConveyancePreference = none
 
+# Basic transformations that should be applied to the username that is initially collected in the registration flow
+#idp.authn.webauthn.registration.username.uppercase = false
+#idp.authn.webauthn.registration.username.lowercase = false
+#idp.authn.webauthn.registration.username.trime = false
+
 
 #### Administrator properties
 
@@ -124,3 +129,8 @@ idp.authn.webauthn.supportedPrincipals = \
 # to any registered credentials? 
 #idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle = false
 #idp.authn.webauthn.userHandleNoRegisteredCredentialsEventId = NoCredentialsRegisteredForUserHandle
+
+# Basic transformations that should be applied to the username that is collected as part of the usernameless flow
+#idp.authn.webauthn.passwordless.username.uppercase = false
+#idp.authn.webauthn.passwordless.username.lowercase = false
+#idp.authn.webauthn.passwordless.username.trime = false
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 efa7284..7d6ad5b 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
@@ -145,6 +145,10 @@
             <div id="unsupportedDiv" class="hidden">
                 #springMessageText("idp.webauthn.authn.unsupported", "Your browser is not WebAuthn compatible")
             </div>
+             <ul>
+                    <li><a href="#springMessageText('idp.webauthn.enrollment.url', '/idp/profile/admin/webauthn-registration')">#springMessageText("idp.webauthn.enrollment", "Enroll New Devices")</a></li>
+                    <li><a href="#springMessageText('idp.url.helpdesk', '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+                </ul>
         </section>
         <footer>
             <div class="container container-footer">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
index cd1e9cf..d3310e4 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
@@ -50,9 +50,6 @@ $response.addHeader("Content-Security-Policy", "script-src-elem 'nonce-$nonce'")
                     <label for="username">#springMessageText("idp.login.username", "Username")</label>
                     <input id="j_username" name="j_username" type="text" required
                         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>
 
                     <div class="grid">
                         <div class="grid-item">
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java
new file mode 100644
index 0000000..8d5023b
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link AllowCurrentUserAccessPredicate}
+ */
+public class AllowCurrentUserAccessPredicateTest extends AbstractWebAuthnTest {
+    
+    private AllowCurrentUserAccessPredicate predicate;
+    
+    private WebAuthnRegistrationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();  
+        
+        predicate = new AllowCurrentUserAccessPredicate();
+        predicate.setId("CurrentUserAccessPredicate");
+    }
+    
+    @Test
+    public void testAccessGranted() throws ComponentInitializationException {
+        prc.addSubcontext(new SubjectContext().setPrincipalName(USERNAME));
+        predicate.initialize();
+        context.setUsername(USERNAME);
+        
+        final boolean access = predicate.test(prc);
+        assertTrue(access);
+    }
+    
+    @Test
+    public void testAccessDenied_DifferentUsername() throws ComponentInitializationException {
+        prc.addSubcontext(new SubjectContext().setPrincipalName("differet"));
+        predicate.initialize();
+        context.setUsername(USERNAME);
+        
+        final boolean access = predicate.test(prc);
+        assertFalse(access);
+    }
+    
+    @Test
+    public void testAccessDenied_NoPrincipalName() throws ComponentInitializationException {
+        prc.addSubcontext(new SubjectContext());
+        predicate.initialize();
+        context.setUsername(USERNAME);
+        
+        final boolean access = predicate.test(prc);
+        assertFalse(access);
+    }
+    
+    @Test
+    public void testAccessDenied_NoSubjectContext() throws ComponentInitializationException {
+        predicate.initialize();
+        context.setUsername(USERNAME);
+        
+        final boolean access = predicate.test(prc);
+        assertFalse(access);
+    }
+    
+    @Test
+    public void testAccessGranted_NoUsernameInRegistrationContext() throws ComponentInitializationException {
+        prc.addSubcontext(new SubjectContext().setPrincipalName(USERNAME));
+        //context.setUsername(USERNAME);
+        
+        predicate.initialize();
+        
+        final boolean access = predicate.test(prc);
+        assertTrue(access);
+    }
+
+
+}

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


More information about the commits mailing list