[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-6 - Signal an end-state from the authentication flow if user has no registered credentials

Phil Smart philip.smart at jisc.ac.uk
Wed Apr 17 15:57:01 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=e2b697f71bb21538b364343ecdb30a1c69981eae

The following commit(s) were added to refs/heads/main by this push:
     new e2b697f  JWEBAUTHN-6 - Signal an end-state from the authentication flow if user has no registered credentials
e2b697f is described below

commit e2b697f71bb21538b364343ecdb30a1c69981eae
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Apr 17 16:56:57 2024 +0100

    JWEBAUTHN-6 - Signal an end-state from the authentication flow if user
    has no registered credentials
    
     - Add the ability to configure custom events that should be built if no
    credentials can be found for the given username or userhandle.
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-6
---
 .../authn/WebAuthnAuthenticationEventIds.java      |   4 +
 .../context/logic/UsernamelessFlowEnabled.java     |   2 +-
 .../impl/AbstractWebAuthnRegistrationAction.java   |   2 +-
 .../authn/webauthn/admin/impl/AddUserId.java       |   2 +-
 .../impl/AbstractWebAuthnAuthenticationAction.java |  25 +++-
 .../webauthn/impl/LookupRegisteredCredentials.java |  29 +++-
 .../LookupRegisteredCredentialsFromUserHandle.java | 160 +++++++++++++++++++++
 .../webauthn-registration-beans.xml                |  16 +--
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  17 ++-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |   3 +
 .../storage/impl/InMemoryRegistrationStorage.java  |   2 +
 11 files changed, 238 insertions(+), 24 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/WebAuthnAuthenticationEventIds.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/WebAuthnAuthenticationEventIds.java
index 98fcfae..b9b26e6 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/WebAuthnAuthenticationEventIds.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/WebAuthnAuthenticationEventIds.java
@@ -34,5 +34,9 @@ public final class WebAuthnAuthenticationEventIds {
     @Nonnull @NotEmpty 
     public static final String NO_REGISTERED_WEBAUTHN_CREDENTIALS = "NoRegisteredWebAuthnCredentials";
     
+    /** The user has no registered WebAuthn credentials for the user handle supplied. */
+    @Nonnull @NotEmpty 
+    public static final String USER_HANDLE_NOT_REGISTERED = "UserHandleNotRegistered";
+    
 
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/UsernamelessFlowEnabled.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/UsernamelessFlowEnabled.java
index 0f8ab01..fed84b3 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/UsernamelessFlowEnabled.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/UsernamelessFlowEnabled.java
@@ -35,7 +35,7 @@ public class UsernamelessFlowEnabled extends AbstractInitializableComponent
                                                 implements Predicate<ProfileRequestContext> {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(IsDiscoverableCredentialRequired.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(UsernamelessFlowEnabled.class);
     
     /** 
      * Determines if we want a usernameless flow (true), or a passwordless flow (false). 
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
index 7edcac6..3b0a83a 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
@@ -150,7 +150,7 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
      * 
      * @return the credential repository.
      */
-    @NonnullAfterInit public StorageServiceCredentialRepository getCredentialRepository() {
+    @NonnullAfterInit protected StorageServiceCredentialRepository getCredentialRepository() {
         return credentialRepository;
     }
     
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
index ec72a7b..4ed89c7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
@@ -120,7 +120,7 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
     }    
     
     /**
-     * Default user.id generator that generates a 64 byte randomized UserHandle (must be at least 32 bytes long). 
+     * Default user.id generator that generates a 64 byte randomized user.id (must be at least 32 bytes long). 
      * Returns {@code null} iff one can not be generated.
      * 
      * <p>This could contain some form of state if required, but must not contain retrievable PII.</p>
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
index 3678978..2736b5c 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.authn.webauthn.impl;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
@@ -31,6 +32,7 @@ import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -65,6 +67,9 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
     /** The WebAuthn client to use.*/
     @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
     
+    /** The credential repository to store WebAuthn credentials in.*/
+    @Nullable private StorageServiceCredentialRepository credentialRepository;
+    
     
     /**
      * Set the WebAuthn client used to handle registration and authentication ceremonies.
@@ -85,7 +90,25 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
         checkComponentActive();
         return webAuthnClient;
     }
-        
+    
+    /**
+     * Set the credential repository used to store WebAuthn credentials.
+     *  
+     * @param repository The respository to set.
+     */
+    public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
+        checkSetterPreconditions();
+        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+    }
+    
+    /**
+     * Get the credential repository used to store WebAuthn credentials.
+     * 
+     * @return the credential repository.
+     */
+    @Nullable protected StorageServiceCredentialRepository getCredentialRepository() {
+        return credentialRepository;
+    } 
     
     /** Constructor.*/
     protected AbstractWebAuthnAuthenticationAction() {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
index d4bc589..1fce097 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentials.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
 import java.util.Collection;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
@@ -29,10 +30,13 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * An action that lookups existing registered credentials and sets them onto the base WebAuthn context.
+ * An action that lookups existing registered credentials based on the username contained in the WebAuthn context 
+ * and sets them back onto the context. If no credentials exist and the trigger event condition is set, an error 
+ * event will be produced.
  * 
  * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
  * @post BaseWebAuthnContext.setExistingCredentials() is either null if no existing credentials are found, or contains
@@ -45,7 +49,7 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnBaseAction {
     private final Logger log = LoggerFactory.getLogger(LookupRegisteredCredentials.class);
     
     /** Should an event be built if there are no credentials found?.*/
-    private boolean triggerEventOnNoCredentials;
+    private Predicate<ProfileRequestContext> triggerEventOnNoCredentialsPredicate;
     
     /** 
      * The EventID of the event to build if no credentials are foud and <code>triggerEventOnNoCredentials<code> 
@@ -59,13 +63,24 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnBaseAction {
     }
     
     /**
-     * Set a flag which triggers a non-proceed event to be built if no credentials are found?
+     * Set a flag which triggers a custom event to be built if no credentials are found?
      * 
      * @param trigger the flag to set
      */
     public void setTriggerEventOnNoCredentials(final boolean trigger) {
         checkSetterPreconditions();
-        triggerEventOnNoCredentials = trigger;
+        triggerEventOnNoCredentialsPredicate = trigger ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+    
+    /**
+     * Set a predicate which triggers a custom event to be built if no credentials are found?
+     * 
+     * @param trigger the flag to set
+     */
+    public void setTriggerEventOnNoCredentialsPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        triggerEventOnNoCredentialsPredicate = 
+                Constraint.isNotNull(predicate, "TriggerEventOnNoCredentialsPredicate can not be null");
     }
     
     /**
@@ -85,7 +100,7 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnBaseAction {
         
         final String username = context.getUsername();
         if (username == null) {
-            log.error("{} Unable to find username in registration context", getLogPrefix());
+            log.error("{} Unable to find username in WebAuthn context", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         } 
@@ -96,8 +111,8 @@ public class LookupRegisteredCredentials extends AbstractWebAuthnBaseAction {
         log.debug("{} Found '{}' registered credentials for '{}'", getLogPrefix(), credentials.size(), username);
         context.setExistingCredentials(credentials);
         
-        if (triggerEventOnNoCredentials && credentials.isEmpty()) {
-            log.error("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
+        if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && credentials.isEmpty()) {
+            log.debug("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
             ActionSupport.buildEvent(profileRequestContext, noCredentialsEventId);
             return;
         } 
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
new file mode 100644
index 0000000..39bcaf8
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
@@ -0,0 +1,160 @@
+/*
+ * 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.Collection;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+
+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.authn.WebAuthnAuthenticationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that lookups existing registered credentials from the userHandle supplied in the authenticators assertion
+ * response. If no credentials exist and the trigger condition is set, an error event will be produced.
+ * 
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @post BaseWebAuthnContext.setExistingCredentials() is either null if no existing credentials are found, or contains
+ * the credentials from the credential repository
+ */
+public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(LookupRegisteredCredentialsFromUserHandle.class);
+    
+    /** Should an event be built if there are no credentials found?.*/
+    private Predicate<ProfileRequestContext> triggerEventOnNoCredentialsPredicate;
+    
+    /** 
+     * The EventID of the event to build if no credentials are foud and <code>triggerEventOnNoCredentials<code> 
+     * is set.
+     */
+    @Nonnull @NotEmpty private String noCredentialsEventId;
+    
+    /** Constructor.  */
+    public LookupRegisteredCredentialsFromUserHandle() {
+        noCredentialsEventId = WebAuthnAuthenticationEventIds.NO_REGISTERED_WEBAUTHN_CREDENTIALS;
+    }
+    
+    /**
+     * Set a flag which triggers a custom event to be built if no credentials are found?
+     * 
+     * @param trigger the flag to set
+     */
+    public void setTriggerEventOnNoCredentials(final boolean trigger) {
+        checkSetterPreconditions();
+        triggerEventOnNoCredentialsPredicate = trigger ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+    }
+    
+    /**
+     * Set a predicate which triggers a custom event to be built if no credentials are found?
+     * 
+     * @param trigger the flag to set
+     */
+    public void setTriggerEventOnNoCredentialsPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        triggerEventOnNoCredentialsPredicate = 
+                Constraint.isNotNull(predicate, "TriggerEventOnNoCredentialsPredicate can not be null");
+    }
+    
+    /**
+     * Set the EventID of the event to build if no credentials are foud and <code>triggerEventOnNoCredentials<code> 
+     * is set.
+     * 
+     * @param eventId the eventId to build.
+     */
+    public void setNoCredentialsEventId(@Nonnull @NotEmpty final String eventId) {
+        checkSetterPreconditions();
+        noCredentialsEventId = Constraint.isNotEmpty(eventId, "NoCredentialsEventId can not be null or empty");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (getCredentialRepository() ==  null) {
+            throw new ComponentInitializationException("The credential repository can not be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> assertion = 
+                context.getAuthenticatorAssertionResponse();
+        if (assertion == null) {
+            log.error("{} Unable to find Assertion in WebAuthn authentication context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        } 
+        
+        final StorageServiceCredentialRepository repository = getCredentialRepository();
+        assert repository != null;
+        
+        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, "
+                    + "no registered credentials", getLogPrefix());
+        } else {
+            final Optional<String> potentialUsername = repository.getUsernameForUserHandle(userHandle.get());
+            if (potentialUsername.isEmpty()) {
+                log.debug("{} User could not be found from the supplied userHandle, no registered credentials", getLogPrefix());
+            } else {
+                final Collection<CredentialRegistration> credentials = 
+                        repository.getRegistrationsByUsername(potentialUsername.get());
+                if (credentials.isEmpty()) {
+                    log.debug("{} Could not find any registered credentials for userHandle '{}'", getLogPrefix(), 
+                            userHandle.get().getBase64());
+                } else {
+                    credentialsFound = true;
+                }
+                
+            }
+        }
+
+        if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && credentialsFound == false) {
+            log.debug("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
+            ActionSupport.buildEvent(profileRequestContext, noCredentialsEventId);
+            return;
+        } 
+    }
+
+}
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 fe8b0db..716933c 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
@@ -63,36 +63,36 @@
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
         p:userVerificationRequirement="%{idp.authn.webauthn.registration.userVerification:discouraged}" />
 
-    <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnBaseAction"
+    <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
 
-    <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
+    <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
 
-    <bean id="AddUserId" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="AddUserId" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddUserId" />
 
-    <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"/>
 
-    <bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractAuthenticatorAttestationFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
 
-    <bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyRemovalInformationFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
 
-    <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential" />
 
     <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
 
 
-    <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
+    <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.StorePublicKeyCredential"
         p:storageService-ref="shibboleth.authn.webauthn.StorageService"
         p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository" />
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 8b22d66..7e2c029 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
@@ -30,7 +30,8 @@
     </bean>
     
     <bean id="PopulateWebAuthnAuthenticationContextUsernameless" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"/>   
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
+        p:usernameRequired="false"/>   
     
     <bean id="PopulateWebAuthnAuthenticationContextFor2FA" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
@@ -75,23 +76,29 @@
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
         p:userVerificationRequirement="discouraged" />
 
-    <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnBaseAction"
+    <bean id="LookupRegisteredCredentials" scope="prototype" parent="AbstractWebAuthnBaseAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
         p:triggerEventOnNoCredentials="%{idp.authn.webauthn.triggerEventOnNoCredentials:false}"
         p:noCredentialsEventId="%{idp.authn.webauthn.noCredentialsEventId:NoRegisteredWebAuthnCredentials}"
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
 
-    <bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
+    <bean id="GenerateServerChallenge" scope="prototype" parent="AbstractWebAuthnBaseAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
         p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
 
-    <bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
+    <bean id="CreatePublicKeyCredentialRequestOptions" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"/>
 
-    <bean id="ExtractAuthenticatorAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
+    <bean id="ExtractAuthenticatorAssertionFromFormRequest" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractAuthenticatorAssertionFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
         p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+        
+    <bean id="LookupRegisteredCredentialsFromUserHandle" scope="prototype" parent="AbstractWebAuthnAuthenticationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentialsFromUserHandle"
+        p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository"
+        p:triggerEventOnNoCredentials="%{idp.authn.webauthn.triggerEventOnUserHandleNotRegistered:false}"
+        p:noCredentialsEventId="%{idp.authn.webauthn.userHandleNotRegisteredEventId:UserHandleNotRegistered}"/>        
 
     <bean id="ValidateWebAuthnAssertion" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion"
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 b10bd24..0c69d56 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
@@ -119,6 +119,9 @@
      
     <action-state id="AuthenticatePublicKeyCredential">
         <evaluate expression="ExtractAuthenticatorAssertionFromFormRequest"/>
+        <!-- lookup credentials here so we can exit the process before validation if no registered credentials exist and
+        the authentication plugin has been configured to trigger a custom event. Useful for the usernameless flow. -->
+        <evaluate expression="LookupRegisteredCredentialsFromUserHandle"/>
         <evaluate expression="ValidateWebAuthnAssertion"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="proceed" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
index 95a7e40..775a301 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -50,6 +50,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredential
 /**
  * In memory credential repository to use for testing.
  */
+ at SuppressWarnings("null")
 public class InMemoryRegistrationStorage implements StorageServiceCredentialRepository {
 
     private final Cache<String, Set<CredentialRegistration>> storage = CacheBuilder.newBuilder().maximumSize(1000)
@@ -138,6 +139,7 @@ public class InMemoryRegistrationStorage implements StorageServiceCredentialRepo
                 registration.getCredential().toBuilder().signatureCount(result.getSignatureCount()).build()));
     }
 
+
     public Optional<CredentialRegistration> getRegistrationByUsernameAndCredentialId(final String username,
             final ByteArray id) {
         try {

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


More information about the commits mailing list