[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle

Phil Smart philip.smart at jisc.ac.uk
Fri Feb 14 09:18:02 UTC 2025


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-idp-plugin-webauthn.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-webauthn.git;a=commit;h=4b77dcf3bbcd7960259bcd7ed414169f465b3a8d

The following commit(s) were added to refs/heads/main by this push:
     new 4b77dcf  JWEBAUTHN-37 - Lookup user credentials from user handle expects user handle
4b77dcf is described below

commit 4b77dcf3bbcd7960259bcd7ed414169f465b3a8d
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Feb 14 09:17:58 2025 +0000

    JWEBAUTHN-37 - Lookup user credentials from user handle expects user
    handle
    
     - The lookup action functions correctly if the user is already
    identified and has credentials in the context
     - Also, streamline the actions (revert the previous commit to seperate
    them).
     - Add more tests
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-37
---
 .../impl/CheckUserHandleAgainstUsername.java       | 118 ---------------------
 .../CheckUserHandleExistsIfNoAllowCredentials.java |  22 +++-
 .../LookupRegisteredCredentialsFromUserHandle.java |  53 +++++++--
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |   5 -
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |  49 +--------
 .../authn/webauthn/flow/TestPasswordlessFlow.java  |  83 ++++++++++++++-
 ...dlessFlowTriggerOnNoUserHandleCredentials.java} |  95 +++++------------
 .../authn/webauthn/flow/TestSecondFactorFlow.java  |  50 +++++++++
 .../flow/TestSecondFactorFlowWithFilterPolicy.java |   2 +-
 .../flow/TestUsernameslessFlowWithPolicy.java      |   4 +-
 ...NoCredentialsApplicationContextInitializer.java |  50 +++++++++
 ...kupRegisteredCredentialsFromUserHandleTest.java |  74 ++++++++++++-
 12 files changed, 352 insertions(+), 253 deletions(-)

diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java
deleted file mode 100644
index e05e00e..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleAgainstUsername.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.Optional;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
-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.storage.WebAuthnCredentialRepository;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.primitive.LoggerFactory;
-
-/**
- * An action that checks if the userHandle in the authentication assertion refers to the same user as in the WebAuthn 
- * context. Noting, under some circumstances e.g. a passwordless flow where the user and their credentials have 
- * already been identified, the authenticator does not need to provide a userHandle, in which case this action does 
- * nothing.
- * 
- * 
- * @event {EventIds#INVALID_PROFILE_CTX}
- * @event {AuthnEventIds#INVALID_CREDENTIALS}
- * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post a non-proceed event is signalled if the username found for the userHandle does not match that in the context
- * 
- * @since 1.1.0
- */
-public class CheckUserHandleAgainstUsername extends AbstractWebAuthnAction<WebAuthnAuthenticationContext> {
-    
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(CheckUserHandleAgainstUsername.class);
-    
-    /** The credential repository to use.*/
-    @NonnullAfterInit private WebAuthnCredentialRepository repository;
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        repository = getCredentialRepository();
-        if (repository == null) {
-            throw new ComponentInitializationException("Credential repository can not be null");
-        }
-    }
-    
-    /**
-     * Constructor.
-     */
-    protected CheckUserHandleAgainstUsername() {
-        super(new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
-                compose(new ChildContextLookup<>(AuthenticationContext.class)));
-    }
-    
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
-        
-       
-        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion = 
-                context.getPublicKeyCredentialAssertionResponse();
-        if (assertion == null) {
-            log.error("{} Unable to find Assertion in WebAuthn authentication context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return;
-        }
-        final String username = context.getUsername();
-        
-        final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
-        if (userHandle.isEmpty()) {
-            log.trace("{} User could not be found, the authenticator did not supply a userHandle", getLogPrefix());
-        } else {
-            final Optional<String> potentialUsername = repository.getUsernameForUserHandle(userHandle.get());
-            
-            if (potentialUsername.isEmpty()) {
-                log.trace("{} User could not be found from the supplied userHandle, no registered credentials", 
-                        getLogPrefix());
-            } else if (username != null && !potentialUsername.get().equals(username)){
-                 
-                log.trace("{} Username '{}' found from the userHandle was not the same as in the authentication "
-                        + "context '{}'",getLogPrefix(), potentialUsername.get(), username);
-                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
-                return;                               
-                
-            } else {
-                log.trace("{} Username '{}' found from the userHandle matched that in the authentication context",
-                        getLogPrefix(), potentialUsername.get());
-            }
-        }
-             
-    } 
-
-
-}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
index 2bb2530..676185f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CheckUserHandleExistsIfNoAllowCredentials.java
@@ -42,6 +42,8 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * in the request was empty. If they are empty, the userHandle must be returned, if they are not empty the userHandle
  * may be returned in the response. 
  * 
+ * <p>This generally protects against bad behaviour from authenticators for the rest of the flow.</p>
+ * 
  * @event {EventIds#INVALID_PROFILE_CTX}
  * @event {AuthnEventIds#INVALID_CREDENTIALS}
  * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
@@ -95,20 +97,30 @@ public class CheckUserHandleExistsIfNoAllowCredentials extends AbstractWebAuthnA
             @Nonnull final WebAuthnAuthenticationContext context) {
         
         final Optional<List<PublicKeyCredentialDescriptor>> allowCredentials = requestOptions.getAllowCredentials();
-        final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
+        final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();        
+        final boolean allowedCredentialsRequested = 
+                allowCredentials.isPresent() && !allowCredentials.get().isEmpty();
+        
+        // Guard to catch a condition which should not occur.
+        if (allowedCredentialsRequested  && context.getExistingCredentials().isEmpty()) {
+            log.error("{} Credentials were requested but none exist in the context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
+
         
         if (allowCredentials.isEmpty() || allowCredentials.get() == null || allowCredentials.get().isEmpty()) {            
             if (userHandle.isEmpty()) {
-                log.debug("{} Allow credentials are empty and the userHandle was not returned in the response, the "
-                        + "userHandle is required in this case", getLogPrefix());
+                log.debug("{} Allow credentials is empty and the userHandle was not returned in the response, the "
+                        + "userHandle is required", getLogPrefix());
                 ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
                 return;
             } else {
-                log.trace("{} Allow credentials are empty and the userHandle was returned in the response", 
+                log.trace("{} Allow credentials is empty and the userHandle was returned in the response", 
                         getLogPrefix());
             }
         } else {
-            log.trace("{} Allow credentials are not empty and the optional userHandle was {}", 
+            log.trace("{} Allow credentials is not empty and the (optional) userHandle was {}", 
                     getLogPrefix(), userHandle.isPresent() ? "returned in the response" :
                             "not returned in the response");
         }
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 e6fde32..5eae2ad 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
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.Optional;
 import java.util.function.Predicate;
 
@@ -30,6 +31,8 @@ import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
 import com.yubico.webauthn.data.ByteArray;
 import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -74,6 +77,12 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
     /** The credential repository to use.*/
     @NonnullAfterInit private WebAuthnCredentialRepository repository;
     
+    /** The stashed assertion from the context.*/
+    private PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion;
+
+    /** The stashed request options from the context.*/
+    private PublicKeyCredentialRequestOptions requestOptions;
+    
     
     /** Constructor.  */
     public LookupRegisteredCredentialsFromUserHandle() {
@@ -124,21 +133,51 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
         checkSetterPreconditions();
         noCredentialsEventId = Constraint.isNotEmpty(eventId, "NoCredentialsEventId can not be null or empty");
     }
-
+    
     /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+    @Override
+    protected boolean doPreExecute(final ProfileRequestContext profileRequestContext, 
+            final WebAuthnAuthenticationContext context) {
+        
+        if (!super.doPreExecute(profileRequestContext, context)) {
+            return false;
+        }
         
-        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion = 
-                context.getPublicKeyCredentialAssertionResponse();
+        assertion = context.getPublicKeyCredentialAssertionResponse();
         if (assertion == null) {
             log.error("{} Unable to find Assertion in WebAuthn authentication context", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        requestOptions = context.getPublicKeyCredentialRequestOptions();
+        if (requestOptions == null) {
+            log.error("{} Unable to find credential request options in WebAuthn authentication context", 
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+        final String username = context.getUsername(); 
+        final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
+        
+        // If request options specified allowedCredentials and the context already contains credentials we
+        // do not need to look anything up
+        final Optional<List<PublicKeyCredentialDescriptor>> allowedCredentials = requestOptions.getAllowCredentials();
+        final boolean allowedCredentialsRequested = 
+                allowedCredentials.isPresent() && !allowedCredentials.get().isEmpty();
+        
+        if (allowedCredentialsRequested && !context.getExistingCredentials().isEmpty()) {
+            log.trace("{} User already has credentials, nothing to lookup", getLogPrefix());
             return;
         } 
-        final String username = context.getUsername();
         
-        final Optional<ByteArray> userHandle = assertion.getResponse().getUserHandle();
         boolean credentialsFound = false;
         if (userHandle.isEmpty()) {
             log.debug("{} User could not be found, the authenticator did not supply a userHandle", getLogPrefix());
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 e17202f..88b8c4b 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
@@ -161,11 +161,6 @@
 
     <bean id="CheckUserHandleExistsIfNoAllowCredentials" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckUserHandleExistsIfNoAllowCredentials"/> 
-        
-    <bean id="CheckUserHandleAgainstUsername" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckUserHandleAgainstUsername"
-        p:credentialRepository="#{getObject('shibboleth.authn.WebAuthn.CredentialRepository') ?: getObject('shibboleth.authn.WebAuthn.DefaultCredentialRepository')}"
-        />
     
     <bean id="CheckCredentialPolicy" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.CheckCredentialPolicy"
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 e316724..1c330f9 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
@@ -143,54 +143,13 @@
     <action-state id="ExtractPublicKeyCredentialAssertion">
         <evaluate expression="ExtractPublicKeyCredentialAssertionFromFormRequest"/>
         <evaluate expression="'proceed'" />        
-        <transition on="proceed" to="BranchOnAuthenticationMode" />
+        <transition on="proceed" to="ValidatePublicKeyCredential" />
     </action-state>
     
-    <!-- Determine which authentication mode we are using so different actions can apply -->
-    <decision-state id="BranchOnAuthenticationMode">
-        <if test="IsSecondFactorAuthenticationMode.test(opensamlProfileRequestContext)"
-            then="SecondFactorAssertion" 
-            else="BranchOnPasswordlessOrUsernamelessMode" />        
-    </decision-state>
-    
-    <decision-state id="BranchOnPasswordlessOrUsernamelessMode">
-        <if test="IsUsernamelessAuthenticationMode.test(opensamlProfileRequestContext)"
-            then="UsernamelessAssertion" 
-            else="BranchOnPasswordlessMode" />        
-    </decision-state>
-    
-    <!-- By this point, if we are not operating in any mode, this represents a fundamental problem with the flow -->
-    <decision-state id="BranchOnPasswordlessMode">
-        <if test="IsPasswordlessAuthenticationMode.test(opensamlProfileRequestContext)"
-            then="PasswordlessAssertion" 
-            else="RuntimeException" />        
-    </decision-state>
-
-    <action-state id="UsernamelessAssertion">
+    <action-state id="ValidatePublicKeyCredential">
+        <!-- catch no userHandle in assertion when empty allowCredentials here, to protected against bad authenticators -->
         <evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
-        <evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>
-        <evaluate expression="'proceed'" />
-        
-        <transition on="proceed" to="ValidatePublicKeyCredential" />    
-    </action-state>
-    
-    <action-state id="SecondFactorAssertion">  
-        <evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
-        <!-- TODO The WebAuthn client should also check the username it is given matches to the userHandle in the assertion response. So this is pre-emptive.-->
-        <evaluate expression="CheckUserHandleAgainstUsername"/>  
-        <evaluate expression="'proceed'" />        
-        <transition on="proceed" to="ValidatePublicKeyCredential" />    
-    </action-state>
-    
-    <action-state id="PasswordlessAssertion">
-        <evaluate expression="CheckUserHandleExistsIfNoAllowCredentials"/>
-        <!-- TODO The WebAuthn client should also check the username it is given matches to the userHandle in the assertion response. So this is pre-emptive.-->
-        <evaluate expression="CheckUserHandleAgainstUsername"/>
-        <evaluate expression="'proceed'" />        
-        <transition on="proceed" to="ValidatePublicKeyCredential" />    
-    </action-state>
-     
-    <action-state id="ValidatePublicKeyCredential">      
+        <evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>     
         <evaluate expression="CheckCredentialPolicy"/>
         <evaluate expression="ValidateWebAuthnAssertion"/>
         <evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
index 7844132..de456be 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlow.java
@@ -147,6 +147,7 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
      * 
      * @throws Exception on error
      */
+    @SuppressWarnings("null")
     @Test
     public void testPasswordlessFlow_UsernameHasNoCredentials() throws Exception {
         //Register a credential for use.
@@ -167,8 +168,10 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
         setHttpFormRequest("POST", Map.of("j_username", USERNAME));
         externalContext.setEventId("proceed");
         
-        result.getSecond().setCurrentState("CollectUsernameView");
-        result.getSecond().resume(externalContext);
+        final FlowExecutionImpl flowExec = result.getSecond();
+        assert flowExec != null;
+        flowExec.setCurrentState("CollectUsernameView");
+        flowExec.resume(externalContext);
         
         assertFlowExecutionActive(result.getSecond());
         assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
@@ -275,6 +278,82 @@ public class TestPasswordlessFlow extends AbstractWebAuthnFlowTest{
         
     }
     
+    /**
+     * Checks:
+     * 
+     * <ol>
+     * <li>Username entered has 1 credentials</li>
+     * <li>Credential request options contains an allowCredentials with 1 credential</li>
+     * <li>User selects a different credential from the provider, authenticator signs challenge</li>
+     * <li>UserHandle is not returned but user already has existing credentials</li>
+     * <li>fail, on WebAuthn validation step 5, unrequested credential.</li>
+     * </ol>
+     * 
+     * 
+     * @throws Exception on error
+     */
+    @Test
+    @SuppressWarnings("null")
+    public void testPasswordlessFlow_NoUserHandle_CredentialDoesNotMatchToUserinContext() throws Exception {
+        //Register a credential for use.
+        final CredentialRecord registration = 
+                createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+        credentialRepo.addRegistrationByUsername(USERNAME, registration);
+        
+        final CredentialRecord differentRegistration = 
+                createCredentialRegistration("different-user", DISPLAY_NAME, "ZGZ3ZWZ3ZQ==", null);
+        credentialRepo.addRegistrationByUsername("different-user", differentRegistration);
+        
+        final var prc = buildProfileRequestContext(false, false, USERNAME);
+
+        final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
+                addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+        assertFlowExecutionActive(result.getSecond());
+        assertCurrentStateEquals("CollectUsernameView", result.getSecond());
+        
+        // Do next part of flow, populate the username
+        ExternalContextHolder.setExternalContext(externalContext);
+        // Add a username input here, which is different than the user that registered the credential
+        setHttpFormRequest("POST", Map.of("j_username", USERNAME));
+        externalContext.setEventId("proceed");
+        
+        final FlowExecutionImpl flowExec = result.getSecond();
+        assert flowExec != null;
+        flowExec.setCurrentState("CollectUsernameView");
+        flowExec.resume(externalContext);
+        
+        assertFlowExecutionActive(result.getSecond());
+        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+        assertPublicKeyCredentialRequestOptions(prc, true, true);
+        
+        // Do assertion validation half of flow
+        
+        // Get the challenge that was set into the PublicKeyCredentialRequestOptions. 
+        // otherwise we will end up signing a different one, which will provide its own test.
+        final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+        final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+        
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+        assertionResponse = createAssertionReponseFrom(
+                differentRegistration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
+
+        final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+        
+        // Re-set external context to holder
+        ExternalContextHolder.setExternalContext(externalContext);
+        setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME, 
+                assertionResponseJson));
+        externalContext.setEventId("proceed");
+        result.getSecond().setCurrentState("DisplayWebAuthnView");
+        result.getSecond().resume(externalContext);
+        
+        // assert end conditions, no existing result or principal. So this should produce a Username principal
+        assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
+        assertAuthenticationFailureConditions(prc);
+        
+    }
+    
     /**
      * Checks:
      * 
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
similarity index 56%
copy from webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
copy to webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
index 7780cfb..9d7742e 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestPasswordlessFlowTriggerOnNoUserHandleCredentials.java
@@ -31,7 +31,6 @@ import com.yubico.webauthn.data.ByteArray;
 import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 
-import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialAssertionFromFormRequest;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRecord;
@@ -39,7 +38,7 @@ import net.shibboleth.shared.collection.Pair;
 
 
 /**
- * Flow tests for the usernameless flow with active policies.
+ * Flow tests for the passwordless flow.
  */
 @ContextConfiguration(
         locations = {
@@ -48,10 +47,10 @@ import net.shibboleth.shared.collection.Pair;
                 "classpath*:/net/shibboleth/idp/plugin/authn/webauthn/test-beans.xml"},
         initializers = {
                 TestWebAuthnEnvironmentApplicationContextInitializer.class,
-                TestWebAuthnUsernamelessWithPolicyApplicationContextInitializer.class
+                TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.class
                 }
         )
-public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
+public class TestPasswordlessFlowTriggerOnNoUserHandleCredentials extends AbstractWebAuthnFlowTest{
     
     /** Flow ID. */
     @Nonnull public static final String FLOW_ID = "authn/WebAuthn";
@@ -59,17 +58,22 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
     /**
      * Constructor.
      */
-    protected TestUsernameslessFlowWithPolicy() {
-        super(FLOW_ID, "proceed");
+    protected TestPasswordlessFlowTriggerOnNoUserHandleCredentials() {
+        super(FLOW_ID);
     }
     
+    
     /**
      * Checks:
      * 
      * <ol>
-     * <li>User selects credential</li>
-     * <li>Credential matches to a known registered credential</li>
-     * <li>Fail, the credential is filtered by the policy</li>
+     * <li>Username entered has 1 credential</li>
+     * <li>Credential request options contains an allowCredentials with 1 credential</li>
+     * <li>User selects the credential from the provider, authenticator signs challenge</li>
+     * <li>UserHandle is NOT returned in the response</li>
+     * <li>idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle is true, but it should not trigger
+     * because the user is already known and has credentials</li>
+     * <li>(Success) The WebAuthn client validates the credential</li>
      * </ol>
      * 
      * 
@@ -77,75 +81,31 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
      */
     @SuppressWarnings("null")
     @Test
-    public void testUsernamelessFlow_WithPolicy_CredentialRejected_2FAOnly() throws Exception {
-        //Register a credential for use that is only suitable for 2FA
+    public void testPasswordlessFlow_NoUserHandleShouldNotTriggerNoCredentials() throws Exception {
+        //Register a credential for use.
         final CredentialRecord registration = 
-                createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+                createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64,null);
         credentialRepo.addRegistrationByUsername(USERNAME, registration);
         
-        final var prc = buildProfileRequestContext(false, false, null);
+        final var prc = buildProfileRequestContext(false, false, USERNAME);
 
         final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
                 addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
 
         assertFlowExecutionActive(result.getSecond());
-        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-        assertPublicKeyCredentialRequestOptions(prc, false, true);
-        
-        // Do assertion validation half of flow
+        assertCurrentStateEquals("CollectUsernameView", result.getSecond());
         
-        // Get the challenge that was set into the PublicKeyCredentialRequestOptions. 
-        // otherwise we will end up signing a different one, which will provide its own test.
-        final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
-        final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
-        
-        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
-        assertionResponse = createAssertionReponseFrom(
-                registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
-
-        final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
-        
-        // Re-set external context to holder
+        // Do next part of flow, populate the username
         ExternalContextHolder.setExternalContext(externalContext);
-        setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME, 
-                assertionResponseJson));
+        setHttpFormRequest("POST", Map.of("j_username", USERNAME));
         externalContext.setEventId("proceed");
-        result.getSecond().setCurrentState("DisplayWebAuthnView");
-        result.getSecond().resume(externalContext);
         
-        // Policy failure will return the user to the webauthn login page
-        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-
-    }
-    
-    /**
-     * Checks:
-     * 
-     * <ol>
-     * <li>User selects credential</li>
-     * <li>No userHandle in the response.</li>
-     * <li>fail, userHandle is required if allowCredentials is empty.</li>
-     * </ol>
-     * 
-     * 
-     * @throws Exception on error
-     */
-    @SuppressWarnings("null")
-    @Test
-    public void testUsernamelessFlow_WithPolicy_NoUserHandle() throws Exception {
-        //Register a credential for use that is only suitable for 2FA
-        final CredentialRecord registration = 
-                createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
-        credentialRepo.addRegistrationByUsername(USERNAME, registration);
+        result.getSecond().setCurrentState("CollectUsernameView");
+        result.getSecond().resume(externalContext);
         
-        final var prc = buildProfileRequestContext(false, false, null);
-
-        final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
-                addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
-
         assertFlowExecutionActive(result.getSecond());
         assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-        assertPublicKeyCredentialRequestOptions(prc, false, true);
+        assertPublicKeyCredentialRequestOptions(prc, true, true);
         
         // Do assertion validation half of flow
         
@@ -155,7 +115,6 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
         final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
         
         final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
-        //Blank userHandle
         assertionResponse = createAssertionReponseFrom(
                 registration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), true);
 
@@ -169,12 +128,12 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
         result.getSecond().setCurrentState("DisplayWebAuthnView");
         result.getSecond().resume(externalContext);
         
+        // assert end conditions, no existing result or principal. So this should produce a Username principal
+        assertEquals(result.getSecond().getOutcome().getId(), "proceed");
+        assertAuthenticationSuccessConditions(prc, true);
         
-        // Should fail, as no user.name or user.id in the context or userHandle in the response
-        assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
-        assertAuthenticationFailureConditions(prc);
-
     }
     
+   
 
 }
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
index 5351b30..c34287b 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlow.java
@@ -33,6 +33,7 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 
 import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
@@ -161,6 +162,55 @@ public class TestSecondFactorFlow extends AbstractWebAuthnFlowTest{
         
     }
     
+    @SuppressWarnings("null")
+    @Test
+    public void testSecondFactorFlow_UserHandleDoesNotMatchUsername() throws Exception {
+        //Register a credential for use.
+        final CredentialRecord registration = 
+                createCredentialRegistration(USERNAME, DISPLAY_NAME, USER_HANDLE_B64, null);
+        credentialRepo.addRegistrationByUsername(USERNAME, registration);
+        
+        final CredentialRecord differentUserRegistration = 
+                createCredentialRegistration("different-user", DISPLAY_NAME, "d2R3ZXFmZndmd2U=", null);
+        credentialRepo.addRegistrationByUsername("different-user", registration);
+        
+        final var prc = buildProfileRequestContext(false, false, USERNAME);
+        buildMfaContext(prc.ensureSubcontext(AuthenticationContext.class), "authn/Password");
+
+        final Pair<FlowExecutionResult, FlowExecutionImpl> result = launchExecution(FLOW_ID, null, externalContext, 
+                addToConversationScopeMap(Map.of("opensamlProfileRequestContext", prc)));
+
+        assertFlowExecutionActive(result.getSecond());
+        assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
+        assertPublicKeyCredentialRequestOptions(prc, true, false);
+        
+        // Do assertion validation half of flow
+        
+        // Get the challenge that was set into the PublicKeyCredentialRequestOptions. 
+        // otherwise we will end up signing a different one, which will provide its own test.
+        final WebAuthnAuthenticationContext authnContext = getWebAuthnAuthenticationContext(prc);
+        final ByteArray challenge = authnContext.getPublicKeyCredentialRequestOptions().getChallenge();
+        
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
+        // Blank the userHandle
+        assertionResponse = createAssertionReponseFrom(
+                differentUserRegistration.getCredential().getCredentialId().getBytes(), challenge.getBytes(), false);
+
+        final String assertionResponseJson = jsonMapper.writeValueAsString(assertionResponse);
+        
+        // Re-set external context to holder
+        ExternalContextHolder.setExternalContext(externalContext);
+        setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAssertionFromFormRequest.DEFAULT_PARAMETER_NAME, 
+                assertionResponseJson));
+        externalContext.setEventId("proceed");
+        result.getSecond().setCurrentState("DisplayWebAuthnView");
+        result.getSecond().resume(externalContext);
+        
+        assertEquals(result.getSecond().getOutcome().getId(), AuthnEventIds.INVALID_CREDENTIALS);
+        assertAuthenticationFailureConditions(prc);
+        
+    }
+    
     /* 
      * The flow will proceed as if first factor passwordless, as no acceptable previous result. So we will end up on the 
      * CollectUsernameView instead of DisplayWebAuthnView.
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
index f552bc0..4ddecf9 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestSecondFactorFlowWithFilterPolicy.java
@@ -135,7 +135,7 @@ public class TestSecondFactorFlowWithFilterPolicy extends AbstractWebAuthnFlowTe
     }
     
     /*
-     * Tests the 2fa flow when there is not userHandle in the assertion response. Which is allowable if
+     * Tests the 2fa flow when there is no userHandle in the assertion response. Which is allowable if
      * the 'allowedCredentials' are supplied to the WebAuthn get request.
      */
     @SuppressWarnings("null")
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
index 7780cfb..5d14e86 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestUsernameslessFlowWithPolicy.java
@@ -113,9 +113,11 @@ public class TestUsernameslessFlowWithPolicy extends AbstractWebAuthnFlowTest{
         result.getSecond().setCurrentState("DisplayWebAuthnView");
         result.getSecond().resume(externalContext);
         
+        // Just check we had credentials in the context to get the policy from
+        assertEquals(getWebAuthnAuthenticationContext(prc).getExistingCredentials().size(), 1);
         // Policy failure will return the user to the webauthn login page
         assertCurrentStateEquals("DisplayWebAuthnView", result.getSecond());
-
+        
     }
     
     /**
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java
new file mode 100644
index 0000000..a11f3ed
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.flow;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.mock.env.MockPropertySource;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link ApplicationContextInitializer} which prepends properties.
+ *
+ * <p>This needs to include the original IdP-test-layer properties and has to be
+ * set to {@link Ordered#LOWEST_PRECEDENCE} or things blow up.</p>
+ */
+ at Order(Ordered.LOWEST_PRECEDENCE)
+public class TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestWebAuthnPasswordlessTriggerEventOnNoCredentialsApplicationContextInitializer.class);
+
+    /** {@inheritDoc} */
+    @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+        final MockPropertySource mock = new MockPropertySource("passwordless-mock-properties");
+        mock.setProperty("idp.authn.webauthn.usernameless.enabled", "false");
+        mock.setProperty("idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle", "true");
+        applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+        log.info("Prepending usernameless properties '{}'", mock.getSource());
+    }
+    
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
index 44d83cc..f2091d3 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandleTest.java
@@ -34,7 +34,9 @@ import com.yubico.webauthn.data.ByteArray;
 import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
 import com.yubico.webauthn.data.UserIdentity;
+import com.yubico.webauthn.data.UserVerificationRequirement;
 
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.plugin.authn.webauthn.authn.WebAuthnAuthenticationEventIds;
@@ -50,6 +52,8 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
     private LookupRegisteredCredentialsFromUserHandle lookup;
     
     private UserIdentity userIdentity;
+
+    private PublicKeyCredentialRequestOptions credentialRequestOptions;
    
     
     @Override
@@ -67,6 +71,16 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
                 .displayName("Test User")
                 .id(ByteArray.fromBase64(USER_HANDLE_B64))
                 .build();
+        
+        
+        credentialRequestOptions =
+                PublicKeyCredentialRequestOptions.builder()
+                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+                    .rpId(rp.getIdentity().getId())
+                    .userVerification(UserVerificationRequirement.REQUIRED)
+                    .timeout(Optional.of(60000l))
+                    .build();
+        webAuthnContext.setPublicKeyCredentialRequestOptions(credentialRequestOptions);
     }
     
     @SuppressWarnings("null")
@@ -123,7 +137,7 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
      */
     @SuppressWarnings("null")
     @Test
-    public void testUserHandleHasCredentials_UsernameInContextIsDifferent() throws Exception {
+    public void testUserHandleHasCredentials_UsernameInContextIsDifferent() throws Exception {       
         
         // This is different from the username located from the userhandle
         webAuthnContext.setUsername("username-collected");
@@ -231,6 +245,64 @@ public class LookupRegisteredCredentialsFromUserHandleTest extends AbstractWebAu
         assertNull(event);
     }
     
+    @SuppressWarnings("null")
+    @Test
+    public void testNoUserHandle_AllowedCredentialsEmpty() throws Exception {
+        
+        credentialRequestOptions =
+                PublicKeyCredentialRequestOptions.builder()
+                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+                    .rpId(rp.getIdentity().getId())
+                    .userVerification(UserVerificationRequirement.REQUIRED)
+                    .timeout(Optional.of(60000l))
+                    .build();
+        webAuthnContext.setPublicKeyCredentialRequestOptions(credentialRequestOptions);
+        
+        lookup.initialize();
+        
+        mockAuthenticator = new MockAuthenticator(RPID);
+        
+        final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+
+        // Need to register a new credential first
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation = 
+                mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate, 
+                        Base64Support.decode(USER_HANDLE_B64), null);
+        
+        final RegisteredCredential credential = RegisteredCredential.builder()
+                .credentialId(attestation.getId())
+                .userHandle(new ByteArray(Base64Support.decode(USER_HANDLE_B64)))
+                .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+                        .getAttestedCredentialData().get().getCredentialPublicKey())
+                .build();      
+
+        
+        final CredentialRecord reg = CredentialRecord.builder()
+                .withUserIdentity(userIdentity)
+                .withUsername(USERNAME)
+                .withTransports(new TreeSet<AuthenticatorTransport>())
+                .withRegistrationTime(Instant.now())
+                .withCredential(credential)
+                .withCredentialNickname("Nickname")
+                .withDiscoverable(Optional.of(Boolean.TRUE))
+                .withUserVerified(true)
+                .build();
+        
+        credentialRepo.addRegistrationByUsername(USERNAME, reg);
+        
+        final Map<String, String> clientDataGet = createClientData("webauthn.get", ORIGIN, CHALLENGE_B64); 
+        
+        // Now generate an assertion (authentication) and check it is valid
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> 
+            assertion = mockAuthenticator.createAuthenticatorAssertionResponse(attestation.getId().getBytes(), 
+                    clientDataGet, null, true);
+        
+        webAuthnContext.setPublicKeyCredentialAssertionResponse(assertion);
+        
+        final Event event = lookup.execute(src);
+        assertEquals(event.getId(), WebAuthnAuthenticationEventIds.NO_REGISTERED_WEBAUTHN_CREDENTIALS);
+    }
+    
     
 
 }

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


More information about the commits mailing list