[java-idp-plugin-webauthn] branch main updated: Add user.id principal type, set mode/flow of operation to base context

Phil Smart philip.smart at jisc.ac.uk
Tue May 7 15:37:27 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=790be2a4b8312ae45a41a2544040ff7266bc1036

The following commit(s) were added to refs/heads/main by this push:
     new 790be2a  Add user.id principal type, set mode/flow of operation to base context
790be2a is described below

commit 790be2a4b8312ae45a41a2544040ff7266bc1036
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue May 7 16:37:24 2024 +0100

    Add user.id principal type, set mode/flow of operation to base context
    
     - improve subject population
     - small code cleanup
---
 .../authn/webauthn/WebAuthnUserIdPrinicpal.java    | 101 +++++++++++++++++++++
 .../authn/webauthn/authn/AssertionResult.java      |  71 +++++++++++++--
 .../webauthn/context/BaseWebAuthnContext.java      |  58 +++++-------
 .../context/WebAuthnAuthenticationContext.java     |  72 +++++++++++++++
 ...actAuthenticatorAttestationFromFormRequest.java |   3 +-
 .../impl/YubicoWebAuthnAuthenticationClient.java   |   3 +-
 .../BaseWebAuthnAuthenticationContextConsumer.java |  68 ++++++++++++++
 .../PopulateWebAuthnAuthenticationContext.java     |  22 ++++-
 .../impl/SetPaswordlessUsageToContextConsumer.java |  30 ++++++
 .../SetSecondFactorUsageToContextConsumer.java     |  30 ++++++
 .../SetUsernamelessUsageToContextConsumer.java     |  30 ++++++
 .../webauthn/impl/ValidateWebAuthnAssertion.java   |  25 ++++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   9 ++
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  18 +++-
 14 files changed, 482 insertions(+), 58 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnUserIdPrinicpal.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnUserIdPrinicpal.java
new file mode 100644
index 0000000..175c79a
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnUserIdPrinicpal.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import com.google.common.base.MoreObjects;
+
+import net.shibboleth.idp.authn.principal.CloneablePrincipal;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/** Principal based on the WebAuthn user.id base64 encoded*/
+public class WebAuthnUserIdPrinicpal implements CloneablePrincipal {
+    
+    /** The username. */
+    @Nonnull @NotEmpty private String userIdBase64Encoded;
+
+    /**
+     * Constructor.
+     * 
+     * @param name the user.id as raw bytes
+     */
+    public WebAuthnUserIdPrinicpal(@Nonnull @NotEmpty @ParameterName(name="userId") final byte[] id) {
+        Constraint.isNotNull(id, "User.id cannot be null or empty");
+        try {
+            userIdBase64Encoded = Base64Support.encode(id, false);
+        } catch (final EncodingException e) {
+            throw new ConstraintViolationException("User.id can not be base64 encoded");
+        }
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param name the user.id base64 encoded
+     */
+    public WebAuthnUserIdPrinicpal(@Nonnull @NotEmpty @ParameterName(name="userId") final String idBase64) {
+        userIdBase64Encoded = Constraint.isNotNull(idBase64, "User.id cannot be null or empty");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getName() {
+        return userIdBase64Encoded;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return userIdBase64Encoded.hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null) {
+            return false;
+        }
+
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof final WebAuthnUserIdPrinicpal otherPrincipal) {
+            return userIdBase64Encoded.equals(otherPrincipal.getName());
+        }
+
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this).add("userId", userIdBase64Encoded).toString();
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public WebAuthnUserIdPrinicpal clone() throws CloneNotSupportedException {
+        final WebAuthnUserIdPrinicpal copy = (WebAuthnUserIdPrinicpal) super.clone();
+        copy.userIdBase64Encoded = userIdBase64Encoded;
+        return copy;
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AssertionResult.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AssertionResult.java
index 8870655..4192e2e 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AssertionResult.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AssertionResult.java
@@ -35,6 +35,17 @@ public class AssertionResult {
     /** Is the signature count valid?*/
     private final boolean signatureCounterValid;
     
+    /** The user.id.*/
+    private final byte[] userId;
+
+    
+    private AssertionResult(final Builder builder) {
+        this.success = builder.success;
+        this.username = builder.username;
+        this.signatureCounterValid = builder.signatureCounterValid;
+        this.userId = builder.userId;
+    }
+    
     /**
      * @return Returns the success.
      */
@@ -57,41 +68,81 @@ public class AssertionResult {
     public final boolean isSignatureCounterValid() {
         return signatureCounterValid;
     }
+    
+    /**
+     * @return Returns the userId.
+     */
+    @Nonnull public byte[] getUserId() {
+        return userId;
+    }
 
 
-    private AssertionResult(final Builder builder) {
-        this.success = builder.success;
-        this.username = builder.username;
-        this.signatureCounterValid = builder.signatureCounterValid;
-    }
     
-    public static Builder builder() {
+    public static ISuccessStage builder() {
         return new Builder();
     }
+
+    
+    public interface ISuccessStage {
+        public IUsernameStage withSuccess(boolean success);
+    }
+
+    
+    public interface IUsernameStage {
+        public ISignatureCounterValidStage withUsername(String username);
+    }
+
+    
+    public interface ISignatureCounterValidStage {
+        public IUserIdStage withSignatureCounterValid(boolean signatureCounterValid);
+    }
+
+    
+    public interface IUserIdStage {
+        public IBuildStage withUserId(byte[] userId);
+    }
+
     
-    public static final class Builder {
+    public interface IBuildStage {
+        public AssertionResult build();
+    }
+
+    
+    public static final class Builder
+            implements ISuccessStage, IUsernameStage, ISignatureCounterValidStage, IUserIdStage, IBuildStage {
         private boolean success;
         private String username;
         private boolean signatureCounterValid;
+        private byte[] userId;
 
         private Builder() {
         }
 
-        public Builder withSuccess(final boolean success) {
+        @Override
+        public IUsernameStage withSuccess(final boolean success) {
             this.success = success;
             return this;
         }
 
-        public Builder withUsername(@Nonnull final String username) {
+        @Override
+        public ISignatureCounterValidStage withUsername(final String username) {
             this.username = username;
             return this;
         }
 
-        public Builder withSignatureCounterValid(final boolean signatureCounterValid) {
+        @Override
+        public IUserIdStage withSignatureCounterValid(final boolean signatureCounterValid) {
             this.signatureCounterValid = signatureCounterValid;
             return this;
         }
 
+        @Override
+        public IBuildStage withUserId(final byte[] userId) {
+            this.userId = userId;
+            return this;
+        }
+
+        @Override
         public AssertionResult build() {
             return new AssertionResult(this);
         }
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 3802a7b..49195d6 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
@@ -24,10 +24,9 @@ import org.opensaml.messaging.context.BaseContext;
 import com.yubico.webauthn.data.UserVerificationRequirement;
 
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.annotation.constraint.NotLive;
 import net.shibboleth.shared.annotation.constraint.Unmodifiable;
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
 
@@ -37,10 +36,10 @@ import net.shibboleth.shared.logic.Constraint;
 public class BaseWebAuthnContext extends BaseContext {
     
     /** 
-     * The user.name of the user. If {@code null} in the authentication ceremony if we can not determine the 
-     * userHandle (and hence public key) to use, and the flow will require a discoverable credential.
+     * The user.name of the user. If {@code null} in the authentication ceremony and we can not determine the 
+     * user.id (and hence public key) to use, and the flow will require a discoverable credential.
      */
-    @Nullable private String username;    
+    @Nullable private String username;
     
     /** 
      * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
@@ -49,25 +48,25 @@ public class BaseWebAuthnContext extends BaseContext {
     @Nullable private Collection<CredentialRegistration> existingCredentials;
     
     /** The challenge sent to the authenticator in both registration and authentication ceremonies.*/
-    @Nullable private byte[] serverChallenge;  
+    @Nullable private byte[] serverChallenge;
     
     /** The user.id supplied to the authenticator during registration. Generated by the IdP.*/
-    @Nullable private byte[] userId;    
+    @Nullable private byte[] userId;
         
-    /** Does the authentication/registration require user verification.*/
+    /** 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.
+     * @return true iff existing credentials are available, false otherwise.
      */
     public boolean isWebAuthnAvailable() {
         return (existingCredentials != null && !existingCredentials.isEmpty());
     }
     
     /**
-     * Gets the user.name.
+     * Gets the username (user.name in WebAuthn parlance).
      * 
      * @return the user.name
      */
@@ -76,14 +75,14 @@ public class BaseWebAuthnContext extends BaseContext {
     }
 
     /**
-     * Sets the username.
+     * Sets the username (user.name in WebAuthn parlance). Must not be empty or {@code null}.
      * 
-     * @param name the username
+     * @param name the user.name
      * 
      * @return this context
      */
-    @Nonnull public BaseWebAuthnContext setUsername(@Nullable final String name) {
-        username = name;
+    @Nonnull public BaseWebAuthnContext setUsername(@Nonnull @NotEmpty final String name) {
+        username = Constraint.isNotEmpty(name, "User.name can not be null or empty");
         return this;
     }
 
@@ -101,9 +100,9 @@ public class BaseWebAuthnContext extends BaseContext {
     }
     
     /**
-     * Get the  credentials that have already been registered with the IdP.
+     * Get the credentials that have already been registered with the IdP.
      * 
-     * @return the excluded credentials.
+     * @return the existing credentials.
      */
     @Nonnull @Unmodifiable @NotLive public Collection<CredentialRegistration> getExistingCredentials() {
         final Collection<CredentialRegistration> localExistingCredentials = existingCredentials;
@@ -114,7 +113,7 @@ public class BaseWebAuthnContext extends BaseContext {
     }
     
     /**
-     * Get the server challenge sent to the authenticator.
+     * Get the server challenge sent to (or to send to) the authenticator.
      * 
      * @return the server challenge.
      */
@@ -122,23 +121,10 @@ public class BaseWebAuthnContext extends BaseContext {
         return serverChallenge;
     }
     
-    /**
-     * Get the server challenge base64 encoded.
-     * 
-     * @return the challenge base64 encoded
-     * 
-     * @throws EncodingException on error encoding the challenge.
-     */
-    //TODO throw in a context, this could be null (which is bad here?)
-    @SuppressWarnings("null")
-    @Nullable public String getServerChallengeBase64() throws EncodingException {
-        return Base64Support.encode(serverChallenge, false);
-    }
-    
     /**
      * Set the server challenge which forms part of the information the client authenticator needs to sign.
      * 
-     * @param challenge the challenge, must not be empty and must be minimum 16 bytes long.
+     * @param challenge the challenge, must not be empty and must be a minimum 16 bytes long.
      * 
      * @return this context.
      */
@@ -151,9 +137,9 @@ public class BaseWebAuthnContext extends BaseContext {
     
     
     /**
-     * Set the user.id used to map public key credentials to user accounts. Maximum 64 bytes 
+     * Set the user.id used to map public key credentials to user accounts. Maximum 64 bytes.
      * 
-     * @param handle The userHandle to set.
+     * @param id The user.id to set.
      * 
      * @return this context
      */
@@ -165,10 +151,10 @@ public class BaseWebAuthnContext extends BaseContext {
     }
     
     /**
-     * Get the user.id used to map public key credentials to user accounts. Send to the authenticator during credential
-     * creation. Referred to as the userHandle in responses from the authenticator.
+     * Get the user.id used to map public key credentials to user accounts. Sent to the authenticator during credential
+     * creation. Referred to as the userHandle in responses from the authenticator during authentication.
      * 
-     * @return the userId.
+     * @return the user.id.
      */
     @Nullable public byte[] getUserId() {
         return userId;
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
index 4c98aef..6cd6832 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -19,6 +19,15 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
     /** The credential identifier generated by the authenticator.*/
     @Nullable private byte[] credentialId;
     
+    /** Are we operating as a second factor of authentication? */
+    private boolean secondFactor;
+    
+    /** Are we operating in passwordless mode.*/
+    private boolean passwordless;
+    
+    /** Are we operating in usernameless mode.*/
+    private boolean usernameless;
+    
     /** An assertion response that is the result of an authentication.*/
     @Nullable 
     private PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> authenticatorAssertionResponse;
@@ -93,5 +102,68 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
         getAuthenticatorAssertionResponse() {
         return authenticatorAssertionResponse;
     }
+    
+    /**
+     * Set if this authentication is part of a usernameless flow/mode.
+     * 
+     * @param flag the flag to indicate the mode of authentication
+     *
+     * @return this context
+     */
+    @Nonnull public WebAuthnAuthenticationContext setUsernameless(final boolean flag) {
+        usernameless = flag;
+        return this;
+    }
+    
+    /**
+     * Is this authentication part of a usernameless flow/mode?
+     * 
+     * @return true if it is, false otherwise.
+     */
+    public boolean isUsernameless() {
+        return usernameless;
+    }
+    
+    /**
+     * Set if this authentication is part of a passwordless flow/mode.
+     * 
+     * @param flag the flag to indicate the mode of authentication
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnAuthenticationContext setPasswordless(final boolean flag) {
+        passwordless = flag;
+        return this;
+    }
+    
+    /**
+     * Is this authentication part of a passwordless flow/mode.
+     * 
+     * @return true if it is, false otherwise.
+     */
+    public boolean isPasswordless() {
+        return passwordless;
+    }
+    
+    /**
+     * Set if this authentication is part of a second factor flow/mode.
+     * 
+     * @param flag the flag to indicate the mode of authentication.
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnAuthenticationContext setSecondFactor(final boolean flag) {
+        secondFactor = flag;
+        return this;
+    }
+    
+    /**
+     * Is this authentication part of a second factor flow/mode?
+     * 
+     * @return true if it is, false otherwise.
+     */
+    public boolean isSecondFactor() {
+        return secondFactor;
+    }
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
index cdd8803..3bf1864 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
@@ -118,8 +118,7 @@ public class ExtractAuthenticatorAttestationFromFormRequest extends AbstractWebA
         log.trace("Public key credential nickname is '{}'",credNickname);
         if (StringSupport.trimOrNull(credNickname) == null) {
             log.warn("{} No nickname in request", getLogPrefix());
-            //TODO which event?
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
             return;
         }
 
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
index c0855ee..094f540 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
@@ -187,9 +187,10 @@ public class YubicoWebAuthnAuthenticationClient implements WebAuthnAuthenticatio
                 throw new AssertionFailureException("Authenticator assertion was not valid");
             }
             final AssertionResult assertionResult = AssertionResult.builder()
-                    .withSignatureCounterValid(result.isSignatureCounterValid())
                     .withSuccess(result.isSuccess())
                     .withUsername(result.getUsername())
+                    .withSignatureCounterValid(result.isSignatureCounterValid())
+                    .withUserId(result.getCredential().getUserHandle().getBytes())
                     .build();
             assert assertionResult != null;
             return assertionResult;
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAuthenticationContextConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAuthenticationContextConsumer.java
new file mode 100644
index 0000000..43774a0
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAuthenticationContextConsumer.java
@@ -0,0 +1,68 @@
+/*
+ * 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.Consumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+
+/**
+ * A base class that pulls out the {@link WebAuthnAuthenticationContext} for consumers to use.
+ */
+public abstract class BaseWebAuthnAuthenticationContextConsumer implements Consumer<ProfileRequestContext> {
+    
+    /** Strategy used to locate or create the {@link WebAuthnAuthenticationContext} to populate. */
+    @Nonnull 
+    private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
+    
+    /** Constructor.*/
+    public BaseWebAuthnAuthenticationContextConsumer() {
+        webauthnAuthContextCreationStrategy =
+                new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final ProfileRequestContext input) {
+        if (input == null) {
+            // Do nothing
+            return;
+        }
+        final WebAuthnAuthenticationContext webAuthnContext = webauthnAuthContextCreationStrategy.apply(input);
+        if (webAuthnContext == null) {
+            // Do nothing
+            return;
+        }
+        doAccept(webAuthnContext);
+        
+    }
+
+    /**
+     * Consume the WebAuthn authentication context. Implementations must override this method.
+     * 
+     * @param webAuthnContext the WebAuthn authentication context
+     */
+    protected abstract void doAccept(@Nonnull final WebAuthnAuthenticationContext webAuthnContext);
+
+}
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 b4a0538..df0cc93 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
@@ -15,6 +15,7 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -56,7 +57,12 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
     @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
     
     /** Is the username required? */
-    private Predicate<ProfileRequestContext> usernameRequiredPredicate;
+    @Nonnull private Predicate<ProfileRequestContext> usernameRequiredPredicate;
+    
+    /** Consumer to update the context conditionally. For example, to set 
+     * {@link WebAuthnAuthenticationContext#setSecondFactor(boolean)} if we are operating as a second factor.
+     */
+    @Nonnull private Consumer<ProfileRequestContext> contextUpdateConsumer; 
     
 
     /** Constructor.*/
@@ -68,7 +74,18 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
         
         usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
         usernameRequiredPredicate = PredicateSupport.alwaysFalse();
-
+        contextUpdateConsumer = prc -> {};
+    }
+    
+    /**
+     * Set a consumer that updates any part of the profile request context.
+     * 
+     * @param consumer The consumer.
+     */
+    public void setContextUpdateConsumer(@Nonnull final Consumer<ProfileRequestContext> consumer) {
+        checkSetterPreconditions();
+        contextUpdateConsumer = Constraint.isNotNull(consumer,
+                "ContextUpdateConsumer can not be null");
     }
     
     /**
@@ -125,6 +142,7 @@ public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticatio
         if (username != null) {
             context.setUsername(username);
         }
+        contextUpdateConsumer.accept(profileRequestContext);        
 
         log.debug("Created WebAuthn authentication context {}", username != null ? "for user '"+username+"'" : 
             "without existing username");
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetPaswordlessUsageToContextConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetPaswordlessUsageToContextConsumer.java
new file mode 100644
index 0000000..160521e
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetPaswordlessUsageToContextConsumer.java
@@ -0,0 +1,30 @@
+/*
+ * 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 net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+
+/**
+ * A consumer that sets the isPasswordless mode to the authentication context.
+ */
+public class SetPaswordlessUsageToContextConsumer extends BaseWebAuthnAuthenticationContextConsumer{
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doAccept(final WebAuthnAuthenticationContext webAuthnContext) {
+        webAuthnContext.setPasswordless(true);
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetSecondFactorUsageToContextConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetSecondFactorUsageToContextConsumer.java
new file mode 100644
index 0000000..4a8442b
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetSecondFactorUsageToContextConsumer.java
@@ -0,0 +1,30 @@
+/*
+ * 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 net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+
+/**
+ * A consumer that sets the isSecondFactor mode to the authentication context.
+ */
+public class SetSecondFactorUsageToContextConsumer extends BaseWebAuthnAuthenticationContextConsumer{
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doAccept(final WebAuthnAuthenticationContext webAuthnContext) {
+        webAuthnContext.setSecondFactor(true);
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetUsernamelessUsageToContextConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetUsernamelessUsageToContextConsumer.java
new file mode 100644
index 0000000..b5ef843
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/SetUsernamelessUsageToContextConsumer.java
@@ -0,0 +1,30 @@
+/*
+ * 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 net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+
+/**
+ * A consumer that sets the isUsernameless mode to the authentication context.
+ */
+public class SetUsernamelessUsageToContextConsumer extends BaseWebAuthnAuthenticationContextConsumer{
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doAccept(final WebAuthnAuthenticationContext webAuthnContext) {
+        webAuthnContext.setUsernameless(true);
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
index c703601..833d35e 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -20,6 +20,7 @@ import net.shibboleth.idp.authn.AbstractValidationAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnUserIdPrinicpal;
 import net.shibboleth.idp.plugin.authn.webauthn.authn.AssertionResult;
 import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
@@ -154,9 +155,10 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
             updateSignatureCount(result.getUsername(), assertion);
             
             log.info("{} WebAuthn authentication succeeded for '{}'",getLogPrefix(),result.getUsername());
-            // Add the username that matched the credential from the result back to the context. 
+            // Add the username and user.id that matched the credential from the result back to the context. 
             // The result is authorative.
             context.setUsername(result.getUsername());
+            context.setUserId(result.getUserId());
             buildAuthenticationResult(profileRequestContext, authenticationContext);
             
         } catch (final AssertionFailureException e) {
@@ -199,14 +201,27 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
     @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
         super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+
     }
 
     @Override
     protected Subject populateSubject(@Nonnull final Subject subject) {
-        final String username = context.getUsername();
-        assert username != null;
-        subject.getPrincipals().add(new UsernamePrincipal(username));
-        return subject;
+        
+        // Add a WebAuthn specific user.id principal
+        final byte[] userId = context.getUserId();
+        assert userId != null;
+        subject.getPrincipals().add(new WebAuthnUserIdPrinicpal(userId));
+        
+        if (context.isSecondFactor()) {
+            // If second factor, we already have a username principal and a canonical name, so do nothing
+            log.trace("{} second factor usage, username principal already set", getLogPrefix());
+            return subject;
+        } else {
+            final String username = context.getUsername();
+            assert username != null;
+            subject.getPrincipals().add(new UsernamePrincipal(username));
+            return subject;
+        }
     }
 
 }
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index d6725f1..21d5d4c 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -167,6 +167,15 @@
             <bean class="java.text.SimpleDateFormat" c:_0="yyyy-MM-dd'T'HH:mm:ss.SSSZZ" />
         </property>
     </bean>
+    
+    <!-- Principal serialization support. -->
+    <bean p:id="WebAuthnUserId" class="net.shibboleth.idp.authn.principal.GenericPrincipalService"
+            c:claz="net.shibboleth.idp.authn.duo.DuoPrincipal">
+        <constructor-arg name="serializer">
+            <bean class="net.shibboleth.idp.authn.principal.SimplePrincipalSerializer"
+                c:claz=" net.shibboleth.idp.plugin.authn.webauthn.WebAuthnUserIdPrinicpal" c:name="WEBAUTHNUSERID" />
+        </constructor-arg>
+    </bean>
 
   
 </beans>
\ No newline at end of file
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 1cc5b33..7079a9d 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
@@ -22,20 +22,34 @@
     <!-- Flow beans -->
     
     <bean id="PopulateWebAuthnAuthenticationContextPasswordless" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext">
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
+        p:usernameRequired="false">
         <property name="usernameLookupStrategy">
             <bean id="usernameFromAuthnResult" scope="prototype"
                 class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromRegistrationContext"/>
         </property>
+        <property name="contextUpdateConsumer">
+            <bean id="setPasswordlessConsumer" scope="prototype"
+                class="net.shibboleth.idp.plugin.authn.webauthn.impl.SetPaswordlessUsageToContextConsumer"/>
+        </property>
     </bean>
     
     <bean id="PopulateWebAuthnAuthenticationContextUsernameless" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
-        p:usernameRequired="false"/>   
+        p:usernameRequired="false">
+        <property name="contextUpdateConsumer">
+            <bean id="setUsernamelessConsumer" scope="prototype"
+                class="net.shibboleth.idp.plugin.authn.webauthn.impl.SetUsernamelessUsageToContextConsumer"/>
+        </property>
+    </bean>   
     
     <bean id="PopulateWebAuthnAuthenticationContextFor2FA" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
         p:usernameRequired="true">
+        <property name="contextUpdateConsumer">
+            <bean id="setUsernamelessConsumer" scope="prototype"
+                class="net.shibboleth.idp.plugin.authn.webauthn.impl.SetSecondFactorUsageToContextConsumer"/>
+        </property>
     </bean>
     
     <bean id="IsUsernameCollectionRequired" scope="prototype"

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


More information about the commits mailing list