[java-idp-plugin-webauthn] branch main updated: Add authentication and registration request param actions and builders
Phil Smart
philip.smart at jisc.ac.uk
Thu Jan 18 12:00:11 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=59583e6c44f3ecefa299b88f179f0950334c131e
The following commit(s) were added to refs/heads/main by this push:
new 59583e6 Add authentication and registration request param actions and builders
59583e6 is described below
commit 59583e6c44f3ecefa299b88f179f0950334c131e
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Jan 18 12:00:09 2024 +0000
Add authentication and registration request param actions and builders
- Cleanup interfaces
- Cleanup contexts
---
.../admin/CredentialCreationOptionsParameters.java | 268 +++++++++++++++++++++
.../webauthn/authn/BaseOptionsParameters.java | 69 ++++++
.../authn/CredentialRequestOptionsParameters.java | 137 +++++++++++
.../client/WebAuthnAuthenticationClient.java | 38 +--
.../webauthn/context/BaseWebAuthnContext.java | 37 ++-
.../context/WebAuthnAuthenticationContext.java | 17 +-
.../context/WebAuthnRegistrationContext.java | 75 +++++-
.../logic/IsDiscoverableCredentialRequired.java | 2 +-
...CredentialRequired.java => IsSecondFactor.java} | 27 +--
.../CredentialCreationOptionsParametersTest.java | 92 +++++++
.../CredentialRequestOptionsParametersTest.java | 40 +++
.../AddAuthenticatorAttachmentRequirement.java | 83 +++++++
.../admin/impl/AddResidentKeyRequirement.java | 76 ++++++
.../CreatePublicKeyCredentialCreationOptions.java | 55 ++++-
.../impl/YubicoWebauthnAuthenticationClient.java | 57 ++---
.../impl/AddUserVerificationRequirement.java | 77 ++++++
.../CreatePublicKeyCredentialRequestOptions.java | 26 +-
.../impl/EnsureAllowedCredentialsIsEmpty.java | 45 ++++
.../webauthn-registration-beans.xml | 45 ++--
.../webauthn-registration-flow.xml | 3 +
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 49 ++--
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 23 +-
.../authn/webauthn/conf/authn/webauthn.properties | 9 +-
.../plugin/authn/webauthn/views/webauthn-authn.vm | 8 +-
.../authn/webauthn/views/webauthn-register.vm | 22 +-
.../views/webauthn-registration-outcomes.vm | 14 +-
.../authn/webauthn/views/webauthn-selector.vm | 2 +-
.../webauthn/views/webauthn-username-entry.vm | 124 ++++++++++
28 files changed, 1340 insertions(+), 180 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
new file mode 100644
index 0000000..8029a59
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParameters.java
@@ -0,0 +1,268 @@
+/*
+ * 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.admin;
+
+import java.util.Collections;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.authn.BaseOptionsParameters;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class to hold the parameters required to build a PublicKeyCredentialRequestOptions.
+ */
+public class CredentialCreationOptionsParameters extends BaseOptionsParameters {
+
+ /**
+ * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
+ * duplicate credentials during registration.
+ */
+ @Nonnull @NonnullElements final Set<PublicKeyCredentialDescriptor> excludeCredentials;
+
+ /**
+ * The username of the user that is the subject of this authentication.
+ */
+ @Nonnull @NotEmpty final String username;
+
+ /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+ @Nullable final AuthenticatorAttachment authenticatorAttachment;
+
+ /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+ @Nonnull final ResidentKeyRequirement residentKeyRequirement;
+
+ /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+ @Nonnull final byte[] userHandle;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param builder the builder to extract parameters from
+ */
+ @SuppressWarnings("null")
+ private CredentialCreationOptionsParameters(final Builder builder) {
+ super(builder.userVerificationRequirement, builder.challenge);
+ this.excludeCredentials = Constraint.isNotNull(builder.excludeCredentials,
+ "Exclude credentails can not be null");
+ this.username = Constraint.isNotEmpty(builder.username, "Username can not be null or empty");
+ this.residentKeyRequirement = Constraint.isNotNull(builder.residentKeyRequirement,
+ "Resident key requirement can not be null");
+ this.userHandle = Constraint.isNotNull(builder.userHandle, "UserHandle can not be null");
+ this.authenticatorAttachment = builder.authenticatorAttachment;
+ }
+
+ /**
+ * Get the set of credentials that have already been registered with the IdP.
+ *
+ * @return the excludeCredentials.
+ */
+ @Nonnull @NonnullElements public final Set<PublicKeyCredentialDescriptor> getExcludeCredentials() {
+ return excludeCredentials;
+ }
+
+
+ /**
+ * Get the username
+ *
+ * @return the username.
+ */
+ @Nonnull @NotEmpty public final String getUsername() {
+ return username;
+ }
+
+
+ /**
+ * Get the authenticator attachment requirement.
+ *
+ * @return the authenticatorAttachment.
+ */
+ @Nullable public final AuthenticatorAttachment getAuthenticatorAttachment() {
+ return authenticatorAttachment;
+ }
+
+
+ /**
+ * Get the resident key requirement.
+ *
+ * @return the residentKeyRequirement.
+ */
+ @Nonnull public final ResidentKeyRequirement getResidentKeyRequirement() {
+ return residentKeyRequirement;
+ }
+
+
+ /**
+ * Get the user handle.
+ *
+ * @return the userHandle
+ */
+ @Nonnull public final byte[] getUserHandle() {
+ return userHandle;
+ }
+
+
+ /** Get a new builder.*/
+ public static IUserVerificationRequirementStage builder() {
+ return new Builder();
+ }
+
+
+ /** Stage interface.*/
+ public interface IUserVerificationRequirementStage {
+ /** Does the authentication/registration require user verification.*/
+ public IChallengeStage withUserVerificationRequirement(
+ @Nonnull final UserVerificationRequirement userVerificationRequirement);
+ }
+
+
+ /** Stage interface.*/
+ public interface IChallengeStage {
+ /** The challenge sent to the authenticator in registration ceremonies.*/
+ public IExcludeCredentialsStage withChallenge(@Nonnull final byte[] challenge);
+ }
+
+
+ /** Stage interface.*/
+ public interface IExcludeCredentialsStage {
+ /** Credentials that have already been registered with the IdP.*/
+ public IUsernameStage withExcludeCredentials(
+ @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials);
+ }
+
+
+ /** Stage interface.*/
+ public interface IUsernameStage {
+ /** The username of the user that is the subject of this authentication.*/
+ public IResidentKeyRequirementStage withUsername(@Nonnull @NotEmpty final String username);
+ }
+
+
+ /** Stage interface.*/
+ public interface IResidentKeyRequirementStage {
+ /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+ public IUserHandleStage withResidentKeyRequirement(
+ @Nonnull final ResidentKeyRequirement residentKeyRequirement);
+ }
+
+
+ /** Stage interface.*/
+ public interface IUserHandleStage {
+ /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+ public IBuildStage withUserHandle(@Nonnull final byte[] userHandle);
+ }
+
+
+ /** Stage interface.*/
+ public interface IBuildStage {
+ /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+ public IBuildStage withAuthenticatorAttachment(@Nullable AuthenticatorAttachment authenticatorAttachment);
+
+ /** Build the options.*/
+ public CredentialCreationOptionsParameters build();
+ }
+
+
+ /** Builder class.*/
+ public static final class Builder implements IUserVerificationRequirementStage, IChallengeStage,
+ IExcludeCredentialsStage, IUsernameStage, IResidentKeyRequirementStage, IUserHandleStage, IBuildStage {
+
+ /** Does the authentication/registration require user verification.*/
+ private UserVerificationRequirement userVerificationRequirement;
+
+ /** The challenge sent to the authenticator in registration ceremonies.*/
+ private byte[] challenge;
+
+ /** Credentials that have already been registered with the IdP.*/
+ private Set<PublicKeyCredentialDescriptor> excludeCredentials = Collections.emptySet();
+
+ /** The username of the user that is the subject of this authentication.*/
+ private String username;
+
+ /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+ private ResidentKeyRequirement residentKeyRequirement;
+
+ /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+ private byte[] userHandle;
+
+ /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+ private AuthenticatorAttachment authenticatorAttachment;
+
+ /** Constructor.*/
+ private Builder() {
+ }
+
+ @Override
+ public IChallengeStage withUserVerificationRequirement(
+ @Nonnull final UserVerificationRequirement userVerificationRequirement) {
+ this.userVerificationRequirement = userVerificationRequirement;
+ return this;
+ }
+
+ @Override
+ public IExcludeCredentialsStage withChallenge(@Nonnull final byte[] challenge) {
+ this.challenge = challenge;
+ return this;
+ }
+
+ @Override
+ public IUsernameStage withExcludeCredentials(
+ @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials) {
+ this.excludeCredentials = excludeCredentials;
+ return this;
+ }
+
+ @Override
+ public IResidentKeyRequirementStage withUsername(@Nonnull final String username) {
+ this.username = username;
+ return this;
+ }
+
+ @Override
+ public IUserHandleStage withResidentKeyRequirement(
+ @Nonnull final ResidentKeyRequirement residentKeyRequirement) {
+ this.residentKeyRequirement = residentKeyRequirement;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withUserHandle(@Nonnull final byte[] userHandle) {
+ this.userHandle = userHandle;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withAuthenticatorAttachment(
+ @Nullable final AuthenticatorAttachment authenticatorAttachment) {
+ this.authenticatorAttachment = authenticatorAttachment;
+ return this;
+ }
+
+ @Override
+ public CredentialCreationOptionsParameters build() {
+ return new CredentialCreationOptionsParameters(this);
+ }
+ }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.java
new file mode 100644
index 0000000..22d823b
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/BaseOptionsParameters.java
@@ -0,0 +1,69 @@
+/*
+ * 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.authn;
+
+import javax.annotation.Nonnull;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract options parameters class. Common to credential create or request options.
+ */
+public abstract class BaseOptionsParameters {
+
+ /** Does the authentication/registration require user verification.*/
+ @Nonnull protected final UserVerificationRequirement userVerificationRequirement;
+
+ /** The challenge sent to the authenticator in authentication or registration ceremonies.*/
+ @Nonnull protected final byte[] challenge;
+
+ /**
+ * Constructor.
+ *
+ * @param userVerificationRequirement the user verification requirement
+ * @param challenge the challenge
+ */
+ protected BaseOptionsParameters(@Nonnull final UserVerificationRequirement userVerificationRequirement,
+ @Nonnull final byte[] challenge) {
+ super();
+ this.userVerificationRequirement =
+ Constraint.isNotNull(userVerificationRequirement, "UserVerificationRequirement can not be null");
+ this.challenge = Constraint.isNotNull(challenge, "Challenge can not be null");
+ }
+
+ /**
+ * Get the user verification requirement.
+ *
+ * @return the userVerificationRequirement.
+ */
+ @Nonnull public final UserVerificationRequirement getUserVerificationRequirement() {
+ return userVerificationRequirement;
+ }
+
+
+
+ /**
+ * Get the challenge.
+ *
+ * @return the challenge.
+ */
+ @Nonnull public final byte[] getChallenge() {
+ return challenge;
+ }
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java
new file mode 100644
index 0000000..86972f5
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParameters.java
@@ -0,0 +1,137 @@
+/*
+ * 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.authn;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A class to hold the parameters required to build a PublicKeyCredentialRequestOptions.
+ */
+public class CredentialRequestOptionsParameters extends BaseOptionsParameters {
+
+ /**
+ * Credentials that have already been registered with the IdP. The authenticator should use these to avoid creating
+ * duplicate credentials during registration.
+ */
+ @Nonnull @NonnullElements private final List<PublicKeyCredentialDescriptor> allowCredentials;
+
+
+ private CredentialRequestOptionsParameters(final Builder builder) {
+ super(builder.userVerificationRequirement, builder.challenge);
+ Constraint.isNotNull(builder.allowCredentials, "AllowCredentials can not be null");
+ allowCredentials =
+ builder.allowCredentials.stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+
+ }
+
+ /**
+ * Get the set of credentials that are acceptable to the IdP.
+ *
+ * @return the allowable credentials.
+ */
+ @Nonnull @NonnullElements public final List<PublicKeyCredentialDescriptor> getAllowCredentials() {
+ return allowCredentials;
+ }
+
+ /** Create a new builder. */
+ public static IUserVerificationRequirementStage builder() {
+ return new Builder();
+ }
+
+ /** Stage interface.*/
+ public interface IUserVerificationRequirementStage {
+ /** Does the authentication/registration require user verification.*/
+ public IChallengeStage withUserVerificationRequirement(
+ @Nonnull final UserVerificationRequirement userVerificationRequirement);
+ }
+
+ /** Stage interface.*/
+ public interface IChallengeStage {
+ /** The challenge sent to the authenticator in authentication ceremonies.*/
+ public IAllowCredentialsStage withChallenge(@Nonnull final byte[] challenge);
+ }
+
+ /** Stage interface.*/
+ public interface IAllowCredentialsStage {
+ /** Credentials that have already been registered with the IdP.**/
+ public IBuildStage withAllowCredentials(@Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials);
+ }
+
+ /** Build the options.*/
+ public interface IBuildStage {
+ public CredentialRequestOptionsParameters build();
+ }
+
+ /** Builder class.*/
+ public static final class Builder
+ implements IUserVerificationRequirementStage, IChallengeStage, IAllowCredentialsStage, IBuildStage {
+
+ /** Does the authentication/registration require user verification.*/
+ private UserVerificationRequirement userVerificationRequirement;
+
+ /** The challenge sent to the authenticator in authentication ceremonies.*/
+ private byte[] challenge;
+
+ /** Credentials that have already been registered with the IdP.**/
+ private List<PublicKeyCredentialDescriptor> allowCredentials = CollectionSupport.emptyList();
+
+ /** Constructor.*/
+ private Builder() {
+ }
+
+ @Override
+ public IChallengeStage withUserVerificationRequirement(
+ @Nonnull final UserVerificationRequirement userVerificationRequirement) {
+ this.userVerificationRequirement = userVerificationRequirement;
+ return this;
+ }
+
+ @Override
+ public IAllowCredentialsStage withChallenge(@Nonnull final byte[] challenge) {
+ this.challenge = challenge;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withAllowCredentials(@Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials) {
+ this.allowCredentials = allowCredentials;
+ return this;
+ }
+
+ @Override
+ public CredentialRequestOptionsParameters build() {
+ return new CredentialRequestOptionsParameters(this);
+ }
+ }
+
+
+
+
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
index 701a4d2..ed95c88 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
@@ -1,8 +1,5 @@
package net.shibboleth.idp.plugin.authn.webauthn.client;
-import java.util.List;
-import java.util.Set;
-
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
@@ -15,15 +12,16 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
/**
- * A client that manages the entire webauthn authentication and registration ceremony.
+ * A client that manages a WebAuthn authentication or registration ceremony.
*
* <p>Tied to the Yubico library data model, other clients will need to be adapted to support this model, and provide
* translations between their internal representation and the types required.</p>
@@ -36,41 +34,30 @@ public interface WebAuthnAuthenticationClient {
/**
* Create a PublicKeyCredentialRequestOptions for the WebAuthn 'get' call to generate an authentication assertion.
+ *
+ * @param requestParams the options that should be present in the authentication request.
*
- * @param username the username of the username that has been pre-identified. Can be null
- * if no user has been identified, and the IdP is requesting the client discover
- * the credential.
- * @param allowCredentials the set of public key credentials acceptable to the IdP in descending order of
- * preference
- * @param challenge the challenge the authenticator signs along with other data to produce an assertion
- *
- * @return a PublicKeyCredentialRequestOptions object
+ * @return a PublicKeyCredentialRequestOptions object to supply the WebAuthn 'get' call
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
*
*/
- @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
- @Nonnull final List<PublicKeyCredentialDescriptor> allowCredentials, @Nonnull final byte[] challenge)
+ @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(
+ @Nonnull final CredentialRequestOptionsParameters requestParams)
throws WebAuthnAuthenticationClientException;
/**
* Create a PublicKeyCredentialCreationOptions for the WebAuthn 'create' call to generate a registration
* attestation.
*
- * @param username the username of the username that has been pre-identified. Can be {@code null}
- * if no user has been identified, and the IdP is requesting the client discover
- * the credential.
- * @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
- * @param the challenge to sign when creating new credentials
+ * @param creationOptions the options that should be present in the registration request.
*
- * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call.
+ * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call
*
* @throws WebAuthnAuthenticationClientException if there is an error generating the creation request
*/
- @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(
- @Nonnull final Set<PublicKeyCredentialDescriptor> excludeCredentials, @Nullable final String username,
- @Nullable final byte[] userHandle, @Nonnull final byte[] challenge)
- throws WebAuthnAuthenticationClientException;
+ @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(@Nonnull final
+ CredentialCreationOptionsParameters creationOptions) throws WebAuthnAuthenticationClientException;
/**
@@ -85,6 +72,7 @@ public interface WebAuthnAuthenticationClient {
*
* @throws WebAuthnAuthenticationClientException if the assertion is not valid
*/
+ //TODO would need our own AssertionResult to make this usuable beyond Yubico.
AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username,
@Nullable final byte[] userHandle,
@Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
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 ff00804..fc63106 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
@@ -21,6 +21,8 @@ import javax.annotation.Nullable;
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.NotLive;
import net.shibboleth.shared.annotation.constraint.Unmodifiable;
@@ -51,7 +53,10 @@ public class BaseWebAuthnContext extends BaseContext {
@Nullable private byte[] serverChallenge;
/** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
- @Nullable private byte[] userHandle;
+ @Nullable private byte[] userHandle;
+
+ /** Does the authentication/registration require user verification.*/
+ @Nullable private UserVerificationRequirement userVerificationRequirement;
/**
* Gets the username.
@@ -79,9 +84,10 @@ public class BaseWebAuthnContext extends BaseContext {
*
* @param credentials the set of credentials
*/
- public void setExistingCredentials(@Nullable final Collection<CredentialRegistration> credentials) {
+ @Nonnull public BaseWebAuthnContext setExistingCredentials(
+ @Nullable final Collection<CredentialRegistration> credentials) {
existingCredentials = credentials;
-
+ return this;
}
/**
@@ -132,7 +138,7 @@ public class BaseWebAuthnContext extends BaseContext {
*
* @param handle The userHandle to set.
*/
- public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
+ @Nonnull public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
Constraint.isNotEmpty(handle,"UserHandle can not be null or empty");
Constraint.isLessThan(65, handle.length, "UserHandle must be maximum 64 bytes");
userHandle = handle;
@@ -144,8 +150,29 @@ public class BaseWebAuthnContext extends BaseContext {
*
* @return the userHandle.
*/
- public byte[] getUserHandle() {
+ @Nullable public byte[] getUserHandle() {
return userHandle;
}
+
+
+ /**
+ * Set the user verification requirement.
+ *
+ * @param requirement The user verification requirement to set.
+ */
+ @Nonnull public BaseWebAuthnContext setUserVerificationRequirement(
+ @Nullable final UserVerificationRequirement requirement) {
+ userVerificationRequirement = requirement;
+ return this;
+ }
+
+ /**
+ * Get the user verification requirement.
+ *
+ * @return the user verification requirement.
+ */
+ @Nullable public UserVerificationRequirement getUserVerificationRequirement() {
+ return userVerificationRequirement;
+ }
}
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 61c57f1..a6b95a6 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
@@ -15,10 +15,7 @@ import net.shibboleth.shared.logic.Constraint;
/** Authentication context for processing WebAuthn Authentication Ceremonies. */
@NotThreadSafe
public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
-
- /** The credential public key encoded in COSE_Key format.*/
- @Nullable private byte[] publicKey;
-
+
/** The credential identifier generated by the authenticator.*/
@Nullable private byte[] credentialId;
@@ -33,17 +30,6 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
/** The public key credential request options for authentication represented as a JSON string.*/
@Nullable private String publicKeyCredentialRequestOptionsJSON;
-
- /**
- * Set the public key, as a byte array, in COSE_Key format.
- *
- * @param key the public key in COSE_Key format.
- */
- public WebAuthnAuthenticationContext setPublicKey(@Nonnull final byte[] key) {
- publicKey = Constraint.isNotNull(key, "Public key can not be null");
- return this;
- }
-
/**
* Get the credential Id.
*
@@ -122,4 +108,5 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
return this;
}
+
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
index 6c7c8f1..aee6d36 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
@@ -1,13 +1,16 @@
package net.shibboleth.idp.plugin.authn.webauthn.context;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAttachment;
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
/**
@@ -49,11 +52,38 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
/** The ID of the credential that is going to be removed.*/
@Nullable private byte[] credentialIdToRemove;
+
+ /** The requirement on registering a ResidentKey. Also know as a discoverable credential.*/
+ @Nullable private ResidentKeyRequirement residentKeyRequirement;
+
+ /** The AuthenticatorAttachment requirement. {@code null} would represent either possibility.*/
+ @Nullable private AuthenticatorAttachment authenticatorAttachmentRequirement;
+
+ /**
+ * Set the AuthenticatorAttachment requirement. {@code null} would represent either possibility.
+ *
+ * @param authenticatorAttachmentRequirement The authenticatorAttachmentRequirement to set.
+ */
+ public WebAuthnRegistrationContext setAuthenticatorAttachmentRequirement(@Nullable final AuthenticatorAttachment requirement) {
+ authenticatorAttachmentRequirement = requirement;
+ return this;
+ }
+
+ /**
+ *
+ * Get the AuthenticatorAttachment requirement. {@code null} would represent either possibility.
+ *
+ * @return the authenticator attachment requirement.
+ */
+ public AuthenticatorAttachment getAuthenticatorAttachmentRequirement() {
+ return authenticatorAttachmentRequirement;
+ }
+
/**
* Get the attestation response as a result of creating a new credential.
*
- * @return Returns the authenticator attestation response.
+ * @return the authenticator attestation response.
*/
@Nullable public PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
getAuthenticatorAttestationResponse() {
@@ -65,7 +95,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
*/
- public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(
+ @Nonnull public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(
@Nullable final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
attestation) {
authenticatorAttestationResponse = attestation;
@@ -77,7 +107,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param options The publicKeyCredentialCreationOptions to set.
*/
- public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
+ @Nonnull public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
@Nullable final PublicKeyCredentialCreationOptions options) {
publicKeyCredentialCreationOptions = options;
return this;
@@ -98,7 +128,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param registrationResult The registrationResult to set.
*/
- public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
+ @Nonnull public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
registrationResult = result;
return this;
}
@@ -109,21 +139,25 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @return Returns the registrationResult.
*/
- public RegistrationResult getRegistrationResult() {
+ @Nullable public RegistrationResult getRegistrationResult() {
return registrationResult;
}
/**
- * @return Returns the publicKeyCredentialCreationOptionsJSON.
+ * Get the credential public key registration options in JSON format.
+ *
+ * @return the publicKeyCredentialCreationOptions in JSON format.
*/
- public String getPublicKeyCredentialCreationOptionsJSON() {
+ @Nullable public String getPublicKeyCredentialCreationOptionsJSON() {
return publicKeyCredentialCreationOptionsJSON;
}
/**
- * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptionsJSON to set.
+ * Set the credential public key registration options in JSON format.
+ *
+ * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptions in JSON to set.
*/
- public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
+ @Nonnull public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
publicKeyCredentialCreationOptionsJSON = optionsJSON;
return this;
}
@@ -134,7 +168,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param nickname The credential nickname to set.
*/
- public WebAuthnRegistrationContext setCredentialNickname(@Nullable final String nickname) {
+ @Nonnull public WebAuthnRegistrationContext setCredentialNickname(@Nullable final String nickname) {
credentialNickname = nickname;
return this;
}
@@ -153,7 +187,7 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*
* @param credentialId the credential identifier
*/
- public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
+ @Nonnull public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
credentialIdToRemove = id;
return this;
}
@@ -167,4 +201,23 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
return credentialIdToRemove;
}
+ /**
+ * Set the ResidentKey requirement. Also know as a discoverable credential.
+ *
+ * @param requirement The resident key requirement to set.
+ */
+ @Nonnull public WebAuthnRegistrationContext setResidentKeyRequirement(
+ @Nullable final ResidentKeyRequirement requirement) {
+ residentKeyRequirement = requirement;
+ return this;
+ }
+
+ /**
+ * Get the ResidentKey requirement. Also know as a discoverable credential.
+ *
+ * @return the requirement.
+ */
+ @Nullable public ResidentKeyRequirement getResidentKeyRequirement() {
+ return residentKeyRequirement;
+ }
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
index 3db25ca..d8b3800 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
@@ -28,7 +28,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
/**
* A predicate that determines if the authentication ceremony requires a discoverable credential supplied by the
- * authenticator, or if we have a username to determine which credentials to use on from the IdP.
+ * authenticator, or if we have a username to determine which credentials to use from the IdP.
*/
public class IsDiscoverableCredentialRequired implements Predicate<ProfileRequestContext> {
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
similarity index 58%
copy from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
copy to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
index 3db25ca..b680f82 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsDiscoverableCredentialRequired.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/logic/IsSecondFactor.java
@@ -23,17 +23,16 @@ 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.primitive.LoggerFactory;
/**
- * A predicate that determines if the authentication ceremony requires a discoverable credential supplied by the
- * authenticator, or if we have a username to determine which credentials to use on from the IdP.
+ * A predicate that determines if the authentication flow is being used as a second factor of authentication, and not
+ * a first (and possibly only) factor. Returns true if second factor use, or false if passwordless/first factor.
*/
-public class IsDiscoverableCredentialRequired implements Predicate<ProfileRequestContext> {
+public class IsSecondFactor implements Predicate<ProfileRequestContext> {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(IsDiscoverableCredentialRequired.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(IsSecondFactor.class);
@Override
public boolean test(@Nullable final ProfileRequestContext input) {
@@ -46,16 +45,14 @@ public class IsDiscoverableCredentialRequired implements Predicate<ProfileReques
log.trace("Authentication context was null, can not determine if discoverable credentials are required");
return false;
}
- final WebAuthnAuthenticationContext webauthnContext =
- authnContext.getSubcontext(WebAuthnAuthenticationContext.class);
- if (webauthnContext == null) {
- log.trace("WebAuthn authentication context was null, can not determine if discoverable credentials "
- + "are required");
- return false;
+
+ //TODO no decision here yet.
+ final boolean secondFactor = false;
+ if (secondFactor) {
+ log.debug("Request contained a previous factor, assuming second factor");
+ return true;
}
- final boolean discoverableCredentialRequired = webauthnContext.getUsername() == null;
- log.debug("{}", discoverableCredentialRequired ? "Usernameless authentication required" :
- "Passwordless authentication required for '"+webauthnContext.getUsername()+"'");
- return discoverableCredentialRequired;
+ log.debug("Request did not contain a previous factor");
+ return false;
}
}
diff --git a/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java
new file mode 100644
index 0000000..eb0980a
--- /dev/null
+++ b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/CredentialCreationOptionsParametersTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.admin;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link CredentialCreationOptionsParameters}.
+ */
+public class CredentialCreationOptionsParametersTest {
+
+ @Test
+ public void testBuilder() {
+ final var options = CredentialCreationOptionsParameters.builder()
+ .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+ .withChallenge(new byte[0])
+ .withExcludeCredentials(CollectionSupport.emptySet())
+ .withUsername("username")
+ .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+ .withUserHandle(new byte[0])
+ .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+ .build();
+ assertNotNull(options);
+ assertEquals(options.getUsername(), "username");
+ }
+
+ @SuppressWarnings("null")
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testBuilderNullUsername() {
+ CredentialCreationOptionsParameters.builder()
+ .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+ .withChallenge(new byte[0])
+ .withExcludeCredentials(CollectionSupport.emptySet())
+ .withUsername(null)
+ .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+ .withUserHandle(new byte[0])
+ .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+ .build();
+ }
+
+ @SuppressWarnings("null")
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testBuilderNullUserHandle() {
+ CredentialCreationOptionsParameters.builder()
+ .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+ .withChallenge(new byte[0])
+ .withExcludeCredentials(CollectionSupport.emptySet())
+ .withUsername("username")
+ .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+ .withUserHandle(null)
+ .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+ .build();
+ }
+
+ @SuppressWarnings("null")
+ @Test(expectedExceptions = ConstraintViolationException.class)
+ public void testBuilderNullExcludeCredentials() {
+ CredentialCreationOptionsParameters.builder()
+ .withUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED)
+ .withChallenge(new byte[0])
+ .withExcludeCredentials(null)
+ .withUsername("username")
+ .withResidentKeyRequirement(ResidentKeyRequirement.DISCOURAGED)
+ .withUserHandle(new byte[0])
+ .withAuthenticatorAttachment(AuthenticatorAttachment.CROSS_PLATFORM)
+ .build();
+
+ }
+
+}
diff --git a/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java
new file mode 100644
index 0000000..467ab7e
--- /dev/null
+++ b/webauthn-api/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/authn/CredentialRequestOptionsParametersTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.authn;
+
+import static org.testng.Assert.assertNotNull;
+
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Test for {@link CredentialRequestOptionsParameters}
+ */
+public class CredentialRequestOptionsParametersTest {
+
+ @Test
+ public void CredentialRequestOptionsParameters() {
+ final var options = CredentialRequestOptionsParameters.builder()
+ .withUserVerificationRequirement(UserVerificationRequirement.PREFERRED)
+ .withChallenge(new byte[0])
+ .withAllowCredentials(CollectionSupport.emptyList())
+ .build();
+ assertNotNull(options);
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java
new file mode 100644
index 0000000..c47e986
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirement.java
@@ -0,0 +1,83 @@
+/*
+ * 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.admin.impl;
+
+import java.util.stream.Stream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a ResidentKey requirement to the {@link WebAuthnRegistrationContext context}.
+ */
+public class AddAuthenticatorAttachmentRequirement extends AbstractWebAuthnRegistrationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AddAuthenticatorAttachmentRequirement.class);
+
+ /** Set the AuthenticatorAttachment requirement. Default is null, so any.*/
+ @Nullable private AuthenticatorAttachment authenticatorAttachmentRequirement;
+
+ /** Constructor.*/
+ public AddAuthenticatorAttachmentRequirement() {
+ authenticatorAttachmentRequirement = null;
+ }
+
+ /**
+ * Set the AuthenticatorAttachment requirement.
+ *
+ * @param requirement The AuthenticatorAttachment requirement to set.
+ */
+ public void setAuthenticatorAttachmentRequirement(@Nonnull @NotEmpty final String requirement) {
+ checkSetterPreconditions();
+ Constraint.isNotEmpty(requirement, "AuthenticatorAttachment requirement can not be null or empty");
+
+ if ("any".equals(requirement)) {
+ authenticatorAttachmentRequirement = null;
+ } else {
+ final AuthenticatorAttachment aaRequirement =
+ Stream.of(AuthenticatorAttachment.values())
+ .filter(aa -> aa.getValue().equals(requirement))
+ .findAny()
+ .orElseThrow(() ->
+ new ConstraintViolationException("AuthenticatorAttachment requirement "+requirement+" unknown"));
+ assert aaRequirement != null;
+ authenticatorAttachmentRequirement = aaRequirement;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(final ProfileRequestContext profileRequestContext,
+ final WebAuthnRegistrationContext context) {
+
+ log.debug("{} AuthenticatorAttachment is '{}'",getLogPrefix(), authenticatorAttachmentRequirement != null ?
+ authenticatorAttachmentRequirement : "ANY");
+ context.setAuthenticatorAttachmentRequirement(authenticatorAttachmentRequirement);
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java
new file mode 100644
index 0000000..e22d9d5
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirement.java
@@ -0,0 +1,76 @@
+/*
+ * 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.admin.impl;
+
+import java.util.stream.Stream;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a ResidentKey requirement to the {@link WebAuthnRegistrationContext context}.
+ */
+public class AddResidentKeyRequirement extends AbstractWebAuthnRegistrationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AddResidentKeyRequirement.class);
+
+ /** Set the ResidentKey requirement. Default is PREFERRED.*/
+ @Nonnull private ResidentKeyRequirement residentKeyRequirement;
+
+ /** Constructor.*/
+ public AddResidentKeyRequirement() {
+ residentKeyRequirement = ResidentKeyRequirement.PREFERRED;
+ }
+
+ /**
+ * Set the ResidentKey requirement.
+ *
+ * @param requirement The ResidentKey requirement to set.
+ */
+ public void setResidentKeyRequirement(@Nonnull @NotEmpty final String requirement) {
+ checkSetterPreconditions();
+ Constraint.isNotEmpty(requirement, "ResidentKey requirement can not be null or empty");
+
+ final ResidentKeyRequirement uvRequirement =
+ Stream.of(ResidentKeyRequirement.values())
+ .filter(rk -> rk.getValue().equals(requirement))
+ .findAny()
+ .orElseThrow(() -> new ConstraintViolationException("ResidentKey requirement "+requirement+" unknown"));
+ assert uvRequirement != null;
+ residentKeyRequirement = uvRequirement;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(final ProfileRequestContext profileRequestContext,
+ final WebAuthnRegistrationContext context) {
+
+ log.debug("{} ResidentKey is '{}'",getLogPrefix(), residentKeyRequirement);
+ context.setResidentKeyRequirement(residentKeyRequirement);
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
index b225147..e08b9ee 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -30,13 +30,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -81,12 +85,37 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
final byte[] challenge = context.getServerChallenge();
if (challenge == null) {
- log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
+ log.error("{} WebAuthn challenge is null",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
-
- try {
+ final ResidentKeyRequirement residentKeyRequirement = context.getResidentKeyRequirement();
+ if (residentKeyRequirement == null) {
+ log.error("{} ResidentKeyRequirement is null",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final UserVerificationRequirement uvRequirement = context.getUserVerificationRequirement();
+ if (uvRequirement == null) {
+ log.error("{} UserVerificationRequirement is null",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final String username = context.getUsername();
+ if (username == null) {
+ log.error("{} Username is null",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ final byte[] userHandle = context.getUserHandle();
+ if (userHandle == null) {
+ log.error("{} UserHandle is null",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ try {
+
final Set<PublicKeyCredentialDescriptor> existingCredentialDescriptors = context.getExistingCredentials()
.stream()
.map(cred -> cred.toPublicKeyCredentialDescriptor())
@@ -94,21 +123,31 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRe
.collect(Collectors.toSet());
assert null != existingCredentialDescriptors;
-
+
+ final CredentialCreationOptionsParameters creationOptions = CredentialCreationOptionsParameters.builder()
+ .withUserVerificationRequirement(uvRequirement)
+ .withChallenge(challenge)
+ .withExcludeCredentials(existingCredentialDescriptors)
+ .withUsername(username)
+ .withResidentKeyRequirement(residentKeyRequirement)
+ .withUserHandle(userHandle).withAuthenticatorAttachment(
+ context.getAuthenticatorAttachmentRequirement())
+ .build();
+
+ assert creationOptions != null;
final PublicKeyCredentialCreationOptions pkCredCreationOptions =
- getWebAuthnClient().createRegistrationRequest(existingCredentialDescriptors, context.getUsername(),
- context.getUserHandle(), challenge);
+ getWebAuthnClient().createRegistrationRequest(creationOptions);
context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
//convert to JSON for the JS API to use
context.setPublicKeyCredentialCreationOptionsJSON(objectMapper.writeValueAsString(pkCredCreationOptions));
log.debug("{} Created PublicKeyCredentialCreationOptions '{}'",getLogPrefix(), pkCredCreationOptions);
- } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
+ } catch (final WebAuthnAuthenticationClientException | JsonProcessingException | ConstraintViolationException e) {
log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
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 28b4cdf..25e0f61 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
@@ -18,7 +18,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -41,14 +40,13 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
import com.yubico.webauthn.data.PublicKeyCredential;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialParameters;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
-import com.yubico.webauthn.data.ResidentKeyRequirement;
import com.yubico.webauthn.data.UserIdentity;
-import com.yubico.webauthn.data.UserVerificationRequirement;
import com.yubico.webauthn.exception.RegistrationFailedException;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
@@ -99,26 +97,19 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
}
@Override
- public PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username,
- @Nullable final List<PublicKeyCredentialDescriptor> allowCredentials, @Nonnull final byte[] challenge)
- throws WebAuthnAuthenticationClientException {
-
- //set default to preferred.
- UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
- if (username == null) {
- //then require user verification if no username is known.
- userVerificationRequirement = UserVerificationRequirement.REQUIRED;
- }
+ public PublicKeyCredentialRequestOptions createAuthenticationRequest(
+ @Nonnull final CredentialRequestOptionsParameters requestParams)
+ throws WebAuthnAuthenticationClientException {
final PublicKeyCredentialRequestOptions request = PublicKeyCredentialRequestOptions.builder()
- .challenge(new ByteArray(challenge))
+ .challenge(new ByteArray(requestParams.getChallenge()))
.rpId(rp.getIdentity().getId())
- .allowCredentials(Optional.ofNullable(allowCredentials))
+ .allowCredentials(Optional.ofNullable(requestParams.getAllowCredentials()))
// .extensions(
// startAssertionOptions
// .getExtensions()
// .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
- .userVerification(userVerificationRequirement)
+ .userVerification(requestParams.getUserVerificationRequirement())
.timeout(Optional.of(60000l))
.build();
if (request == null) {
@@ -131,35 +122,27 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
/** {@inheritDoc} */
@Override
public PublicKeyCredentialCreationOptions createRegistrationRequest(
- @Nullable final Set<PublicKeyCredentialDescriptor> excludeCredentials, @Nullable final String username,
- final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
-
- //set default to preferred.
- ResidentKeyRequirement residentKeyRquirement = ResidentKeyRequirement.PREFERRED;
- if (username == null) {
- // FIXME Fix this
- //then require a resident key, but this can not happen during registration
- residentKeyRquirement = ResidentKeyRequirement.REQUIRED;
+ @Nonnull final CredentialCreationOptionsParameters creationOptions)
+ throws WebAuthnAuthenticationClientException {
- }
-
- // If U2F we do not need UV. So conditional required.
- final UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
final UserIdentity identity =
- UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
+ UserIdentity.builder().name(creationOptions.getUsername())
+ .displayName(creationOptions.getUsername())
+ .id(new ByteArray(creationOptions.getUserHandle()))
+ .build();
final PublicKeyCredentialCreationOptions creation = PublicKeyCredentialCreationOptions.builder()
.rp(rp.getIdentity())
.user(identity)
- .challenge(new ByteArray(challenge))
+ .challenge(new ByteArray(creationOptions.getChallenge()))
.pubKeyCredParams(preferredPublickeyParams)
- .excludeCredentials(excludeCredentials)
+ .excludeCredentials(creationOptions.getExcludeCredentials())
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
- .userVerification(userVerificationRequirement)
- .residentKey(residentKeyRquirement)
+ .userVerification(creationOptions.getUserVerificationRequirement())
+ .residentKey(creationOptions.getResidentKeyRequirement())
+ .authenticatorAttachment(creationOptions.getAuthenticatorAttachment())
.build())
-// .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection())
// .extensions(
// startRegistrationOptions
// .getExtensions()
@@ -197,7 +180,7 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
log.debug("Attempting validation of credential with known username '{}' and userHandle '{}'",
username, userHandle);
}
-
+
final AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(requestAssertion)
.response(authenticatorAssertionResponse)
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java
new file mode 100644
index 0000000..01e09ad
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirement.java
@@ -0,0 +1,77 @@
+/*
+ * 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.stream.Stream;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Add a UserVerification requirement to the {@link WebAuthnAuthenticationContext context}.
+ */
+public class AddUserVerificationRequirement extends AbstractWebAuthnBaseAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AddUserVerificationRequirement.class);
+
+ /** Set the user verification requirement.*/
+ @Nonnull private UserVerificationRequirement userVerificationRequirement;
+
+ /** Constructor.*/
+ public AddUserVerificationRequirement() {
+ userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+ }
+
+ /**
+ * Set the UserVerification requirement.
+ *
+ * @param requirement The userVerificationRequirement to set.
+ */
+ public void setUserVerificationRequirement(@Nonnull @NotEmpty final String requirement) {
+ checkSetterPreconditions();
+ Constraint.isNotEmpty(requirement, "userVerificationRequirement can not be null or empty");
+
+ final UserVerificationRequirement uvRequirement =
+ Stream.of(UserVerificationRequirement.values())
+ .filter(uv -> uv.getValue().equals(requirement))
+ .findAny()
+ .orElseThrow(() -> new ConstraintViolationException("UserVerification requirement "+requirement+" unknown"));
+ assert uvRequirement != null;
+ userVerificationRequirement = uvRequirement;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final BaseWebAuthnContext context) {
+
+ log.debug("{} UserVerification is '{}'",getLogPrefix(), userVerificationRequirement);
+ context.setUserVerificationRequirement(userVerificationRequirement);
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
index 72129a9..e8da523 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -18,6 +18,7 @@ package net.shibboleth.idp.plugin.authn.webauthn.impl;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import javax.annotation.Nonnull;
@@ -29,14 +30,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+import com.yubico.webauthn.data.UserVerificationRequirement;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
@@ -84,7 +88,13 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
final byte[] challenge = context.getServerChallenge();
if (challenge == null) {
- log.error("{} WebAuthn challenge is null, has the context been created correctly?",getLogPrefix());
+ log.error("{} WebAuthn challenge is null",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ return;
+ }
+ final UserVerificationRequirement uvRequirement = context.getUserVerificationRequirement();
+ if (uvRequirement == null) {
+ log.error("{} User verification requirement is null",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
return;
}
@@ -94,13 +104,19 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
final List<PublicKeyCredentialDescriptor> existingCredentialDescriptors = existingCredentials.stream()
.map(cred -> cred.toPublicKeyCredentialDescriptor())
.filter(Objects::nonNull)
- .toList();
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+
+ final CredentialRequestOptionsParameters requestParams = CredentialRequestOptionsParameters.builder()
+ .withUserVerificationRequirement(uvRequirement)
+ .withChallenge(challenge)
+ .withAllowCredentials(existingCredentialDescriptors)
+ .build();
final PublicKeyCredentialRequestOptions pkCredRequestOptions =
- client.createAuthenticationRequest(context.getUsername(), existingCredentialDescriptors,
- challenge);
+ client.createAuthenticationRequest(requestParams);
+
context.setPublicKeyCredentialRequestOptions(pkCredRequestOptions);
- // Convert to JSON for the view. TODO maybe that should be converted by velocity
+ // Convert to JSON for the view. TODO maybe that should be converted by velocity etc.
context.setPublicKeyCredentialRequestOptionsJSON(objectMapper.writeValueAsString(pkCredRequestOptions));
log.debug("{} Created PublicKeyCredentialRequestOptions: '{}'",getLogPrefix(), pkCredRequestOptions);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java
new file mode 100644
index 0000000..909c86a
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/EnsureAllowedCredentialsIsEmpty.java
@@ -0,0 +1,45 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+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.primitive.LoggerFactory;
+
+/**
+ * An action that blanks any existing registered credentials in the WebAuthn context.
+ */
+//TODO should never be necessary given the flow, but just to make it explicit?
+public class EnsureAllowedCredentialsIsEmpty extends AbstractWebAuthnAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(EnsureAllowedCredentialsIsEmpty.class);
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext,
+ @Nonnull final WebAuthnAuthenticationContext context) {
+
+ log.trace("{} Removing any existing credentials found from the authentication request", getLogPrefix());
+ context.setExistingCredentials(null);
+ }
+
+}
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 8695eb7..0933b6c 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
@@ -12,6 +12,12 @@
<bean id="shibboleth.AdminProfileId" class="java.lang.String"
c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
+ <!-- Flow Functions -->
+
+ <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+ 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">
@@ -20,38 +26,45 @@
class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy" />
</property>
</bean>
-
- <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
- class="org.opensaml.messaging.context.navigate.ChildContextLookup"
- c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
-
+
+ <bean id="AddResidentKeyRequirement" scope="prototype" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddResidentKeyRequirement"
+ p:residentKeyRequirement="%{idp.authn.webauthn.registration.residentKey:preferred}" />
+
+ <bean id="AddAuthenticatorAttachmentRequirement" scope="prototype" parent="AbstractWebAuthnRegistrationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AddAuthenticatorAttachmentRequirement"
+ p:authenticatorAttachmentRequirement="%{idp.authn.webauthn.registration.authenticatorAttachment:any}" />
+
+ <bean id="AddUserVerificationRequired" parent="AbstractWebAuthnBaseAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+ p:userVerificationRequirement="%{idp.authn.webauthn.registration.userVerification:discouraged}" />
+
<bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
- p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
<bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
- p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
-
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
<bean id="GenerateUserHandle" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.GenerateUserHandle" />
<bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"
- p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"/>
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions"
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
<bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractAuthenticatorAttestationFromFormRequest"
- p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
- p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
-
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
<bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyRemovalInformationFromFormRequest"
- p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
-
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
<bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
- class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential"/>
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential" />
<bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
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 b6b95ad..f2517d3 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
@@ -28,6 +28,9 @@
<evaluate expression="LookupRegisteredCredentials"/>
<evaluate expression="GenerateServerChallenge"/>
<evaluate expression="GenerateUserHandle"/>
+ <evaluate expression="AddResidentKeyRequirement"/>
+ <evaluate expression="AddAuthenticatorAttachmentRequirement"/>
+ <evaluate expression="AddUserVerificationRequired"/>
<evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="DisplayWebAuthnView" />
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 5d3ac72..8b8ff30 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
@@ -9,42 +9,59 @@
default-init-method="initialize" default-destroy-method="destroy">
- <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
+ <!-- Functions -->
+ <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+ parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
+ c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+
+
+ <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
p:usernameRequiredPredicate="false">
</bean>
-
- <bean id="IsDiscoverableCredentialRequired" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsDiscoverableCredentialRequired"/>
-
+
+ <bean id="IsSecondFactor" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsSecondFactor" />
+
+ <bean id="IsDiscoverableCredentialRequired" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.logic.IsDiscoverableCredentialRequired" />
+
<bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext) }" />
+
+ <bean id="EnsureAllowedCredentialsIsEmpty" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.EnsureAllowedCredentialsIsEmpty"/>
+
+ <bean id="AddUserVerificationRequired" parent="AbstractWebAuthnBaseAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+ p:userVerificationRequirement="required" />
+
+ <bean id="AddUserVerificationNotRequired" parent="AbstractWebAuthnAuthenticationAction"
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.AddUserVerificationRequirement" scope="prototype"
+ p:userVerificationRequirement="discouraged" />
- <bean id ="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
- parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
- c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
-
<bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
- p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"/>
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
<bean id="GenerateServerChallenge" parent="AbstractWebAuthnBaseAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge"
- p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"/>
-
+ p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext" />
+
<bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"
- p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper"/>
-
+ p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
<bean id="ExtractAuthenticatorAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractAuthenticatorAssertionFromFormRequest"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
<bean id="ValidateWebAuthnAssertion" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion"
- p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
+ class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}" />
</beans>
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 9a134ce..27aca88 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
@@ -5,23 +5,42 @@
<action-state id="PopulateWebauthnContext">
<evaluate expression="PopulateWebAuthnAuthenticationContext"/>
<evaluate expression="'proceed'" />
- <transition on="proceed" to="DetermineUsernamelessOrPasswordlessLogin" />
+ <transition on="proceed" to="DetermineSecondFactorLogin" />
</action-state>
+ <!-- Test if we are operating as a 2FA -->
+ <decision-state id="DetermineSecondFactorLogin">
+ <if test="IsSecondFactor.test(opensamlProfileRequestContext)"
+ then="SecondFactorLogin"
+ else="DetermineUsernamelessOrPasswordlessLogin" />
+ </decision-state>
+
+ <!-- Test if we are operating as a first (and possibly only) factor -->
<decision-state id="DetermineUsernamelessOrPasswordlessLogin">
<if test="IsDiscoverableCredentialRequired.test(opensamlProfileRequestContext)"
then="UsernamelessLogin"
else="PasswordlessLogin" />
</decision-state>
- <!-- If passwordless and not usernameless we need a different flow here. Also User verification not required for 2FA-->
+ <!-- If Usernameless: require ResidentKey, UV, UP, and provide no previous credentials-->
<action-state id="UsernamelessLogin">
+ <evaluate expression="AddUserVerificationRequired"/>
+ <evaluate expression="EnsureAllowedCredentialsIsEmpty"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />
</action-state>
+ <!-- If Passwordless: do not require ResidentKey, require UV, UP, and provide previous credentials based on username -->
<action-state id="PasswordlessLogin">
<evaluate expression="LookupRegisteredCredentials"/>
+ <evaluate expression="AddUserVerificationRequired"/>
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />
+ </action-state>
+
+ <!-- If we are running after a first factor, perform 2FA only -->
+ <action-state id="SecondFactorLogin">
+ <evaluate expression="AddUserVerificationNotRequired"/>
<evaluate expression="'proceed'" />
<transition on="proceed" to="GenerateAuthenticationCeremonyOptions" />
</action-state>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
index c746019..5892659 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
@@ -7,4 +7,11 @@ idp.authn.webauthn.allowOriginPort = true
idp.authn.webauthn.allowOriginSubdomain = false
## Display debug information about the registration and authentication ceremony on their respective views?
-#idp.authn.webauthn.ui.debug = false
\ No newline at end of file
+#idp.authn.webauthn.ui.debug = false
+
+## Registration properties
+# idp.authn.webauthn.registration.residentKey = preferred
+### The authenticatorAttachment requirement. One-of 'any', 'cross-platform', or 'platform'.
+# idp.authn.webauthn.registration.authenticatorAttachment = any
+### Require User Verification
+# idp.authn.webauthn.registration.userVerification = discouraged
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
index b462f3b..5ccd4b3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -76,17 +76,17 @@
<form id="authn-form" action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
<input type="hidden" id="publicKeyAssertion" name="publicKeyAssertion"/>
- <button class="hidden" id="authenticationSubmit" type="submit" name="_eventId_proceed">Submit Authentication</button>
+ <button class="hidden" id="authenticationSubmit" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.authn.submit", "Submit Authentication")</button>
</form>
#if($debug == "true")
<div class="centre">
- <button id="authenticate" class="form-element form-button">Authenticate</button>
+ <button id="authenticate" class="form-element form-button">#springMessageText("idp.webauthn.authn.authenticate", "Authenticate")</button>
</div>
<hr/>
- <button type="button" class="collapsible">Debugging</button>
+ <button type="button" class="collapsible">#springMessageText("idp.webauthn.debug.title", "Debugging")</button>
<div class="debug" id="debug-div">
- <label for="publicKeyCredentialCreation">Request Options</label>
+ <label for="publicKeyCredentialCreation">#springMessageText("idp.webauthn.debug.request", "Request Options")</label>
<textarea id="publicKeyCredentialRequestOptions" name="publicKeyCredentialRequestOptions" rows="20" cols="50">
$webauthnContext.publicKeyCredentialRequestOptions</textarea>
</div>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index c4f6992..093890e 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -82,11 +82,11 @@
#if ($webauthnRegContext.existingCredentials)
<table>
<tr>
- <th>Key Name</th>
- <th>Transports</th>
- <th>Is Passkey</th>
- <th>Registration Time</th>
- <th>Action</th>
+ <th>#springMessageText("idp.webauthn.register.table.keyName", "Key Name")</th>
+ <th>#springMessageText("idp.webauthn.register.table.transports", "Transports")</th>
+ <th>#springMessageText("idp.webauthn.register.table.passkey", "Passkey?")</th>
+ <th>#springMessageText("idp.webauthn.register.table.registrationTime", "Registration Time")</th>
+ <th>#springMessageText("idp.webauthn.register.table.action", "Action")</th>
</tr>
#foreach($cred in $webauthnRegContext.existingCredentials)
<tr>
@@ -98,35 +98,35 @@
<form id="deleteKeyForm" action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
<input type="hidden" name="credentialId" value="$cred.credentialIdBase64Url"/>
- <button class="webauthn-table-button" id="removeButton" type="submit" name="_eventId_deleteKey">Remove</button>
+ <button class="webauthn-table-button" id="removeButton" type="submit" name="_eventId_deleteKey">#springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
</form>
</td>
</tr>
#end
</table>
#else
- <div><span>You have no registered keys</span></div>
+ <div><span>#springMessageText("idp.webauthn.register.registered.noKeys", "You have no registered keys")</span></div>
#end
<br/>
<div>
- <button class="form-element form-button" id="registerButton">Add Key</button>
+ <button class="form-element form-button" id="registerButton">#springMessageText("idp.webauthn.register.addKey", "Add Key")</button>
</div>
<form id="authenticatorAttestationForm" action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
<input type="hidden" id="credentialNickname" name="credentialNickname"/>
<input type="hidden" id="authenticatorAttestation" name="authenticatorAttestation"/>
- <button class="hidden" id="registrationSubmit" type="submit" name="_eventId_addKey">Submit Registration</button>
+ <button class="hidden" id="registrationSubmit" type="submit" name="_eventId_addKey">#springMessageText("idp.webauthn.register.submit", "Submit Registration")</button>
</form>
</div>
#if($debug == "true")
<hr/>
- <button type="button" class="collapsible">Debugging</button>
+ <button type="button" class="collapsible">#springMessageText("idp.webauthn.register.debug.title", "Debugging")</button>
<div class="debug" id="debug-div">
- <label for="publicKeyCredentialCreation">Registration Options</label>
+ <label for="publicKeyCredentialCreation">#springMessageText("idp.webauthn.register.debug.registration", "Registration Options")</label>
<textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
$webauthnRegContext.publicKeyCredentialCreationOptions</textarea>
</div>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
index cb7e06d..a43f1d3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registration-outcomes.vm
@@ -55,11 +55,11 @@
<p>Registered Keys</p>
<table>
<tr>
- <th>Key Name</th>
- <th>Transports</th>
- <th>Is Passkey</th>
- <th>User Verified On Registration</th>
- <th>Registration Time</th>
+ <th>#springMessageText("idp.webauthn.register.table.keyName", "Key Name")</th>
+ <th>#springMessageText("idp.webauthn.register.table.transports", "Transports")</th>
+ <th>#springMessageText("idp.webauthn.register.table.passkey", "Passkey?")</th>
+ <th>#springMessageText("idp.webauthn.register.table.userVerifiedOnRegistration", "User Verified On Registration")</th>
+ <th>#springMessageText("idp.webauthn.register.table.registrationTime", "Registration Time")</th>
</tr>
#foreach($cred in $webauthnRegContext.existingCredentials)
<tr>
@@ -75,9 +75,9 @@
<div><span>You have no registered keys</span></div>
#end
<br />
- <form id="doneButtonForm" action="$flowExecutionUrl" method="post">
+ <form id="finish_button_form" action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
- <button id="doneButton" type="submit" name="_eventId_proceed">Done</button>
+ <button id="finish_button" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.register.finish", "Done")</button>
</form>
</div>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
index a7099dc..1a2784d 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
@@ -53,7 +53,7 @@
</div>
</form>
- </section>
+ </section>
</main>
<footer class="footer">
<div class="cc">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm
new file mode 100644
index 0000000..c3965ff
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-username-entry.vm
@@ -0,0 +1,124 @@
+##
+## Velocity Template for collection of username for Duo Passwordless use
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## passwordlessContext - context with Duo username and enrollment status
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+## cspNonce - Calculates secure nonces (call generateIdentifier)
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.profile.context.RelyingPartyContext'))
+#set ($eventCtx = $profileRequestContext.getSubcontext('org.opensaml.profile.context.EventContext'))
+#if ($eventCtx)
+#set ($eventId = $eventCtx.getEvent())
+#end
+#set ($nonce = $cspNonce.generateIdentifier())
+$response.addHeader("Content-Security-Policy", "script-src-elem 'nonce-$nonce'")
+#set ($onClick = "document.forms.password.j_username.value = document.forms.passwordless.j_username.value")
+$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onClick)'")
+##
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+ </head>
+ <body onLoad="$onLoad">
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+
+ <section>
+ #*
+ //
+ // SP Description & Logo (optional)
+ // These idpui lines will display added information (if available
+ // in the metadata) about the Service Provider (SP) that requested
+ // authentication. These idpui lines are "active" in this example
+ // (not commented out) - this extra SP info will be displayed.
+ // Remove or comment out these lines to stop the display of the
+ // added SP information.
+ //
+ *#
+ #set ($logo = $rpUIContext.getLogo())
+ #if ($logo)
+ <img class="service-logo" src= "$encoder.encodeForHTMLAttribute($logo)" alt="$encoder.encodeForHTMLAttribute($serviceName)">
+ #end
+ #set ($desc = $rpUIContext.getServiceDescription())
+ #if ($desc)
+ <p>$encoder.encodeForHTML($desc)</p>
+ #end
+
+ <blockquote>#springMessageText("idp.duo.passwordless.explain", "If you've enrolled a passkey or device/token for passwordless login,
+ please enter your username below and press the corresponding button. To bypass this option, just press the alternate button
+ to perform a traditional login.")</blockquote>
+
+
+
+ #if ($eventId == "RequestUnsupported")
+ <p class="output-message output--error">$encoder.encodeForHTML("#springMessageText('idp.duo.passwordless.unsupported', 'You have not enrolled a qualifying device for Passwordless use.')")</p>
+ #end
+
+ <form name="username-entry" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+
+ <label for="username">#springMessageText("idp.login.username", "Username")</label>
+ <input id="j_username" name="j_username" type="text"
+ value="#if($username)$encoder.encodeForHTML($username)#end" />
+
+ <input type="checkbox" name="donotcache" value="1" id="donotcache" />
+ <label for="donotcache">#springMessageText("idp.login.donotcache", "Don't Remember Login")</label>
+
+ <input id="_shib_idp_revokeConsent" type="checkbox" name="_shib_idp_revokeConsent" value="true" />
+ <label for="_shib_idp_revokeConsent">#springMessageText("idp.attribute-release.revoke", "Clear prior granting of permission for release of your information to this service.")</label>
+
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="_eventId_proceed"
+ >#springMessageText("idp.webauthn.passwordless.proceed", "Login with Passkey or Device")</button>
+ </div>
+ </div>
+ </form>
+
+ <ul>
+ <li><a href="#springMessageText('idp.duo.enrollment.url', 'idp/profile/admin/webauthn-registration')">#springMessageText("idp.duo.enrollment", "Enroll New Devices")</a></li>
+ <li><a href="#springMessageText('idp.url.helpdesk', '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+ </ul>
+ </section>
+ </main>
+ <footer class="footer">
+ <div class="cc">
+ <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+
+ <script #if ($nonce)nonce="$nonce"#end>
+ <!--
+ const input = document.getElementById('j_username');
+ const end = input.value.length;
+ input.setSelectionRange(end, end);
+ input.focus();
+ // -->
+ </script>
+
+ </body>
+</html>
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list