[java-idp-plugin-webauthn] branch main updated: Add username collection step if passwordless enabled

Phil Smart philip.smart at jisc.ac.uk
Mon Jan 22 09:56:51 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=a68b9bb0ffd7e968942579b1549feb7d882428b0

The following commit(s) were added to refs/heads/main by this push:
     new a68b9bb  Add username collection step if passwordless enabled
a68b9bb is described below

commit a68b9bb0ffd7e968942579b1549feb7d882428b0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Jan 22 09:56:49 2024 +0000

    Add username collection step if passwordless enabled
---
 .../context/logic/UsernamelessFlowEnabled.java     |  84 ++++++++++
 .../webauthn/impl/ExtractUsernameFromForm.java     | 169 +++++++++++++++++++++
 .../PopulateWebAuthnAuthenticationContext.java     |  60 +-------
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  11 +-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |  73 ++++++---
 .../authn/webauthn/conf/authn/webauthn.properties  |  15 +-
 6 files changed, 330 insertions(+), 82 deletions(-)

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
new file mode 100644
index 0000000..0f8ab01
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/UsernamelessFlowEnabled.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context.logic;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Predicate to determine if the user is required to enter their username or not. True implies a usernameless flow 
+ * whilst false implies a passwordless flow.
+ */
+public class UsernamelessFlowEnabled extends AbstractInitializableComponent 
+                                                implements Predicate<ProfileRequestContext> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(IsDiscoverableCredentialRequired.class);
+    
+    /** 
+     * Determines if we want a usernameless flow (true), or a passwordless flow (false). 
+     * Default is false (passwordless).
+     */
+    @Nonnull private Predicate<ProfileRequestContext> enabled;
+    
+    /** Constructor.*/ 
+    public UsernamelessFlowEnabled() {
+        enabled = PredicateSupport.alwaysFalse();
+    }
+    
+    /**
+    * Set a predicate that determines if we want a usernameless flow (true), or a passwordless flow (false). 
+    * 
+    * @param predicate the predicate
+    */
+   public void setEnabled(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+       checkSetterPreconditions();
+       enabled = Constraint.isNotNull(predicate, "Enabled predicate can not be null");
+   }
+   
+   /**
+    * Set a flag that determines if we want a usernameless flow (true), or a passwordless flow (false). 
+    * 
+    * @param flag the flag to set
+    */
+   public void setEnabled(final boolean flag) {
+       checkSetterPreconditions();
+       enabled = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+   }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (input == null) {
+            log.trace("Profile context was null, assuming username is required");
+            return false;
+        }
+        final boolean usernamelessEnabled = enabled.test(input);
+        log.debug("{}", usernamelessEnabled ? "Usernameless authentication flow initiated" : 
+            "Passwordless authentication flow initiated");
+        return usernamelessEnabled;
+    }
+
+}
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
new file mode 100644
index 0000000..9d1afa0
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
@@ -0,0 +1,169 @@
+/*
+ * 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.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+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.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+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
+ */
+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;
+
+    /** 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;
+    
+    /** Constructor.*/
+    public ExtractUsernameFromForm() {            
+        usernameFieldName = "j_username";
+        ssoBypassFieldName = "donotcache";
+
+        webAuthnContextLookupStrategy =
+                new ChildContextLookup<>(WebAuthnAuthenticationContext.class).compose(
+                        new ChildContextLookup<>(AuthenticationContext.class));
+    }
+    
+    /**
+    * Set the strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on.
+    * 
+    * @param strategy lookup strategy
+    */
+   public void setWebAuthnContextLookupStrategy(
+           @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+       checkSetterPreconditions();
+
+       webAuthnContextLookupStrategy =
+               Constraint.isNotNull(strategy, "WebAuthnAuthenticationContext lookup strategy cannot be null");
+   }
+   
+    /**
+     * Sets the name of the form field to carry the username.
+     * 
+     * @param name field name
+     */
+    public void setUsernameFieldName(@Nonnull final String name) {
+        checkSetterPreconditions();
+        
+        usernameFieldName = Constraint.isNotNull(StringSupport.trimOrNull(name) ,
+                "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) {
+        
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        webAuthnContext = webAuthnContextLookupStrategy.apply(profileRequestContext);
+        if (webAuthnContext == null) {
+            log.debug("{} No WebAuthnContext found, nothing to do", getLogPrefix());
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {        
+
+        final String username = getUsernameFromForm(profileRequestContext, authenticationContext);
+        if (username == null) {
+            log.warn("{} Unable to find username in HTTP request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
+            return;
+        }
+        log.trace("{} Populating username '{}' from form",getLogPrefix(), username);
+        webAuthnContext.setUsername(username);
+        
+    }
+    
+    /**
+     * 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) {
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request != null) {
+            // FIXME This has not been thought out here.
+            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);
+            }            
+            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/impl/PopulateWebAuthnAuthenticationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
index 2089ede..0544080 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
@@ -16,7 +16,6 @@
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
 import java.util.function.Function;
-import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
@@ -29,9 +28,6 @@ import org.slf4j.Logger;
 import net.shibboleth.idp.authn.AbstractAuthenticationAction;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
-import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -53,12 +49,6 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
     @Nonnull 
     private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
     
-    /** Lookup strategy for username to extract. */
-    @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
-    
-    /** Is the username required?*/
-    private Predicate<ProfileRequestContext> usernameRequiredPredicate;
-    
 
     /** Constructor.*/
     public PopulateWebAuthnAuthenticationContext() {
@@ -66,66 +56,22 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
         webauthnAuthContextCreationStrategy =
                 new ChildContextLookup<>(WebAuthnAuthenticationContext.class, true).
                 compose(new ChildContextLookup<>(AuthenticationContext.class));
-        
-        usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
-        usernameRequiredPredicate = PredicateSupport.alwaysTrue();
-    }
-
-    /**
-     * @param flag The usernameRequired to set.
-     */
-    public void setUsernameRequired(final boolean flag) {
-        checkSetterPreconditions();
-        usernameRequiredPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
-    }
-    
-    /**
-     * @param usernameRequiredPredicate The usernameRequiredPredicate to set.
-     */
-    public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
-        checkSetterPreconditions();
-        usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
-    }
-    
-    /**
-     * Set the lookup strategy to use for the username to use if we are not using a discoverable credential.
-     * 
-     * @param strategy lookup strategy
-     */
-    public void setUsernameLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, String> strategy) {
-        checkSetterPreconditions();
 
-        usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
     }
-    
 
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
 
         
-        final WebAuthnAuthenticationContext context = webauthnAuthContextCreationStrategy.apply(profileRequestContext);
+        final WebAuthnAuthenticationContext context = 
+                webauthnAuthContextCreationStrategy.apply(profileRequestContext);
         if (context == null) {
             log.error("{} Error creating WebauthnAuthenticationContext", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
-        
-        //TODO this should not go in this action? Unless we always assume username input step before webauthn
-        final String username = usernameLookupStrategy.apply(profileRequestContext);
-        //username = "philsmart";
-        if (username == null && usernameRequiredPredicate.test(profileRequestContext)) {
-            log.error("{} Error creating WebauthnAuthenticationContext, no username found", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return;
-        }
-        if (username != null) {
-            context.setUsername(username);
-            log.debug("Created Webauthn authentication context for user '{}'", username);
-            return;
-        }
-        log.debug("Created Webauthn authentication context, no previous username provided'");
+        log.debug("Created Webauthn authentication context");
         
     }
     
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 66d44e9..f849186 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
@@ -16,8 +16,7 @@
 
 
     <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
-        p:usernameRequiredPredicate="false">
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext">
     </bean>
 
     <bean id="IsSecondFactor" scope="prototype"
@@ -26,6 +25,10 @@
             getObject('shibboleth.authn.webauthn.SecondFactorOverride') : %{idp.authn.webauthn.2fa.forceSecondFactorFlow:false}}"
         p:allowedPreviousFactors="%{idp.authn.webauthn.2fa.allowedPreviousFactors}"
         p:enabled="%{idp.authn.webauthn.2fa.enabled:false}"/>
+    
+    <bean id="IsUsernamelessFlow" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.UsernamelessFlowEnabled"
+        p:enabled="%{idp.authn.webauthn.usernameless.enabled:false}"/>
 
     <bean id="IsDiscoverableCredentialRequired" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsDiscoverableCredentialRequired" />
@@ -33,6 +36,10 @@
     <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext) }" />
+        
+    <bean id="ExtractUsernameFromForm" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractUsernameFromForm"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
     
     <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 7f8cf74..aa308e8 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
@@ -1,45 +1,84 @@
 <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
     parent="authn.abstract, authn/conditions">
-
-    <action-state id="PopulateWebauthnContext">
-        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
-        <evaluate expression="'proceed'" />
-        <transition on="proceed" to="DetermineSecondFactorLogin" />    
-    </action-state>
     
-    <!-- Test if we are operating as a 2FA -->
+     <!-- Test if we are operating as a 2FA -->
      <decision-state id="DetermineSecondFactorLogin">
         <if test="IsSecondFactor.test(opensamlProfileRequestContext)"
             then="SecondFactorLogin" 
-            else="DetermineUsernamelessOrPasswordlessLogin" />        
+            else="DetermineUsernamelessFlow" />        
     </decision-state>
     
-    <!-- Test if we are operating as a first (and possibly only) factor -->
-    <decision-state id="DetermineUsernamelessOrPasswordlessLogin">
-        <if test="IsDiscoverableCredentialRequired.test(opensamlProfileRequestContext)"
+    <!-- if usernameless flow, assume username input is not required. Otherwise prompt for username -->
+    <decision-state id="DetermineUsernamelessFlow">
+        <if test="IsUsernamelessFlow.test(opensamlProfileRequestContext)"
             then="UsernamelessLogin" 
             else="PasswordlessLogin" />        
     </decision-state>
     
-    <!-- If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials-->
-    <action-state id="UsernamelessLogin">
-        <evaluate expression="AddUserVerificationRequired"/>
-        <evaluate expression="EnsureAllowedCredentialsIsEmpty"/>
+    <!-- 
+        Passwordless login
+     -->
+     <action-state id="PasswordlessLogin">
+        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
+        <transition on="proceed" to="CollectUsernameView" />    
     </action-state>
+        
+    <view-state id="CollectUsernameView" view="webauthn/webauthn-username-entry">
+        <on-render>
+            <evaluate expression="environment" result="viewScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="viewScope.webauthnContext" />
+            <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
+            <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="viewScope.authenticationErrorContext" />
+            <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="viewScope.authenticationWarningContext" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+        </on-render>
+       <transition on="proceed" to="PasswordlessLoginProceed" />
+       
+    </view-state>
     
     <!-- If Passwordless: do not require ResidentKey, require UV, UP, and provide previous credentials based on username -->
-    <action-state id="PasswordlessLogin">
+    <action-state id="PasswordlessLoginProceed">
+        <evaluate expression="ExtractUsernameFromForm"/>
+        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
         <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="AddUserVerificationRequired"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
+    </action-state>    
+    
+    <!-- 
+        Usernameless login
+     -->
+    
+    <!-- If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials-->
+    <action-state id="UsernamelessLogin">
+        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
+        <evaluate expression="AddUserVerificationRequired"/>
+        <evaluate expression="EnsureAllowedCredentialsIsEmpty"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
     </action-state>
     
+    <!-- Test if we are operating as a first (and possibly only) factor -->
+    <!-- <decision-state id="DetermineUsernamelessOrPasswordlessLogin">
+        <if test="IsDiscoverableCredentialRequired.test(opensamlProfileRequestContext)"
+            then="UsernamelessLogin" 
+            else="PasswordlessLogin" />        
+    </decision-state> -->
+    
+    <!-- 
+        Second Factor login
+     -->
+    
     <!-- If we are running after a first factor, perform 2FA only. Needs existing username -->
     <action-state id="SecondFactorLogin">
+        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
         <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="AddUserVerificationNotRequired"/>
         <evaluate expression="'proceed'" />
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 6a08a1f..0f5c994 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
@@ -1,16 +1,16 @@
-## Thre relying party ID. Must be a valid domain string.
-## A public key credential is only registered and valid for a single relying party ID.
+## WebAuthn relying party setup
+### The relying party ID. Must be a valid domain string.
+### A public key credential is only registered and valid for a single relying party ID.
 idp.authn.webauthn.relyingPartyId = localhost
 idp.authn.webauthn.relyingPartyName = Shibboleth
 ## Allow any port on that origin
 idp.authn.webauthn.allowOriginPort = true
 idp.authn.webauthn.allowOriginSubdomain = false
 
-## Display debug information about the registration and authentication ceremony on their respective views?
-#idp.authn.webauthn.ui.debug = false
+## Which type of flow is supported? Usernameless or passwordless
+# idp.authn.webauthn.usernameless.enabled = false
 
 ## Registration properties.
-
 ### Require a residentKey to be created when registering a credential. One-of 'discouraged', 'preferred', 'required'
 # idp.authn.webauthn.registration.residentKey = preferred
 ### The authenticatorAttachment requirement. One-of 'any', 'cross-platform', or 'platform'. 
@@ -24,4 +24,7 @@ idp.authn.webauthn.2fa.allowedPreviousFactors = authn/Password
 #idp.authn.webauthn.2fa.forceSecondFactorFlow = false
 ### Deny second factor irrespective of the value of forceSecondFactorFlow and if an acceptable previous factor is found
 ### Effectively turning off its ability to act as a second factor only
-#idp.authn.webauthn.2fa.enabled = false
\ No newline at end of file
+#idp.authn.webauthn.2fa.enabled = false
+
+## Display debug information about the registration and authentication ceremony on their respective views?
+#idp.authn.webauthn.ui.debug = false
\ No newline at end of file

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


More information about the commits mailing list