[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