[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-51 - Allowing editing of nicknames
Phil Smart
philip.smart at jisc.ac.uk
Wed Apr 23 09:33:44 UTC 2025
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=d458f2b168ea363a90b2bb7e730cbf9df1d03fd0
The following commit(s) were added to refs/heads/main by this push:
new d458f2b JWEBAUTHN-51 - Allowing editing of nicknames
d458f2b is described below
commit d458f2b168ea363a90b2bb7e730cbf9df1d03fd0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Apr 23 10:29:34 2025 +0100
JWEBAUTHN-51 - Allowing editing of nicknames
- Allow nickname updates
- Separate nickname extraction from public key attestation extraction
https://shibboleth.atlassian.net/browse/JWEBAUTHN-51
---
.../context/WebAuthnRegistrationContext.java | 31 ++++++
.../storage/WebAuthnCredentialRepository.java | 19 +++-
.../impl/ExtractKeyInformationFromFormRequest.java | 2 +-
...st.java => ExtractNicknameFromFormRequest.java} | 83 +++++---------
...licKeyCredentialAttestationFromFormRequest.java | 33 +-----
...istrationContextCredentialToModifyConsumer.java | 77 +++++++++++++
.../admin/impl/UpdateCredentialNickname.java | 109 +++++++++++++++++++
.../IdPStorageServiceCredentialRespository.java | 54 +++++++++-
.../webauthn-registration-beans.xml | 23 ++++
.../webauthn-registration-flow.xml | 17 +++
.../idp/plugin/authn/webauthn/css/webauthn.css | 31 ++++++
.../idp/plugin/authn/webauthn/messages.properties | 3 +
.../authn/webauthn/views/webauthn-management.vm | 4 +-
.../authn/webauthn/views/webauthn-register.vm | 65 ++++++-----
.../ExtractKeyInformationFromFormRequestTest.java | 120 +++++++++++++++++++++
.../impl/ExtractNicknameFromFormRequestTest.java | 94 ++++++++++++++++
...eyCredentialAttestationFromFormRequestTest.java | 56 +---------
.../authn/webauthn/flow/TestRegistrationFlow.java | 5 +-
18 files changed, 651 insertions(+), 175 deletions(-)
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 0781695..328cb81 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
@@ -82,6 +82,9 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
/** Is nickname collection enabled or disabled?.*/
private boolean nicknameRequired;
+ /** The ID of a credential that is going to be modified in some way.*/
+ @Nullable private byte[] credentialIdToModify;
+
/**
* Is nickname collection required or not. The nickname is used for display purposes only.
@@ -251,7 +254,10 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
* @param id the credential identifier
*
* @return this context
+ *
+ * @deprecated Use {@link #setCredentialIdToModify(byte[])} instead
*/
+ @Deprecated(since= "1.2.0", forRemoval = true)
@Nonnull public WebAuthnRegistrationContext setCredentialIdToRemove(@Nullable final byte[] id) {
credentialIdToRemove = id;
return this;
@@ -332,6 +338,31 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
@Nullable public String getDisplayName() {
return displayName;
}
+
+ /**
+ * Set the ID of the credential that is going to be modified.
+ *
+ * @param id the credential identifier
+ *
+ * @return this context
+ *
+ * @since 1.2.0
+ */
+ @Nonnull public WebAuthnRegistrationContext setCredentialIdToModify(@Nullable final byte[] id) {
+ credentialIdToModify = id;
+ return this;
+ }
+
+ /**
+ * Get the ID of the credential that is going to be modified.
+ *
+ * @return the identifier of the credential to be modified.
+ *
+ * @since 1.2.0
+ */
+ @Nullable public byte[] getCredentialIdToModify() {
+ return credentialIdToModify;
+ }
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
index 9ca5141..db55c70 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/WebAuthnCredentialRepository.java
@@ -151,6 +151,23 @@ public interface WebAuthnCredentialRepository extends CredentialRepository {
default boolean updateLastUsedTime(final @Nonnull String username, final @Nonnull ByteArray credentialId,
@Nonnull final Instant lastUsedTime) {
return false;
- }
+ }
+
+ /**
+ * Update the nickname for the credential that belongs to the given user.
+ *
+ * @param username the username of the user to update the nickname for
+ * @param credentialId the identifier of the credential to update the nickname for
+ * @param nickname the new nickname of the credential
+ *
+ * @return true iff the registration was updated, false otherwise.
+ *
+ * @since 1.2.0
+ */
+ //TODO remove default in v2.
+ default boolean updateNickname(final @Nonnull String username, final @Nonnull ByteArray credentialId,
+ @Nonnull final String nickname) {
+ return false;
+ }
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
index 3eb6fa6..27445b6 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequest.java
@@ -115,7 +115,7 @@ public class ExtractKeyInformationFromFormRequest extends AbstractProfileAction
// Should be base64 encoded credential.
final byte[] credentialIdAsBytes = Base64Support.decode(credentialId);
contextSettingConsumer.accept(profileRequestContext, credentialIdAsBytes);
- log.trace("{} Credential to remove '{}'",getLogPrefix(),credentialId);
+ log.trace("{} Credential from request '{}'",getLogPrefix(),credentialId);
} catch (final DecodingException e) {
log.debug("{} Unable to base64 decode credentialID, can not set credential", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
similarity index 52%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
index 0903471..7e5af84 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequest.java
@@ -13,8 +13,6 @@
*/
package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
-import java.io.IOException;
-
import javax.annotation.Nonnull;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
@@ -22,12 +20,7 @@ import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
-import com.yubico.webauthn.data.PublicKeyCredential;
-
import jakarta.servlet.http.HttpServletRequest;
-import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
@@ -38,63 +31,61 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action that extracts the PublicKeyCredential containing the authenticator attestation response from the incoming
- * HTTP request. Also extracts the user entered credential nickname. Failure to find the attestation results in an
- * a non-proceed event. Similarly, if a nickname is required and not provided a non-proceed event is signalled.
+ * An action that extracts the key nickname from the incoming HTTP request. If a nickname is required and not provided
+ * a non-proceed event is signalled.
*
* @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
* @event {AuthnEventIds#NO_CREDENTIALS}
* @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post add an AuthenticatorAttestationResponse and credential nickname (if present) to the registration context
+ * @post add a credential nickname (if present) to the registration context
*/
-public class ExtractPublicKeyCredentialAttestationFromFormRequest
+public class ExtractNicknameFromFormRequest
extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
-
- /** Default public key credential attestation parameter name. */
- @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "publicKeyCredential";
/** Default nickname parameter name. */
@Nonnull @NotEmpty public static final String DEFAULT_NICKNAME_FIELD_NAME = "credentialNickname";
/** Class logger. */
@Nonnull
- private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialAttestationFromFormRequest.class);
-
- /** Name of the public key credential attestation parameter. */
- @Nonnull @NotEmpty private String publicKeyCredentialAttestationParameterName;
+ private final Logger log = LoggerFactory.getLogger(ExtractNicknameFromFormRequest.class);
/** Name of the nickname parameter. */
@Nonnull @NotEmpty private String credentialNicknameParameterName;
+ /**
+ * The EventID of the event to build if no credential nickname can be found in the request and a nickname is
+ * required.
+ */
+ @Nonnull @NotEmpty private String noCredentialNicknameEventId;
+
/** Constructor. */
- public ExtractPublicKeyCredentialAttestationFromFormRequest() {
+ public ExtractNicknameFromFormRequest() {
super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
- publicKeyCredentialAttestationParameterName = DEFAULT_PARAMETER_NAME;
credentialNicknameParameterName = DEFAULT_NICKNAME_FIELD_NAME;
+ noCredentialNicknameEventId = WebAuthnRegistrationEventIds.INVALID_REGISTRATION;
}
/**
- * Set the name of the parameter to extract the public key credential attestation response from.
+ * Set the name of the parameter to extract the credential nickname from.
*
- * @param field the field name
+ * @param field field name
*/
- public void setPublickKeyCredentialAttestationParameterName(@Nonnull @NotEmpty final String field) {
+ public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
checkSetterPreconditions();
- publicKeyCredentialAttestationParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
- "Attestation parameter name cannot be null or empty");
+ credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
+ "Nickname parameter can not be null or empty");
}
/**
- * Set the name of the parameter to extract the credential nickname from.
+ * Set the EventID of the event to build if no credential nickname can be found in the request and a nickname is
+ * required.
*
- * @param field field name
+ * @param eventId the eventId to build.
*/
- public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
+ public void setNoCredentialNicknameEventId(@Nonnull @NotEmpty final String eventId) {
checkSetterPreconditions();
-
- credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
- "Nickname parameter can not be null or empty");
+ noCredentialNicknameEventId = Constraint.isNotEmpty(eventId, "NoCredentialsEventId can not be null or empty");
}
@Override
@@ -104,38 +95,20 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
final HttpServletRequest request = getHttpServletRequest();
if (request == null) {
log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
- }
-
- final String pkCredAttestationJson = request.getParameter(publicKeyCredentialAttestationParameterName);
- if (StringSupport.trimOrNull(pkCredAttestationJson) == null) {
- log.debug("{} No PublicKeyCredential with authenticator attestation response in request", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
return;
}
- log.trace("{} PublicKeyCredential authenticator attestation response:'{}'",getLogPrefix(),
- pkCredAttestationJson);
final String credNickname = request.getParameter(credentialNicknameParameterName);
log.trace("{} Credential nickname is '{}'",getLogPrefix(), credNickname);
if (StringSupport.trimOrNull(credNickname) == null && context.isNicknameRequired()) {
log.debug("{} Credential nickname is not in the request but is required", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
- return;
- }
-
- try {
- final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
- pkCredAttestation = PublicKeyCredential.parseRegistrationResponseJson(pkCredAttestationJson);
- context.setPublicKeyCredentialAttestationResponse(pkCredAttestation);
- context.setCredentialNickname(credNickname);
- } catch (final IOException e) {
- log.debug("{} Could not parse PublicKeyCredential response from request parameter '{}'", getLogPrefix(),
- publicKeyCredentialAttestationParameterName, e);
- ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+ ActionSupport.buildEvent(profileRequestContext, noCredentialNicknameEventId);
return;
}
+
+ context.setCredentialNickname(credNickname);
+
}
}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
index 0903471..0402617 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequest.java
@@ -39,13 +39,12 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
* An action that extracts the PublicKeyCredential containing the authenticator attestation response from the incoming
- * HTTP request. Also extracts the user entered credential nickname. Failure to find the attestation results in an
- * a non-proceed event. Similarly, if a nickname is required and not provided a non-proceed event is signalled.
+ * HTTP request. Failure to find the attestation results in an a non-proceed event.
*
* @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION}
* @event {AuthnEventIds#NO_CREDENTIALS}
* @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
- * @post add an AuthenticatorAttestationResponse and credential nickname (if present) to the registration context
+ * @post add an AuthenticatorAttestationResponse to the registration context
*/
public class ExtractPublicKeyCredentialAttestationFromFormRequest
extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
@@ -53,9 +52,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
/** Default public key credential attestation parameter name. */
@Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "publicKeyCredential";
- /** Default nickname parameter name. */
- @Nonnull @NotEmpty public static final String DEFAULT_NICKNAME_FIELD_NAME = "credentialNickname";
-
/** Class logger. */
@Nonnull
private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialAttestationFromFormRequest.class);
@@ -63,14 +59,10 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
/** Name of the public key credential attestation parameter. */
@Nonnull @NotEmpty private String publicKeyCredentialAttestationParameterName;
- /** Name of the nickname parameter. */
- @Nonnull @NotEmpty private String credentialNicknameParameterName;
-
/** Constructor. */
public ExtractPublicKeyCredentialAttestationFromFormRequest() {
super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
publicKeyCredentialAttestationParameterName = DEFAULT_PARAMETER_NAME;
- credentialNicknameParameterName = DEFAULT_NICKNAME_FIELD_NAME;
}
/**
@@ -85,18 +77,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
"Attestation parameter name cannot be null or empty");
}
- /**
- * Set the name of the parameter to extract the credential nickname from.
- *
- * @param field field name
- */
- public void setCredentialNicknameParameterName(@Nonnull @NotEmpty final String field) {
- checkSetterPreconditions();
-
- credentialNicknameParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field),
- "Nickname parameter can not be null or empty");
- }
-
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final WebAuthnRegistrationContext context) {
@@ -116,20 +96,11 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequest
}
log.trace("{} PublicKeyCredential authenticator attestation response:'{}'",getLogPrefix(),
pkCredAttestationJson);
-
- final String credNickname = request.getParameter(credentialNicknameParameterName);
- log.trace("{} Credential nickname is '{}'",getLogPrefix(), credNickname);
- if (StringSupport.trimOrNull(credNickname) == null && context.isNicknameRequired()) {
- log.debug("{} Credential nickname is not in the request but is required", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
- return;
- }
try {
final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
pkCredAttestation = PublicKeyCredential.parseRegistrationResponseJson(pkCredAttestationJson);
context.setPublicKeyCredentialAttestationResponse(pkCredAttestation);
- context.setCredentialNickname(credNickname);
} catch (final IOException e) {
log.debug("{} Could not parse PublicKeyCredential response from request parameter '{}'", getLogPrefix(),
publicKeyCredentialAttestationParameterName, e);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.java
new file mode 100644
index 0000000..7a26530
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/RegistrationContextCredentialToModifyConsumer.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.admin.impl;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link BiConsumer} that sets the credential ID to modify onto the WebAuthn registration context.
+ */
+public class RegistrationContextCredentialToModifyConsumer extends AbstractIdentifiableInitializableComponent
+ implements BiConsumer<ProfileRequestContext, byte[]> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(RegistrationContextCredentialToModifyConsumer.class);
+
+ /** Lookup strategy to locate the WebAuthn registration context. */
+ @Nonnull
+ private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
+
+ /** Constructor.*/
+ protected RegistrationContextCredentialToModifyConsumer() {
+ //prc -> WebAuthnContext
+ webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+ }
+
+ /**
+ * Set WebAuthn registration context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setWebauthnRegistrationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
+ checkSetterPreconditions();
+
+ webauthnRegistrationContextLookupStrategy =
+ Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final ProfileRequestContext prc, final byte[] credentialId) {
+ final WebAuthnRegistrationContext webauthnRegistrationContext =
+ webauthnRegistrationContextLookupStrategy.apply(prc);
+ if (webauthnRegistrationContext == null) {
+ log.warn("{} No WebAuthn registration context returned by lookup strategy, can not set credential "
+ + "identifier to modify",getId());
+ return;
+ }
+ webauthnRegistrationContext.setCredentialIdToModify(credentialId);
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java
new file mode 100644
index 0000000..86f281b
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/UpdateCredentialNickname.java
@@ -0,0 +1,109 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.ByteArray;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.audit.impl.AbstractWebAuthnAuditingAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * An action that updates the nickname of a credential in the credential repository.
+ *
+ * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post a credential's nickname is updated in the credential repository
+ */
+public class UpdateCredentialNickname extends AbstractWebAuthnAuditingAction<WebAuthnRegistrationContext> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(UpdateCredentialNickname.class);
+
+ /** The credential repository to use.*/
+ @NonnullAfterInit private WebAuthnCredentialRepository repository;
+
+ /** Constructor.*/
+ protected UpdateCredentialNickname() {
+ super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ repository = getCredentialRepository();
+ if (repository == null) {
+ throw new ComponentInitializationException("Credential repository can not be null");
+ }
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnRegistrationContext context) {
+
+ // This should come from the context before the registration page i.e. it should not come from a form the user
+ // can manipulate (it should be the authenticated user).
+ final String username = context.getUsername();
+ if (username == null) {
+ log.error("{} Unable to find username in registration context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+ return;
+ }
+
+ // This comes from the credential the user input
+ final byte[] credentialId = context.getCredentialIdToModify();
+ if (credentialId == null) {
+ log.error("{} Unable to find credentialId in registration context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+ return;
+ }
+ final ByteArray credentialIdAsByteArray = new ByteArray(credentialId);
+
+ // This comes from the nickname the user entered directly.
+ final String nickname = context.getCredentialNickname();
+ if (StringSupport.trimOrNull(nickname) == null) {
+ log.error("{} Unable to find nickname in registration context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+ return;
+ }
+
+ final boolean updated = repository.updateNickname(username, credentialIdAsByteArray, nickname);
+ log.info("{} Credential '{}' {} updated for user '{}'", getLogPrefix(),
+ credentialIdAsByteArray.getBase64(), updated ? "was" : "was not", username);
+ if(updated) {
+ auditSuccess(profileRequestContext, "credential-updated");
+ } else {
+ auditFailure(profileRequestContext, "credential-updated");
+ }
+
+ // Remove the key ID to be updated in case the context state is re-used
+ context.setCredentialIdToModify(null);
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
index 6eeca45..387ccf5 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespository.java
@@ -624,7 +624,7 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
.findFirst();
if (credential.isEmpty()) {
- log.warn("Can not update signature count for user '{}' and credential '{}'. "
+ log.warn("Can not update last used time for user '{}' and credential '{}'. "
+ "No existing credential found.", username, credentialId.getBase64());
return false;
}
@@ -641,7 +641,57 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
if (updateCredentialSet.isEmpty()) {
// We are updating, so this should not be possible
- log.debug("Can not update signature count for user '{}' and credential '{}'. "
+ log.debug("Can not last used time for user '{}' and credential '{}'. "
+ + "Update set is empty.", username, credentialId.getBase64());
+ return false;
+ }
+
+ try {
+ final Long updatedVersion = storageService.updateWithVersion(
+ existingRegistrations.getVersion(), STORAGE_CONTEXT, username, updateCredentialSet,
+ serializer, null);
+ return updatedVersion != null;
+ } catch (final IOException | VersionMismatchException e) {
+ throw new CredentialRepositoryException(e);
+ }
+
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean updateNickname(@Nonnull final String username, @Nonnull final ByteArray credentialId,
+ final String nickname) {
+ final Lock writeLock = lock.writeLock();
+ try {
+ writeLock.lock();
+
+ final VersionedCredentialSet existingRegistrations = getRegistrationsByUsernameWithVersion(username);
+ final Optional<CredentialRecord> credential = existingRegistrations.getCredentials().stream()
+ .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId()))
+ .findFirst();
+
+ if (credential.isEmpty()) {
+ log.warn("Can not update nickname for user '{}' and credential '{}'. "
+ + "No existing credential found.", username, credentialId.getBase64());
+ return false;
+ }
+
+ // Copy the credential into a new credential and update the last used time
+ final CredentialRecord updatedRegistration = credential.get().toBuilder()
+ .withCredentialNickname(nickname)
+ .build();
+
+ final Set<CredentialRecord> updateCredentialSet =
+ new LinkedHashSet<>(existingRegistrations.getCredentials());
+ updateCredentialSet.remove(credential.get());
+ updateCredentialSet.add(updatedRegistration);
+
+ if (updateCredentialSet.isEmpty()) {
+ // We are updating, so this should not be possible
+ log.debug("Can not nickname for user '{}' and credential '{}'. "
+ "Update set is empty.", username, credentialId.getBase64());
return false;
}
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 ad9076b..3a39e08 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
@@ -178,6 +178,15 @@
<bean id="ExtractPublicKeyCredentialAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractPublicKeyCredentialAttestationFromFormRequest"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+ <bean id="ExtractNicknameFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+
+ <bean id="ExtractNicknameToUpdateFromFormRequest" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:noCredentialNicknameEventId="EmptyNickname"/>
<bean id="ExtractKeyRemovalInformationFromFormRequest" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest"
@@ -186,6 +195,20 @@
<bean class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RegistrationContextCredentialRemovalConsumer"/>
</property>
</bean>
+
+ <bean id="ExtractKeyUpdateInformationFromFormRequest" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+ <property name="contextSettingConsumer">
+ <bean class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.RegistrationContextCredentialToModifyConsumer"/>
+ </property>
+ </bean>
+
+ <bean id="UpdateCredentialNickname" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.UpdateCredentialNickname"
+ p:populateAuditContextAction="#{%{idp.authn.webauthn.registration.audit.enabled:false} ? getObject('RegistrationOperationPopulateAuditContext') : null}"
+ p:writeAuditLogAction="#{%{idp.authn.webauthn.registration.audit.enabled:false} ? getObject('WriteAdminAuditLog') : null}"
+ p:auditContextCreationStrategy-ref="AdminAuditContextLookup"/>
<bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.DeletePublicKeyCredential"
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 a195509..b43227e 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
@@ -126,6 +126,7 @@
<transition on="finish" to="RegistrationComplete" />
<transition on="resume" to="RegistrationCompleteResumeFlow" />
<transition on="addKey" to="AddKey" />
+ <transition on="updateNickname" to="UpdateNickname" />
<transition on="deleteKey" to="DeleteKey" />
<on-exit>
<evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationInformationContext)).reset()"/>
@@ -133,8 +134,24 @@
</on-exit>
</view-state>
+ <action-state id="UpdateNickname">
+ <evaluate expression="ExtractNicknameToUpdateFromFormRequest"/>
+ <evaluate expression="ExtractKeyUpdateInformationFromFormRequest"/>
+ <evaluate expression="UpdateCredentialNickname"/>
+ <evaluate expression="'proceed'" />
+
+ <transition on="EmptyNickname" to="DoClientStorageSaveContext">
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationErrorContext)).addClassifiedError('EmptyNickname')"/>
+ </transition>
+
+ <transition on="proceed" to="DoClientStorageSaveContext">
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext)).ensureSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationInformationContext)).addClassifiedMessage('NicknameUpdated')"/>
+ </transition>
+ </action-state>
+
<action-state id="AddKey">
<evaluate expression="ExtractPublicKeyCredentialAttestationFromFormRequest"/>
+ <evaluate expression="ExtractNicknameFromFormRequest"/>
<evaluate expression="CheckRegistrationPolicy"/>
<evaluate expression="ValidateAuthenticatorAttestationResponse"/>
<evaluate expression="StorePublicKeyCredential"/>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
index 8034b7b..e7b6ec3 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
@@ -101,4 +101,35 @@ tr:hover {
.info {
background-color: #2196F3;
+}
+
+.update-btn {
+ display: inline-flex;
+ align-items: center;
+ font-size: 10px;
+ padding: 2px 6px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ gap: 4px;
+}
+
+.nickname-input {
+ padding: 4px 6px;
+ font-size: 14px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ width: 100px;
+}
+
+.update-nickname-form {
+ display: flex;
+ gap: 1px;
+}
+
+.update-icon {
+ width: 15px;
+ height: 25px;
}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
index d70e5a0..16ad014 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/messages.properties
@@ -38,6 +38,7 @@ idp.webauthn.register.submit = Submit registration
idp.webauthn.register.username.explain = Please enter your username below.
idp.webauthn.register.username.proceed = Next
idp.webauthn.register.unsupported = Your browser is not WebAuthn compatible
+idp.webauthn.register.credential.nickname.update.button.text = Update Nickname
idp.webauthn.admin.search.title = Search for user
idp.webauthn.admin.header = Registered keys for
@@ -65,6 +66,8 @@ idp.webauthn.debug.register.request = Registration options
InvalidRegistration = Key registration unsuccessful
ValidRegistration = Key was registered successfully
KeyRemoved = Key was removed successfully
+NicknameUpdated = Nickname updated
+EmptyNickname = Nickname can not be empty
# Messages to report back to the admin during key management
SearchUsernameNotFoundAfterC14N = Error determining username to search for
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
index 6d20db7..4dfb1a2 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
@@ -65,9 +65,9 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
<th>#springMessageText("idp.webauthn.admin.table.header.authenticatorIcon", "Icon")</th>
<th>#springMessageText("idp.webauthn.admin.table.header.labels", "Labels")</th>
#if ($lastUsed == 'true')
- <th>#springMessageText("idp.webauthn.admin.table.header.lastUsedTime", "Last Used")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.lastUsedTime", "Last Used")</th>
#else
- <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
#end
<th>#springMessageText("idp.webauthn.admin.table.header.hasMetadata", "Metadata?")</th>
<th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
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 0e1d062..a483604 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
@@ -141,33 +141,44 @@ $response.addHeader("Content-Security-Policy", "default-src 'none'; style-src 's
<th>#springMessageText("idp.webauthn.register.table.header.action", "Action")</th>
</tr>
#foreach($cred in $webauthnRegContext.existingCredentials)
- <tr>
- <td>$encoder.encodeForHTML($cred.credentialRecord.nickname)</td>
- #if ($cred.authenticatorDescription)
- <td>$encoder.encodeForHTML($cred.authenticatorDescription)</td>
- #else
- <td>#springMessageText("idp.webauthn.register.table.unknownCredential", "unknown")</td>
- #end
- #if ($cred.icon)
- <td> <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon)" alt="$encoder.encodeForHTML($cred.credentialRecord.nickname)-authenticator-icon"/></td>
- #else
- <td></td>
- #end
- <td>
- #foreach($label in $cred.labels)
- <span class="label info">$encoder.encodeForHTML($label)</span>
- #end
- </td>
- <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
- <td>
- <form id="delete_key_form" action="$flowExecutionUrl" method="post">
- #parse("csrf/csrf.vm")
- <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
- <button class="webauthn-table-button" onclick="$areYouSure" id="removeButton" type="submit" name="_eventId_deleteKey">
- #springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
- </form>
- </td>
- </tr>
+ <tr>
+ <td>
+ <form id="update_nickname_form" action="$flowExecutionUrl" method="post" class="update-nickname-form">
+ #parse("csrf/csrf.vm")
+ <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
+ <input name="credentialNickname" class="nickname-input" value="$encoder.encodeForHTML($cred.credentialRecord.nickname)"/>
+ <button title="#springMessageText("idp.webauthn.register.credential.nickname.update.button.text", "Update Nickname")" class="update-btn" onclick="$areYouSure" id="updateNicknameButton" type="submit" name="_eventId_updateNickname">
+ <svg class="update-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M17.65 6.40A7.95 8.95 0 0 0 12 4.6V1L7 7l5 5V7a6 5 0 1 1-6 6h-2a8 8 0 1 0 13.65-6.65z"/>
+ </svg>
+ </button>
+ </form>
+ </td>
+ #if ($cred.authenticatorDescription)
+ <td>$encoder.encodeForHTML($cred.authenticatorDescription)</td>
+ #else
+ <td>#springMessageText("idp.webauthn.register.table.unknownCredential", "unknown")</td>
+ #end
+ #if ($cred.icon)
+ <td> <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon)" alt="$encoder.encodeForHTML($cred.credentialRecord.nickname)-authenticator-icon"/></td>
+ #else
+ <td></td>
+ #end
+ <td>
+ #foreach($label in $cred.labels)
+ <span class="label info">$encoder.encodeForHTML($label)</span>
+ #end
+ </td>
+ <td>$encoder.encodeForHTML($webAuthnEncoder.chooseLatestAndTransform($cred.credentialRecord.lastUsedTime, $cred.credentialRecord.registrationTime))</td>
+ <td>
+ <form id="delete_key_form" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <input type="hidden" name="credentialId" value="$cred.credentialRecord.credentialIdBase64Url"/>
+ <button class="webauthn-table-button" onclick="$areYouSure" id="removeButton" type="submit" name="_eventId_deleteKey">
+ #springMessageText("idp.webauthn.register.credential.remove", "Remove")</button>
+ </form>
+ </td>
+ </tr>
#end
</table>
#else
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java
new file mode 100644
index 0000000..b54896e
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractKeyInformationFromFormRequestTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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 static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.testing.ConstantSupplier;
+
+/**
+ * Tests for {@link ExtractKeyInformationFromFormRequest}
+ */
+public class ExtractKeyInformationFromFormRequestTest extends AbstractWebAuthnTest {
+
+ private ExtractKeyInformationFromFormRequest action;
+
+ private WebAuthnRegistrationContext context;
+
+ private MockHttpServletRequest request;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+
+ request = new MockHttpServletRequest();
+
+ action = new ExtractKeyInformationFromFormRequest();
+ }
+
+ @Test
+ public void testExtraction() throws Exception {
+ final SimpleContext contextToUpdate = new SimpleContext();
+ action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+
+ final byte[] credentialIdBytes = generateRandomBytes(16);
+ final String credentialIdb64 = Base64Support.encodeURLSafe(credentialIdBytes);
+
+ request.addParameter(ExtractKeyInformationFromFormRequest.DEFAULT_PARAMETER_NAME,
+ new String[]{credentialIdb64});
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertNull(result);
+ assertNotNull(contextToUpdate);
+ assert contextToUpdate != null;
+ assertNotNull(contextToUpdate.getCredentialIdToRemove());
+
+ }
+
+ @Test
+ public void testExtraction_NoCredentialIdFound() throws Exception {
+ final SimpleContext contextToUpdate = new SimpleContext();
+ action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertFailure(result, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
+
+ }
+
+ @Test
+ public void testExtraction_CredentialIdCouldNotBeBase64Decoded() throws Exception {
+ final SimpleContext contextToUpdate = new SimpleContext();
+ action.setContextSettingConsumer((prc, bytes) -> contextToUpdate.setCredentialIdToRemove(bytes));
+
+ request.addParameter(ExtractKeyInformationFromFormRequest.DEFAULT_PARAMETER_NAME,
+ new String[]{"not-encoded"});
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertFailure(result, WebAuthnRegistrationEventIds.INVALID_ADMIN_ACTION);
+
+ }
+
+ /** Simple context to store the update.*/
+ private class SimpleContext extends BaseContext {
+
+ private byte[] credentialIdToRemove;
+
+ public SimpleContext() {
+ }
+
+ public void setCredentialIdToRemove(final byte[] credId) {
+ this.credentialIdToRemove = credId;
+ }
+
+ public byte[] getCredentialIdToRemove() {
+ return credentialIdToRemove;
+ }
+ }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java
new file mode 100644
index 0000000..9e85daa
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractNicknameFromFormRequestTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.testing.ConstantSupplier;
+
+/**
+ * Tests for {@link ExtractNicknameFromFormRequest}
+ */
+public class ExtractNicknameFromFormRequestTest extends AbstractWebAuthnTest {
+
+ private ExtractNicknameFromFormRequest action;
+
+ private WebAuthnRegistrationContext context;
+
+ private MockHttpServletRequest request;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+ request = new MockHttpServletRequest();
+ action = new ExtractNicknameFromFormRequest();
+ action.setWebAuthnClient(client);
+ action.setCredentialRepository(credentialRepo);
+ context.setNicknameRequired(true);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testExtraction() throws Exception {
+
+ request.addParameter(ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME,
+ "nickname");
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertNull(result);
+ assertNotNull(context.getCredentialNickname());
+ }
+
+
+ @SuppressWarnings("null")
+ @Test
+ public void testExtraction_NoNicknameInResponse_IsRequired() throws Exception {
+
+ context.setNicknameRequired(true);
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertFailure(result, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testExtraction_NoNicknameInResponse_NotRequired() throws Exception {
+
+ context.setNicknameRequired(false);
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ action.initialize();
+
+ final Event result = action.execute(src);
+ assertNull(result);
+ assertNull(context.getCredentialNickname());
+ }
+
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
index 966133f..e306097 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractPublicKeyCredentialAttestationFromFormRequestTest.java
@@ -53,7 +53,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
action = new ExtractPublicKeyCredentialAttestationFromFormRequest();
action.setWebAuthnClient(client);
action.setCredentialRepository(credentialRepo);
- context.setNicknameRequired(true);
}
@SuppressWarnings("null")
@@ -67,16 +66,13 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
attestationResponseJson);
- request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME,
- "nickname");
+
action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
action.initialize();
final Event result = action.execute(src);
assertNull(result);
- assertNotNull(context.getCredentialNickname());
assertNotNull(context.getPublicKeyCredentialAttestationResponse());
- assertEquals(context.getCredentialNickname(),"nickname");
assertEquals(context.getPublicKeyCredentialAttestationResponse().getId(), attestationResponse.getId());
}
@@ -86,8 +82,6 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
"this-is-not-good");
- request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME,
- "nickname");
action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
action.initialize();
@@ -99,57 +93,11 @@ public class ExtractPublicKeyCredentialAttestationFromFormRequestTest extends Ab
@Test
public void testExtraction_NoCredentialInResponse() throws Exception {
- request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME,
- "nickname");
action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
action.initialize();
final Event result = action.execute(src);
assertFailure(result, AuthnEventIds.NO_CREDENTIALS);
- }
-
- @SuppressWarnings("null")
- @Test
- public void testExtraction_NoNicknameInResponse_IsRequired() throws Exception {
-
- final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
- attestationResponse = createAttestationReponse();
-
- final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
-
- request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
- attestationResponseJson);
-
- context.setNicknameRequired(true);
- action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
- action.initialize();
-
- final Event result = action.execute(src);
- assertFailure(result, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
- }
-
- @SuppressWarnings("null")
- @Test
- public void testExtraction_NoNicknameInResponse_NotRequired() throws Exception {
-
- final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
- attestationResponse = createAttestationReponse();
-
- final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
-
- request.addParameter(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
- attestationResponseJson);
-
- context.setNicknameRequired(false);
- action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
- action.initialize();
-
- final Event result = action.execute(src);
- assertNull(result);
- assertNull(context.getCredentialNickname());
- assertNotNull(context.getPublicKeyCredentialAttestationResponse());
- assertEquals(context.getPublicKeyCredentialAttestationResponse().getId(), attestationResponse.getId()) ;
- }
-
+ }
}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
index 96dd034..8479448 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/flow/TestRegistrationFlow.java
@@ -39,6 +39,7 @@ import com.yubico.webauthn.data.RegistrationExtensionInputs;
import com.yubico.webauthn.data.UserIdentity;
import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractKeyInformationFromFormRequest;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractNicknameFromFormRequest;
import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractPublicKeyCredentialAttestationFromFormRequest;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
@@ -185,7 +186,7 @@ public class TestRegistrationFlow extends AbstractWebAuthnFlowTest{
final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
attestationResponseJson,
- ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
+ ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
externalContext.setEventId("addKey");
result.getSecond().setCurrentState("DisplayWebAuthnRegistrationView");
result.getSecond().resume(externalContext);
@@ -275,7 +276,7 @@ public class TestRegistrationFlow extends AbstractWebAuthnFlowTest{
final String attestationResponseJson = jsonMapper.writeValueAsString(attestationResponse);
setHttpFormRequest("POST", Map.of(ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_PARAMETER_NAME,
attestationResponseJson,
- ExtractPublicKeyCredentialAttestationFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
+ ExtractNicknameFromFormRequest.DEFAULT_NICKNAME_FIELD_NAME, "new-cred"));
externalContext.setEventId("addKey");
result.getSecond().setCurrentState("DisplayWebAuthnRegistrationView");
result.getSecond().resume(externalContext);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list