[java-idp-plugin-webauthn] branch main updated: Add initial username lookup to registration flow, pre-authentication

Phil Smart philip.smart at jisc.ac.uk
Fri Feb 16 11:41:58 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=83cba90d98debd852cfb108a3285f16f10b903ed

The following commit(s) were added to refs/heads/main by this push:
     new 83cba90  Add initial username lookup to registration flow, pre-authentication
83cba90 is described below

commit 83cba90d98debd852cfb108a3285f16f10b903ed
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Feb 16 11:41:55 2024 +0000

    Add initial username lookup to registration flow, pre-authentication
    
     - Allows the MFA flow to determine which authentication flow to follow
    depending on whether the user already has pre-registered credentials.
---
 webauthn-api/pom.xml                               |  5 ++
 .../webauthn/context/BaseWebAuthnContext.java      |  9 ++
 .../logic/IsUsernameCollectionRequired.java        | 64 ++++++++++++++
 .../navigate/UsernameLookupFromHttpRequest.java    | 97 ++++++++++++++++++++++
 .../UsernameLookupFromRegistrationContext.java     | 56 +++++++++++++
 .../webauthn/impl/ExtractUsernameFromForm.java     |  2 +-
 .../PopulateWebAuthnAuthenticationContext.java     | 19 +++--
 .../webauthn-registration-beans.xml                |  8 +-
 .../webauthn-registration-flow.xml                 | 42 +++++++---
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    | 15 +++-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     | 49 +++++++----
 11 files changed, 320 insertions(+), 46 deletions(-)

diff --git a/webauthn-api/pom.xml b/webauthn-api/pom.xml
index a4d0850..8eac6bb 100644
--- a/webauthn-api/pom.xml
+++ b/webauthn-api/pom.xml
@@ -25,6 +25,11 @@
         distribution. -->
 
         <!-- Provided dependencies -->
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>${idp.groupId}</groupId>
             <artifactId>idp-authn-api</artifactId>
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
index a85b26f..a297505 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
@@ -58,6 +58,15 @@ public class BaseWebAuthnContext extends BaseContext {
     /** Does the authentication/registration require user verification.*/
     @Nullable private UserVerificationRequirement userVerificationRequirement;
     
+    /**
+     * Are credentials available to use for WebAuthn authentication.
+     * 
+     * @return true iff credentials are available, false otherwise.
+     */
+    public boolean isWebAuthnAvailable() {
+        return (existingCredentials != null && !existingCredentials.isEmpty());
+    }
+    
     /**
      * Gets the username.
      * 
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernameCollectionRequired.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernameCollectionRequired.java
new file mode 100644
index 0000000..1c2ae70
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsUsernameCollectionRequired.java
@@ -0,0 +1,64 @@
+/*
+ * 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.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A predicate that determines if a username already exists in the authentication context.
+ */
+public class IsUsernameCollectionRequired extends AbstractInitializableComponent implements Predicate<ProfileRequestContext> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(IsUsernameCollectionRequired.class);    
+
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext input) {
+        if (input == null) {
+            log.trace("Profile context was null, can not determine if a username already exists, "
+                    + "assume collection required");
+            return true;
+        }
+        final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+        if (authnContext == null) {
+            log.trace("Authentication context was null, can not determine if a username already exists, "
+                    + "assume collection required");
+            return true;
+        }
+        final WebAuthnAuthenticationContext webauthnContext = 
+                authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
+        if (webauthnContext == null) {
+            log.trace("WebAuthn authentication context was null, can not determine if a username already exists, "
+                    + "assume collection required");
+            return true;
+        }
+        final boolean usernameExists = webauthnContext.getUsername() != null;
+        log.debug("{}", usernameExists ? "Username exists, skipping collection" : 
+            "Username does not exist, collecting");
+        return !usernameExists;
+
+    }
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromHttpRequest.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromHttpRequest.java
new file mode 100644
index 0000000..5ab2b74
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromHttpRequest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Extract a username from the HTTP request, returning {@code null} if not found.
+ */
+ at ThreadSafeAfterInit
+public class UsernameLookupFromHttpRequest extends AbstractIdentifiableInitializableComponent
+                        implements Function<ProfileRequestContext, String> {
+    
+    /** Form parameter name to carry username. */
+    @Nonnull @NotEmpty private String usernameFieldName;
+    
+    /** Supplier for the Current HTTP request, if available. */
+    @Nullable private NonnullSupplier<HttpServletRequest> httpServletRequestSupplier;
+    
+    /** Constructor.*/
+    public UsernameLookupFromHttpRequest() {
+        usernameFieldName = "j_username";
+    }
+    
+    /**
+     * Get the current HTTP request if available.
+     * 
+     * @return current HTTP request
+     */
+    @Nullable public HttpServletRequest getHttpServletRequest() {
+        checkComponentActive();
+        if (httpServletRequestSupplier != null) {
+            return httpServletRequestSupplier.get();
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Set the current HTTP request Supplier.
+     *
+     * @param requestSupplier Supplier for the current HTTP request
+     */
+    public void setHttpServletRequestSupplier(@Nullable final NonnullSupplier<HttpServletRequest> requestSupplier) {
+        checkSetterPreconditions();
+        httpServletRequestSupplier = requestSupplier;
+    }
+    
+    /**
+     * 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");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String apply(@Nullable final ProfileRequestContext prc) {
+        checkComponentActive();
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request != null) {
+            return request.getParameter(usernameFieldName);
+        }
+        return null;
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromRegistrationContext.java
new file mode 100644
index 0000000..64f3991
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromRegistrationContext.java
@@ -0,0 +1,56 @@
+/*
+ * 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.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Pull out a username from a WebAuthn Registration Context if it exists. Useful when operating inside a WebAuthn 
+ * registration flow.
+ */
+public class UsernameLookupFromRegistrationContext implements Function<ProfileRequestContext, String> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(UsernameLookupFromRegistrationContext.class);
+
+    /** {@inheritDoc} */
+    @Override
+    public String apply(@Nullable final ProfileRequestContext input) {
+        if (input == null) {
+            log.trace("Profile context was null, can not find existing username");
+            return null;
+        }
+        final WebAuthnRegistrationContext registrationContext = 
+                input.getSubcontext(WebAuthnRegistrationContext.class);
+        if (registrationContext == null) {
+            log.trace("WebAuthn registration context was null, can not find existing username");
+            return null;
+        }
+        final String username = registrationContext.getUsername();
+        log.debug("{}", username != null ? "Found existing username" : 
+            "Did not find existing username");
+        return username;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
index 9d1afa0..10f42d1 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
@@ -153,7 +153,7 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
         
         final HttpServletRequest request = getHttpServletRequest();
         if (request != null) {
-            // FIXME This has not been thought out here.
+            // FIXME this will not work atm?
             final String donotcache = request.getParameter(ssoBypassFieldName);
             if (donotcache != null && "1".equals(donotcache)) {
                 log.debug("{} Recording do-not-cache instruction in authentication context", getLogPrefix());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
index de11d69..95225cc 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
@@ -112,18 +112,19 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
-        
-        if (usernameRequiredPredicate.test(profileRequestContext)) {
-            final String username = usernameLookupStrategy.apply(profileRequestContext);
-            if (username == null && usernameRequiredPredicate.test(profileRequestContext)) {
-                log.error("{} Error creating WebauthnAuthenticationContext, no username found", getLogPrefix());
-                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-                return;
-            }
+        // Username can be null, but if required record exception if it is
+        final String username = usernameLookupStrategy.apply(profileRequestContext);
+        if (usernameRequiredPredicate.test(profileRequestContext) && username == null) {   
+            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");
+        log.debug("Created WebAuthn authentication context {}", username != null ? "for user '"+username+"'" : 
+            "without existing username");
         
     }
     
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 3b92309..dde01ff 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
@@ -18,12 +18,12 @@
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
 
-    <!-- TODO Should this been populating an authentication context for an admin flow? -->
     <bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
-        <property name="usernameLookupStrategy">
-            <bean id="UsernameFromAuthenticationContextLookupStrategy"
-                class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy" />
+         <property name="usernameLookupStrategy">
+            <bean id="usernameFromHttpRequest" scope="prototype"
+                class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromHttpRequest"
+                p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
         </property>
     </bean>
     
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
index af69ac8..b7c4519 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -8,6 +8,30 @@
     <action-state id="InitializeProfileRequestContext">
         <evaluate expression="InitializeProfileRequestContext" />
         <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="'proceed'" />        
+
+        <transition on="proceed" to="CollectUsernameView" />
+    </action-state>
+
+    
+    <view-state id="CollectUsernameView" view="webauthn/webauthn-username-register">
+        <on-render>
+            <evaluate expression="environment" result="viewScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="requestScope.cspNonce" />            
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
+            <evaluate expression="T(net.shibboleth.shared.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="ExtractUsernameAndPopulateContext" />
+       
+    </view-state>
+    
+    <action-state id="ExtractUsernameAndPopulateContext">
+        <evaluate expression="PopulateWebAuthnRegistrationContext"/>
+        <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="'proceed'" />
         
         <!-- Branch to determine if authentication is required. -->
@@ -20,12 +44,10 @@
         <evaluate expression="CheckAccess" />
         <evaluate expression="'proceed'" />
         
-        <transition on="proceed" to="PopulateWebAuthnContext" />
+        <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions" />
     </action-state>   
     
-     <action-state id="PopulateWebAuthnContext">
-        <evaluate expression="PopulateWebAuthnRegistrationContext"/>
-        <evaluate expression="LookupRegisteredCredentials"/>
+     <action-state id="GeneratePublicKeyCredentialCreationOptions">        
         <evaluate expression="GenerateServerChallenge"/>
         <evaluate expression="GenerateUserHandle"/>
         <evaluate expression="AddResidentKeyRequirement"/>
@@ -41,11 +63,9 @@
         <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.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
-            <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="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="requestScope.cspNonce" />   
             <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
             <evaluate expression="T(net.shibboleth.idp.plugin.authn.webauthn.impl.WebAuthnEncoder)" result="viewScope.webAuthnEncoder"/>
             <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
@@ -87,11 +107,9 @@
         <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.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
-            <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="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="requestScope.cspNonce" />   
             <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
             <evaluate expression="T(net.shibboleth.idp.plugin.authn.webauthn.impl.WebAuthnEncoder)" result="viewScope.webAuthnEncoder"/>
             <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
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 fc33f7d..f4bc07e 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
@@ -15,14 +15,25 @@
         c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
 
 
-    <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
+    <bean id="PopulateWebAuthnAuthenticationContextPasswordless" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext">
-    </bean>    
+        <!-- In-case a username has been supplied by an outer WebAuthn registration context -->
+        <property name="usernameLookupStrategy">
+            <bean id="UsernameLookupFromRegistrationContext" 
+                class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromRegistrationContext"/>
+        </property>
+    </bean>
+    
+    <bean id="PopulateWebAuthnAuthenticationContextUsernameless" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"/>   
     
     <bean id="PopulateWebAuthnAuthenticationContextFor2FA" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
         p:usernameRequired="true">
     </bean>
+    
+    <bean id="IsUsernameCollectionRequired" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsUsernameCollectionRequired"/>
 
     <bean id="IsSecondFactor" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsSecondFactor" 
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 7d5e59d..ea528f8 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
@@ -17,13 +17,23 @@
     </decision-state>
     
     <!-- 
-        Passwordless login
+        Passwordless login. If Passwordless: do not require ResidentKey, require UV, UP, and provide previous credentials based on username.
      -->
      <action-state id="PasswordlessLogin">
-        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
+        <evaluate expression="PopulateWebAuthnAuthenticationContextPasswordless"/>
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="CollectUsernameView" />    
+        <transition on="proceed" to="DetermineIfUsernameCollectionIsRequired" />    
     </action-state>
+    
+    <!-- If the registration context already contains a username, e.g. from an outer registration flow, skip collection -->
+    <decision-state id="DetermineIfUsernameCollectionIsRequired">
+        <if test="IsUsernameCollectionRequired.test(opensamlProfileRequestContext)"
+            then="CollectUsernameView" 
+            else="PasswordlessLoginProceed" />        
+    </decision-state>
+    
+        
+    <!-- need decision state, we might have already collected username, so might not need it again e.g. when part of a registration flow -->
         
     <view-state id="CollectUsernameView" view="webauthn/webauthn-username">
         <on-render>
@@ -34,31 +44,34 @@
             <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.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="requestScope.cspNonce" />   
+            <evaluate expression="T(net.shibboleth.shared.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" />
+       <transition on="proceed" to="PasswordlessLoginExtractUsername" />
        
     </view-state>
     
-    <!-- If Passwordless: do not require ResidentKey, require UV, UP, and provide previous credentials based on username -->
-    <action-state id="PasswordlessLoginProceed">
+    <action-state id="PasswordlessLoginExtractUsername">
         <evaluate expression="ExtractUsernameFromForm"/>
-        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="PasswordlessLoginProceed" />    
+    </action-state>    
+    
+    <action-state id="PasswordlessLoginProceed">
         <evaluate expression="LookupRegisteredCredentials"/>
         <evaluate expression="AddUserVerificationRequired"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />    
-    </action-state>    
+    </action-state>  
     
     <!-- 
-        Usernameless login
-     -->
-    
-    <!-- If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials-->
+        Usernameless login. If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials.
+     -->   
     <action-state id="UsernamelessLogin">
-        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
+        <evaluate expression="PopulateWebAuthnAuthenticationContextUsernameless"/>
         <evaluate expression="AddUserVerificationRequired"/>
         <evaluate expression="EnsureAllowedCredentialsIsEmpty"/>
         <evaluate expression="'proceed'" />
@@ -73,10 +86,8 @@
     </decision-state> -->
     
     <!-- 
-        Second Factor login
+        Second Factor login. If we are running after a first factor, perform 2FA only. Needs existing username.
      -->
-    
-    <!-- If we are running after a first factor, perform 2FA only. Needs existing username -->
     <action-state id="SecondFactorLogin">
         <evaluate expression="PopulateWebAuthnAuthenticationContextFor2FA"/>
         <evaluate expression="LookupRegisteredCredentials"/>
@@ -102,7 +113,9 @@
             <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.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="requestScope.cspDigester" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="requestScope.cspNonce" />   
+            <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
             <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
             <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
         </on-render>

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


More information about the commits mailing list