[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage other users keys
Phil Smart
philip.smart at jisc.ac.uk
Fri Apr 26 15:58:37 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=fead4a6984ad8571a7399be6ca923bf2ef6fc41e
The following commit(s) were added to refs/heads/main by this push:
new fead4a6 JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage other users keys
fead4a6 is described below
commit fead4a6984ad8571a7399be6ca923bf2ef6fc41e
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Apr 26 16:58:34 2024 +0100
JWEBAUTHN-8 - Add an admin flow suitable for an sys admin to manage
other users keys
- A working WiP for the admin UI to allow an admin to remove other
users credential registrations.
https://shibboleth.atlassian.net/browse/JWEBAUTHN-8
---
.../admin/WebAuthnRegistrationEventIds.java | 3 +
.../context/WebAuthnManagementContext.java | 142 ++++++++++++++++++
.../navigate/UsernameLookupFromSubjectContext.java | 2 +-
.../StorageServiceCredentialRepository.java | 21 +++
.../impl/AbstractWebAuthnManagementAction.java | 138 ++++++++++++++++++
.../impl/AbstractWebAuthnRegistrationAction.java | 95 +-----------
...al.java => AdminDeletePublicKeyCredential.java} | 48 +++----
...xtractKeyRemovalInformationFromFormRequest.java | 105 ++++++++++++++
.../admin/impl/DeletePublicKeyCredential.java | 2 +
.../impl/ExtractUsernameSearchFromFormRequest.java | 96 +++++++++++++
.../admin/impl/LookupCredentialsForUser.java | 64 +++++++++
.../impl/PopulateWebAuthnManagementContext.java | 112 +++++++++++++++
.../admin/impl/StorePublicKeyCredential.java | 41 ------
.../impl/AbstractWebAuthnAuthenticationAction.java | 1 +
.../webauthn/impl/AbstractWebAuthnBaseAction.java | 68 +--------
.../authn/webauthn/impl/BaseWebAuthnAction.java | 116 +++++++++++++++
.../IdPStorageServiceCredentialRespository.java | 90 +++++++++++-
.../META-INF/net.shibboleth.idp/postconfig.xml | 24 +++-
.../webauthn-management-beans.xml | 58 ++++++++
.../webauthn-management-flow.xml} | 106 ++++++--------
.../webauthn-registration-beans.xml | 7 +-
.../webauthn-registration-flow.xml | 2 +-
.../authn/webauthn/conf/authn/webauthn.properties | 10 ++
.../idp/plugin/authn/webauthn/module.properties | 10 +-
.../webauthn/views/webauthn-authn-username.vm | 2 +-
.../plugin/authn/webauthn/views/webauthn-authn.vm | 2 +-
.../{webauthn-register-end.vm => webauthn-end.vm} | 32 ++++-
.../webauthn/views/webauthn-management-search.vm | 71 +++++++++
.../authn/webauthn/views/webauthn-management.vm | 125 ++++++++++++++++
.../authn/webauthn/views/webauthn-register.vm | 2 +-
...IdPStorageServiceCredentialRespositoryTest.java | 159 +++++++++++++++++++++
.../storage/impl/InMemoryRegistrationStorage.java | 15 ++
32 files changed, 1463 insertions(+), 306 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
index 0985ba7..e5dcb16 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnRegistrationEventIds.java
@@ -35,5 +35,8 @@ public final class WebAuthnRegistrationEventIds {
/** Registration context is invalid. */
@Nonnull @NotEmpty public static final String INVALID_REGISTRATION_CTX = "InvalidRegistrationContext";
+
+ /** Management context is invalid. */
+ @Nonnull @NotEmpty public static final String INVALID_MANAGEMENT_CTX = "InvalidManagmentContext";
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java
new file mode 100644
index 0000000..7835c80
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnManagementContext.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A WebAuthn admin management context for the management admin flow.
+ */
+public class WebAuthnManagementContext extends BaseContext {
+
+ /** Canonical principal name of subject. */
+ @Nullable private String principalName;
+
+ /** The username of the user to find and manage credentials for. */
+ @Nullable private String searchUsername;
+
+ /** The set of credentials registered for the given searchUsername.*/
+ @Nullable @Unmodifiable @NotLive private Collection<CredentialRegistration> foundCredentials;
+
+ /** The ID of the credential that is going to be removed.*/
+ @Nullable private byte[] credentialIdToRemove;
+
+ /**
+ * Get the canonical principal name of the subject.
+ *
+ * @return the canonical principal name
+ */
+ @Nullable public String getPrincipalName() {
+ return principalName;
+ }
+
+ /**
+ * Set the canonical principal name of the subject.
+ *
+ * @param name the canonical principal name
+ *
+ * @return this context
+ */
+ @Nonnull public WebAuthnManagementContext setPrincipalName(@Nullable final String name) {
+ principalName = name;
+
+ return this;
+ }
+
+ /**
+ * Get the username of the user to find and manage credentials for.
+ *
+ * @return the username
+ */
+ @Nullable public String getSearchUsername() {
+ return searchUsername;
+ }
+
+ /**
+ * Set the username of the user to find and manage credentials for.
+ *
+ * @param name the username
+ *
+ * @return this context
+ */
+ @Nonnull public WebAuthnManagementContext setSearchUsername(@Nullable final String name) {
+ searchUsername = name;
+
+ return this;
+ }
+
+ /**
+ * Set the credentials found to belong to the given search user.
+ *
+ * @param credentials the set of credentials
+ *
+ * @return this context
+ */
+ @Nonnull public WebAuthnManagementContext setFoundCredentials(
+ @Nullable final Collection<CredentialRegistration> credentials) {
+ if (credentials == null) {
+ foundCredentials = CollectionSupport.emptyList();
+ } else {
+ foundCredentials = CollectionSupport.copyToList(credentials);
+ }
+ return this;
+ }
+
+ /**
+ * Get the credentials found to belong to the given search user.
+ *
+ * @return the credentials.
+ */
+ @Nonnull @Unmodifiable @NotLive public Collection<CredentialRegistration> getFoundCredentials() {
+ final Collection<CredentialRegistration> localFoundCredentials = foundCredentials;
+ if (localFoundCredentials == null) {
+ return CollectionSupport.emptyList();
+ }
+ return localFoundCredentials;
+ }
+
+ /**
+ * Set the ID of the credential that is going to be removed.
+ *
+ * @param id the credential identifier
+ *
+ * @return this context
+ */
+ @Nonnull public WebAuthnManagementContext setCredentialIdToRemove(@Nullable final byte[] id) {
+ credentialIdToRemove = id;
+ return this;
+ }
+
+ /**
+ * Get the ID of the credential that is going to be removed.
+ *
+ * @return Returns the credentialIdToRemove.
+ */
+ @Nullable public byte[] getCredentialIdToRemove() {
+ return credentialIdToRemove;
+ }
+
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
index 18393ab..c2b1384 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/navigate/UsernameLookupFromSubjectContext.java
@@ -26,7 +26,7 @@ import net.shibboleth.idp.authn.context.SubjectContext;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * Pull out a username from a the {@link SubjectContext#getPrincipalName()} if it exists.
+ * Pull out a username/principalName from the {@link SubjectContext#getPrincipalName()} if it exists.
*/
public class UsernameLookupFromSubjectContext implements Function<ProfileRequestContext, String> {
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
index 16cbb1b..dc6a754 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/StorageServiceCredentialRepository.java
@@ -24,6 +24,7 @@ import com.yubico.webauthn.data.ByteArray;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
/**
* An extension of the {@link CredentialRepository} interface to support additional read and add operations.
@@ -86,4 +87,24 @@ public interface StorageServiceCredentialRepository extends CredentialRepository
*/
boolean removeRegistrationByUsername(@Nonnull final String username,
@Nonnull final CredentialRegistration credentialRegistration);
+
+
+ /**
+ * Remove the credential by its identifier for any user it is registered for. This should, in practice, only be
+ * registered to a single user.
+ *
+ * @param credentialId the credential to remove
+ *
+ * @return the number of credentials that have been removed. Should always be 1 if removed, 0 if not.
+ */
+ int removeRegistrationByCredentialId(@Nonnull final ByteArray credentialId);
+
+
+ /**
+ * Get all credential registrations known to the repository. Typically used to provide an administrative view
+ * of the repository.
+ *
+ * @return all credential registrations know to the system.
+ */
+ @Nonnull @NotLive @Unmodifiable Set<CredentialRegistration> getAllRegistrations();
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java
new file mode 100644
index 0000000..b8c1427
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnManagementAction.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.BaseWebAuthnAction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * <p>A base class for WebAuthn administrative management function actions.</p>
+ *
+ * <p>In addition to the work performed by {@link AbstractProfileAction}, this action also looks up
+ * and makes available the {@link WebAuthnManagementContext}.</p>
+ *
+ * <p>WebAuthn administrative action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, WebAuthnManagementContext)} method.</p>
+ *
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
+ */
+public abstract class AbstractWebAuthnManagementAction extends BaseWebAuthnAction {
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnManagementAction.class);
+
+ /** Lookup strategy to locate the webauthn management context. */
+ @Nonnull
+ private Function<ProfileRequestContext,WebAuthnManagementContext> webauthnManagementContextLookupStrategy;
+
+ /** The WebAuthn management Context.*/
+ @NonnullBeforeExec private WebAuthnManagementContext webauthnManagementContext;
+
+ /** Constructor.*/
+ protected AbstractWebAuthnManagementAction() {
+ //prc -> WebAuthnContext
+ webauthnManagementContextLookupStrategy = new ChildContextLookup<>(WebAuthnManagementContext.class);
+ }
+
+ /**
+ * Set WebAuthn management context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setWebauthnManagementContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,WebAuthnManagementContext> strategy) {
+ checkSetterPreconditions();
+
+ webauthnManagementContextLookupStrategy =
+ Constraint.isNotNull(strategy, "WebauthnManagementContextLookupStrategy strategy cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ webauthnManagementContext = webauthnManagementContextLookupStrategy.apply(profileRequestContext);
+ if (webauthnManagementContext == null) {
+ log.warn("{} No WebAuthn management context returned by lookup strategy",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+
+ }
+ return doPreExecute(profileRequestContext, webauthnManagementContext);
+ }
+
+ /**
+ * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnManagementContext)} to perform the
+ * actual action. Implementations can not override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ */
+ @Override
+ protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ doExecute(profileRequestContext, webauthnManagementContext);
+ }
+
+ /**
+ * Performs this admin action's pre-execute step. Default implementation just returns true.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param context the WebAuthn management context
+ *
+ * @return true iff execution should continue
+ */
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnManagementContext context) {
+ return true;
+ }
+
+ /**
+ * Performs this webauthn authentication action using the supplied WebAuthn management context. Implementations
+ * should override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param context the WebAuthn management context
+ */
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnManagementContext context) {
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
index 3b0a83a..4b01b18 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
@@ -20,7 +20,6 @@ package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.util.function.Function;
import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
@@ -28,16 +27,11 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
-import com.yubico.fido.metadata.FidoMetadataService;
-
-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.storage.StorageServiceCredentialRepository;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.BaseWebAuthnAction;
import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
@@ -54,7 +48,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
* @post <pre>AuthenticationContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
*/
-public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfileAction {
+public abstract class AbstractWebAuthnRegistrationAction extends BaseWebAuthnAction {
/** Class logger. */
@Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnRegistrationAction.class);
@@ -65,62 +59,14 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
/** The WebAuthn registration Context.*/
@NonnullBeforeExec private WebAuthnRegistrationContext webauthnRegistrationContext;
-
- /** The WebAuthn client to use.*/
- @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
-
- /** The credential respository to store valid credentials in.*/
- @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
-
- /** Optional FIDO metadata service resolver.*/
- @Nullable private FidoMetadataService fidoMetadataService;
+
/** Constructor.*/
protected AbstractWebAuthnRegistrationAction() {
//prc -> WebAuthnContext
webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
}
-
- /**
- * Set the WebAuthn client used to handle registration and authentication ceremonies.
- *
- * @param client The webauthnClient to set.
- */
- public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
- checkSetterPreconditions();
- webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
- }
-
- /**
- * Get the WebAuthn client used to handle registration and authentication ceremonies.
- *
- * @return the webAuthnClient.
- */
- @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
- checkComponentActive();
- return webAuthnClient;
- }
-
- /**
- * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
- *
- * @param service The FIDO metadata service to set.
- */
- public void setFidoMetadataService(@Nullable final FidoMetadataService service) {
- checkSetterPreconditions();
- fidoMetadataService = service;
- }
-
- /**
- * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
- *
- * @return the fido metadata service.
- */
- @Nullable protected FidoMetadataService getFidoMetadataService() {
- checkComponentActive();
- return fidoMetadataService;
- }
-
+
/**
* Set WebAuthn registration context lookup strategy to use.
*
@@ -134,39 +80,6 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
}
-
- /**
- * Set the credential repository used to store WebAuthn credentials.
- *
- * @param repository The respository to set.
- */
- public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
- checkSetterPreconditions();
- credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
- }
-
- /**
- * Get the credential repository used to store WebAuthn credentials.
- *
- * @return the credential repository.
- */
- @NonnullAfterInit protected StorageServiceCredentialRepository getCredentialRepository() {
- return credentialRepository;
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (webAuthnClient == null) {
- throw new ComponentInitializationException("WebAuthn Client can not be null");
- }
- if (credentialRepository == null) {
- throw new ComponentInitializationException("CredentialRepository can not be null");
- }
- }
-
/** {@inheritDoc} */
@Override
protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
similarity index 57%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
index b849c32..0cbe6a7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminDeletePublicKeyCredential.java
@@ -17,8 +17,6 @@
package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
-import java.util.Optional;
-
import javax.annotation.Nonnull;
import org.opensaml.profile.action.ActionSupport;
@@ -28,53 +26,47 @@ 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.context.WebAuthnRegistrationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * An action that removes a public key credential from the credential repository.
+ * An action that removes a public key credential from the credential repository based on the credential Id
+ * found in the management context.
*
* @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION_CTX}
- * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
* @post a credential is removed from credential repository
*/
-public class DeletePublicKeyCredential extends AbstractWebAuthnRegistrationAction {
+public class AdminDeletePublicKeyCredential extends AbstractWebAuthnManagementAction {
/** Class logger. */
@Nonnull
- private final Logger log = LoggerFactory.getLogger(DeletePublicKeyCredential.class);
+ private final Logger log = LoggerFactory.getLogger(AdminDeletePublicKeyCredential.class);
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final WebAuthnRegistrationContext context) {
+ @Nonnull final WebAuthnManagementContext context) {
- 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;
- }
final byte[] credentialId = context.getCredentialIdToRemove();
if (credentialId == null) {
log.error("{} Unable to find credentialId in registration context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+ ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_MANAGEMENT_CTX);
return;
}
+
+ final int removed = getCredentialRepository().removeRegistrationByCredentialId(new ByteArray(credentialId));
- final Optional<CredentialRegistration> credential =
- getCredentialRepository().getRegistrationByUsernameAndCredentialId(
- username, new ByteArray(credentialId));
-
- if (credential.isEmpty()) {
- // This is not an error
- log.debug("{} Unable to find credential to remove, nothing to remove", getLogPrefix());
- } else {
- final boolean removed = getCredentialRepository().removeRegistrationByUsername(username, credential.get());
- log.debug("{} Credential '{}' {} removed", getLogPrefix(), credential.get().getCredentialIdBase64Url(),
- removed ? "was" : "was not");
- }
+ if (log.isDebugEnabled()) {
+ try {
+ log.debug("{} Credential '{}' {} removed", getLogPrefix(), Base64Support.encodeURLSafe(credentialId),
+ removed > 0 ? "was" : "was not");
+ } catch (final EncodingException e) {
+ // Do nothing if the encoding fails.
+ }
+ }
// Remove the key to be deleted in-case the context state is re-used
context.setCredentialIdToRemove(null);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java
new file mode 100644
index 0000000..7168959
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AdminExtractKeyRemovalInformationFromFormRequest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that extracts the credential identifier for removal from the incoming HTTP request.
+ *
+ * @event {AuthnEventIds#NO_CREDENTIALS}
+ * @event {EventIds#IO_ERROR}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnManagementContext.class) != null</pre>
+ * @post add credential ID to remove from the the management context
+ */
+//TODO this is identical other than the context to ExtractKeyRemovalInformationFromFormRequest, we should only need one
+public class AdminExtractKeyRemovalInformationFromFormRequest extends AbstractWebAuthnManagementAction {
+
+ /** Default credential Id parameter name. */
+ @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "credentialId";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AdminExtractKeyRemovalInformationFromFormRequest.class);
+
+ /** Name of credential Id parameter. */
+ @NonnullAfterInit @NotEmpty private String credentialIdParameterName;
+
+ /** Constructor. */
+ public AdminExtractKeyRemovalInformationFromFormRequest() {
+ credentialIdParameterName = DEFAULT_PARAMETER_NAME;
+ }
+
+ /**
+ * Set the name of the credential Id parameter.
+ *
+ * @param field field name
+ */
+ public void setCedentialIdParameterName(@Nonnull @NotEmpty final String field) {
+ checkSetterPreconditions();
+
+ credentialIdParameterName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Credential ID "
+ + "parameter cannot be null or empty");
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnManagementContext context) {
+
+ 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 credentialId = request.getParameter(credentialIdParameterName);
+ if (credentialId == null) {
+ log.debug("{} CredentialID not found in HTTP request",getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ try {
+ // Should be base64 encoded credential.
+ final byte[] credentialIdAsBytes = Base64Support.decode(credentialId);
+ context.setCredentialIdToRemove(credentialIdAsBytes);
+ log.trace("{} Credential to remove '{}'",getLogPrefix(),credentialId);
+ } catch (final DecodingException e) {
+ log.debug("{} Unable to base64 decode credentialID, can not remove credential", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
index b849c32..ecb346e 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/DeletePublicKeyCredential.java
@@ -49,6 +49,8 @@ public class DeletePublicKeyCredential extends AbstractWebAuthnRegistrationActio
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 comes from a form the user
+ // can't manipulate.
final String username = context.getUsername();
if (username == null) {
log.error("{} Unable to find username in registration context", getLogPrefix());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java
new file mode 100644
index 0000000..328b609
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameSearchFromFormRequest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that extracts the username search value from the incoming HTTP request.
+ *
+ * @event {AuthnEventIds#NO_CREDENTIALS}
+ * @event {EventIds#IO_ERROR}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post add the parameter value to the context
+ */
+//TODO just make this generic? would need a consumer to set the information?
+public class ExtractUsernameSearchFromFormRequest extends AbstractWebAuthnManagementAction {
+
+ /** Default credential Id parameter name. */
+ @Nonnull @NotEmpty public static final String DEFAULT_PARAMETER_NAME = "username_search";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameSearchFromFormRequest.class);
+
+ /** Name of the request parameter to extract. */
+ @NonnullAfterInit @NotEmpty private String requestParameterToExtract;
+
+ /** Constructor. */
+ public ExtractUsernameSearchFromFormRequest() {
+ requestParameterToExtract = DEFAULT_PARAMETER_NAME;
+ }
+
+ /**
+ * Set the name of the request parameter to extract the value from.
+ *
+ * @param field field name
+ */
+ public void setRequestParameterName(@Nonnull @NotEmpty final String parameter) {
+ checkSetterPreconditions();
+
+ requestParameterToExtract = Constraint.isNotNull(StringSupport.trimOrNull(parameter), "Request parameter "
+ + "parameter cannot be null or empty");
+ }
+
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnManagementContext context) {
+
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request == null) {
+ log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
+ return;
+ }
+
+ final String parameterValue = request.getParameter(requestParameterToExtract);
+ if (parameterValue == null) {
+ log.debug("{} {} not found in HTTP request",getLogPrefix(), requestParameterToExtract);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
+ return;
+ }
+ log.trace("{} Extracting username '{}' to manage credentials for",getLogPrefix(), parameterValue);
+ context.setSearchUsername(parameterValue);
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java
new file mode 100644
index 0000000..1d8d5c6
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/LookupCredentialsForUser.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * An action that lookups registered credentials for a username contained in the WebAuthn management
+ * context and sets them back onto the context.
+ *
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @post BaseWebAuthnContext.setExistingCredentials() is either null if no existing credentials are found, or contains
+ * the credentials from the credential repository
+ */
+public class LookupCredentialsForUser extends AbstractWebAuthnManagementAction {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(LookupCredentialsForUser.class);
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final WebAuthnManagementContext context) {
+
+ final String userToSearchFor = context.getSearchUsername();
+ if (StringSupport.trimOrNull(userToSearchFor) == null) {
+ log.trace("{} No username to search for", getLogPrefix());
+ context.setFoundCredentials(CollectionSupport.emptyList());
+ } else {
+ log.trace("{} Searching registered credentials for '{}'",getLogPrefix(), userToSearchFor);
+ assert userToSearchFor != null;
+ final Collection<CredentialRegistration> credentials =
+ getCredentialRepository().getRegistrationsByUsername(userToSearchFor);
+ log.debug("{} Found '{}' credentials", getLogPrefix(), credentials.size());
+ context.setFoundCredentials(credentials);
+ }
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java
new file mode 100644
index 0000000..8bf5e09
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnManagementContext.java
@@ -0,0 +1,112 @@
+/*
+ * 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.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action to create (or lookup) the {@link WebAuthnManagementContext} and set the admin principal.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @post a {@link WebAuthnManagementContext} is created.
+ */
+public class PopulateWebAuthnManagementContext extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnManagementContext.class);
+
+ /** Strategy used to locate or create the {@link WebAuthnManagementContext} to populate. */
+ @Nonnull private
+ Function<ProfileRequestContext, WebAuthnManagementContext> webAuthnManagementContextCreationStrategy;
+
+ /** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
+ @Nonnull private Function<ProfileRequestContext, String> principalNameLookupStrategy;
+
+ /** Constructor.*/
+ public PopulateWebAuthnManagementContext() {
+ // Default creates a WebAuthn management context under the profile request context.
+ webAuthnManagementContextCreationStrategy =
+ new ChildContextLookup<>(WebAuthnManagementContext.class, true);
+
+ principalNameLookupStrategy = new CanonicalUsernameLookupStrategy();
+
+ }
+
+ /**
+ * Set the strategy used to lookup or create the WebAuthn managment context.
+ *
+ * @param strategy The strategy to set.
+ */
+ public void setWebAuthnManagementContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, WebAuthnManagementContext> strategy) {
+ checkSetterPreconditions();
+ webAuthnManagementContextCreationStrategy = Constraint.isNotNull(
+ strategy,"WebAuthnManagementContextCreationStrategy can not be null");
+ }
+
+ /**
+ * Set the lookup strategy to use to determine the admin principal.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setPrincipalNameLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+
+ principalNameLookupStrategy = Constraint.isNotNull(strategy, "Principal name lookup strategy cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+
+ final WebAuthnManagementContext context =
+ webAuthnManagementContextCreationStrategy.apply(profileRequestContext);
+ if (context == null) {
+ log.error("{} Error creating WebAuthnManagementContext", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ final String principalName = principalNameLookupStrategy.apply(profileRequestContext);
+ if (principalName == null) {
+ log.error("{} Error creating WebAuthnManagementContext, no principal name found", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+ context.setPrincipalName(principalName);
+
+ log.debug("Created WebAuthn management context for '{}'", context.getPrincipalName());
+ }
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
index 0255a32..c509ade 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -26,9 +26,6 @@ import javax.annotation.Nonnull;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.storage.StorageCapabilities;
-import org.opensaml.storage.StorageSerializer;
-import org.opensaml.storage.StorageService;
import org.slf4j.Logger;
import com.yubico.fido.metadata.AAGUID;
@@ -42,13 +39,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventI
import net.shibboleth.idp.plugin.authn.webauthn.authn.RegistrationResult;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistrationSerializer;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.collection.CollectionSupport;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -63,40 +56,6 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
- /** Backing service. */
- @NonnullAfterInit private StorageService storageService;
-
- /** Storage record serializer. */
- @Nonnull private final StorageSerializer<Set<CredentialRegistration>> serializer;
-
- /** Constructor. */
- public StorePublicKeyCredential() {
- serializer = new CredentialRegistrationSerializer();
- }
-
- /**
- * Set the {@link StorageService} back-end to use.
- *
- * @param storage
- * the back-end to use
- */
- public void setStorageService(@Nonnull final StorageService storage) {
- checkSetterPreconditions();
-
- storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
- final StorageCapabilities caps = storageService.getCapabilities();
- Constraint.isTrue(caps.isServerSide(), "StorageService cannot be client-side");
- }
-
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (storageService == null) {
- throw new ComponentInitializationException("StorageService cannot be null");
- }
- }
-
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final WebAuthnRegistrationContext context) {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
index 2736b5c..6ea7976 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnAuthenticationAction.java
@@ -53,6 +53,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
* @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
*/
+//TODO somehow this should be a child of the base webauthn action (BaseWebAuthnAction)?
public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
/** Class logger. */
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
index 482c410..726840f 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnBaseAction.java
@@ -28,14 +28,10 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
-import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
@@ -53,7 +49,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @post <pre>AuthenticationContext.getSubcontext(BaseWebAuthnContext.class) != null</pre>
*/
//TODO a few abstract action types, remove overlap
-public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
+public abstract class AbstractWebAuthnBaseAction extends BaseWebAuthnAction {
/** Class logger. */
@Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnBaseAction.class);
@@ -64,34 +60,7 @@ public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
/** The WebAuthn base Context.*/
@NonnullBeforeExec private BaseWebAuthnContext webAuthnBaseContext;
-
- /** The WebAuthn client to use.*/
- @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
-
- /** The credential repository to store valid credentials in.*/
- @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
-
-
- /**
- * Set the WebAuthn client used to handle registration and authentication ceremonies.
- *
- * @param client The webauthnClient to set.
- */
- public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
- checkSetterPreconditions();
- webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
- }
-
- /**
- * Get the WebAuthn client used to handle registration and authentication ceremonies.
- *
- * @return the webAuthnClient.
- */
- @NonnullBeforeExec public WebAuthnAuthenticationClient getWebAuthnClient() {
- checkComponentActive();
- return webAuthnClient;
- }
-
+
/** Constructor.*/
protected AbstractWebAuthnBaseAction() {
@@ -113,38 +82,7 @@ public abstract class AbstractWebAuthnBaseAction extends AbstractProfileAction {
webAuthnBaseContextLookupStrategy =
Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
}
-
- /**
- * Set the credential repository used to store WebAuthn credentials.
- *
- * @param repository The respository to set.
- */
- public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
- checkSetterPreconditions();
- credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
- }
-
- /**
- * Get the credential repository used to store WebAuthn credentials.
- *
- * @return the credential repository.
- */
- public StorageServiceCredentialRepository getCredentialRepository() {
- return credentialRepository;
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (webAuthnClient == null) {
- throw new ComponentInitializationException("WebAuthn Client can not be null");
- }
- if (credentialRepository == null) {
- throw new ComponentInitializationException("CredentialRepository can not be null");
- }
- }
+
/** {@inheritDoc} */
@Override
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
new file mode 100644
index 0000000..c4be7c8
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
@@ -0,0 +1,116 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import com.yubico.fido.metadata.FidoMetadataService;
+
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A base action for WebAuthn actions that require access to the client, credential repository, and metadata service.
+ */
+public abstract class BaseWebAuthnAction extends AbstractProfileAction {
+
+ /** The WebAuthn client to use.*/
+ @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+
+ /** The credential respository to store valid credentials in.*/
+ @NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
+
+ /** Optional FIDO metadata service resolver.*/
+ @Nullable private FidoMetadataService fidoMetadataService;
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (webAuthnClient == null) {
+ throw new ComponentInitializationException("WebAuthn Client can not be null");
+ }
+ if (credentialRepository == null) {
+ throw new ComponentInitializationException("CredentialRepository can not be null");
+ }
+ }
+
+ /**
+ * Set the WebAuthn client used to handle registration and authentication ceremonies.
+ *
+ * @param client The webauthnClient to set.
+ */
+ public void setWebAuthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
+ checkSetterPreconditions();
+ webAuthnClient = Constraint.isNotNull(client, "WebAuthn client can not be null");
+ }
+
+ /**
+ * Get the WebAuthn client used to handle registration and authentication ceremonies.
+ *
+ * @return the webAuthnClient.
+ */
+ @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
+ checkComponentActive();
+ return webAuthnClient;
+ }
+
+ /**
+ * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @param service The FIDO metadata service to set.
+ */
+ public void setFidoMetadataService(@Nullable final FidoMetadataService service) {
+ checkSetterPreconditions();
+ fidoMetadataService = service;
+ }
+
+ /**
+ * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @return the fido metadata service.
+ */
+ @Nullable protected FidoMetadataService getFidoMetadataService() {
+ checkComponentActive();
+ return fidoMetadataService;
+ }
+
+ /**
+ * Set the credential repository used to store WebAuthn credentials.
+ *
+ * @param repository The respository to set.
+ */
+ public void setCredentialRepository(@Nonnull final StorageServiceCredentialRepository repository) {
+ checkSetterPreconditions();
+ credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+ }
+
+ /**
+ * Get the credential repository used to store WebAuthn credentials.
+ *
+ * @return the credential repository.
+ */
+ @NonnullAfterInit protected StorageServiceCredentialRepository getCredentialRepository() {
+ return credentialRepository;
+ }
+
+}
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 c55a821..9dbcb68 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
@@ -19,6 +19,8 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.locks.Lock;
@@ -45,7 +47,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.exception.CredentialRepositoryEx
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -55,8 +59,8 @@ import net.shibboleth.shared.logic.ConstraintViolationException;
/**
* A {@link CredentialRepository} adaptor for the Shibboleth {@link StorageService}.
*
- * <p>Username is used as the key to the storage context. The object to store (value of the storage record) is a set of
- * {@link CredentialRegistration registered credentials}. Storage records do not expire.</p>
+ * <p>Username (user.name) is used as the key to the storage context. The object to store (value of the storage record)
+ * is a set of {@link CredentialRegistration registered credentials}. Storage records do not expire.</p>
*
* <p>Note, any exception is wrapped in an unchecked {@link CredentialRepositoryException}. If the caller does not deem
* this terminal, they should catch and handle that error appropriately.</p>
@@ -358,6 +362,63 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
writeLock.unlock();
}
}
+
+ /** {@inheritDoc} */
+ @Override
+ public int removeRegistrationByCredentialId(final ByteArray credentialId) {
+ checkComponentActive();
+ final Lock writeLock = lock.writeLock();
+ try {
+ writeLock.lock();
+ int removalCount = 0;
+ for (final Iterator<String> i = storageService.getContextKeys(STORAGE_CONTEXT, null).iterator();
+ i.hasNext();) {
+ final String usernameKey = i.next();
+ assert usernameKey != null;
+
+ final Set<CredentialRegistration> existingRegistrations = getRegistrationsByUsername(usernameKey);
+
+ if (existingRegistrations.isEmpty()) {
+ log.trace("No existing registrations, nothing to remove");
+ // Nothing to do
+ continue;
+ }
+
+ // Find an matching credential from the existing registration
+ final List<CredentialRegistration> matchingRegistrations = existingRegistrations.stream()
+ .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
+ .collect(CollectionSupport.nonnullCollector(Collectors.toList())).get();
+
+ if (matchingRegistrations.size() != 1) {
+ // If no match, there is nothing to remove
+ continue;
+ }
+ final CredentialRegistration registrationToRemove = matchingRegistrations.get(0);
+
+ final Set<CredentialRegistration> updateSet = new LinkedHashSet<>(existingRegistrations);
+ updateSet.remove(registrationToRemove);
+ if (updateSet.isEmpty()) {
+ //remove the entire storage record
+ if (storageService.delete(STORAGE_CONTEXT, usernameKey)) {
+ removalCount++;
+ }
+ } else {
+ //else, add back what remains
+ assert serializer != null;
+ if (storageService.update(STORAGE_CONTEXT, usernameKey, updateSet, serializer, null)) {
+ removalCount ++;
+ }
+ }
+ }
+ // Nothing to do if the registration does not exist
+ return removalCount;
+
+ } catch (final IOException e) {
+ throw new CredentialRepositoryException(e);
+ } finally {
+ writeLock.unlock();
+ }
+ }
/** {@inheritDoc} */
@Override
@@ -403,5 +464,30 @@ public class IdPStorageServiceCredentialRespository extends AbstractIdentifiable
}
}
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull @NotLive @Unmodifiable public Set<CredentialRegistration> getAllRegistrations() {
+ checkComponentActive();
+ final Lock readLock = lock.readLock();
+ try {
+ readLock.lock();
+ final Set<CredentialRegistration> allCredentials = new HashSet<>();
+ for (final Iterator<String> i = storageService.getContextKeys(STORAGE_CONTEXT, null).iterator();
+ i.hasNext();) {
+ final String usernameKey = i.next();
+ assert usernameKey != null;
+ allCredentials.addAll(getRegistrationsByUsername(usernameKey).stream()
+ .filter(Objects::nonNull)
+ .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableSet())).get());
+
+ }
+ return CollectionSupport.copyToSet(allCredentials);
+ } catch (final IOException e) {
+ throw new CredentialRepositoryException(e);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
}
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 8e42bb9..307a389 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -45,7 +45,7 @@
<!-- Admim flow for WebAuthn credential registration -->
<bean parent="shibboleth.AdminFlow"
- c:id="http://shibboleth.net/ns/profiles/webauthn/register-credential"
+ c:id="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential"
p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByIPAddress}"
p:nonBrowserSupported="false"
@@ -65,6 +65,28 @@
</property>
</bean>
+ <!-- Admim flow for admins to manage user credential registrations -->
+ <bean parent="shibboleth.AdminFlow"
+ c:id="http://shibboleth.net/ns/profiles/webauthn/manage-credentials"
+ p:loggingId="%{idp.authn.webauthn.admin.management.logging:WebAuthnCredentialManagement}"
+ p:policyName="%{idp.authn.webauthn.admin.management.accessPolicy:AccessByAdminUser}"
+ p:nonBrowserSupported="false"
+ p:authenticated="true"
+ p:resolveAttributes="%{idp.authn.webauthn.admin.management.resolveIdentityAttributes:true}">
+ <property name="authenticationFlows">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.admin.management.authenticationFlows:}'.trim()}" />
+ </property>
+ <property name="postAuthenticationFlows">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.admin.management.postAuthenticationFlows:}'.trim()}" />
+ </property>
+ <property name="defaultAuthenticationMethodsByString">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.authn.webauthn.admin.management.defaultAuthenticationMethods:}'.trim()}" />
+ </property>
+ </bean>
+
<!-- Singleton clients and repositories -->
<!-- TODO configure these with getbeans and properties -->
<bean id="shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory" scope="singleton"
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml
new file mode 100644
index 0000000..84c3fd5
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-beans.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean id="shibboleth.AdminProfileId" class="java.lang.String"
+ c:_0="http://shibboleth.net/ns/profiles/webauthn/manage-credentials" />
+
+ <!-- Flow Functions -->
+
+ <bean id="shibboleth.ChildLookup.WebAuthnManagementContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext) }" />
+
+
+
+ <!-- Abstract parent beans -->
+
+ <bean id="AbstractWebAuthnManagementAction" scope="prototype" abstract="true"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"
+ p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"
+ p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
+
+
+ <!-- Flow beans -->
+
+ <bean id="PopulateWebAuthnManagementContext" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnManagementContext">
+ <property name="principalNameLookupStrategy">
+ <bean id="principalNameFromAuthnResult" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromSubjectContext" />
+ </property>
+ </bean>
+
+ <bean id="ExtractUsernameSearchFromFormRequest" parent="AbstractWebAuthnManagementAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractUsernameSearchFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+ <bean id="LookupCredentialsForUser" parent="AbstractWebAuthnBaseAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.LookupCredentialsForUser"
+ p:webauthnManagementContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnManagementContext" />
+
+ <bean id="ExtractKeyRemovalInformationFromFormRequest" parent="AbstractWebAuthnManagementAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AdminExtractKeyRemovalInformationFromFormRequest"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+ <bean id="DeletePublicKeyCredential" parent="AbstractWebAuthnManagementAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AdminDeletePublicKeyCredential" />
+
+
+
+</beans>
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-management/webauthn-management-flow.xml
similarity index 63%
copy from webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
copy to webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-management/webauthn-management-flow.xml
index fe13bc0..33af679 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-management/webauthn-management-flow.xml
@@ -10,64 +10,62 @@
<evaluate expression="FlowStartPopulateAuditContext" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="CollectUsernameView" />
+ <!-- Branch to determine if authentication is required. -->
+ <transition on="proceed" to="DoAdminPreamble" />
</action-state>
+ <!-- Resume actual flow processing. -->
+
+ <action-state id="DoProfileWork">
+ <evaluate expression="CheckAccess" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="PopulateManagementContext" />
+ </action-state>
+
+ <action-state id="PopulateManagementContext">
+ <evaluate expression="PopulateWebAuthnManagementContext"/>
+
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="UsernameSearchView" />
+ </action-state>
- <view-state id="CollectUsernameView" view="webauthn/webauthn-register-username">
+ <view-state id="UsernameSearchView" view="webauthn/webauthn-management-search">
<on-render>
<evaluate expression="environment" result="viewScope.environment" />
<evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext))" result="viewScope.webAuthnManContext" />
<evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPDigester')" result="requestScope.cspDigester" />
- <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />
- <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />
<evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="T(net.shibboleth.idp.plugin.authn.webauthn.impl.WebAuthnEncoder)" result="viewScope.webAuthnEncoder"/>
<evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
<evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
</on-render>
- <transition on="proceed" to="ExtractUsernameAndPopulateContext" />
-
+
+ <transition on="proceed" to="ExtractSearchFields" />
+ <transition on="finish" to="ManagementComplete" />
</view-state>
- <!-- TODO if we want to do this, we would want this to populate a different context -->
- <action-state id="ExtractUsernameAndPopulateContext">
- <evaluate expression="PopulateInitialWebAuthnRegistrationContext"/>
- <evaluate expression="LookupRegisteredCredentials"/>
+ <action-state id="ExtractSearchFields">
+ <evaluate expression="ExtractUsernameSearchFromFormRequest"/>
+
<evaluate expression="'proceed'" />
-
- <!-- Branch to determine if authentication is required. -->
- <transition on="proceed" to="DoAdminPreamble" />
- </action-state>
-
- <!-- Resume actual flow processing. -->
-
- <action-state id="DoProfileWork">
- <evaluate expression="CheckAccess" />
- <evaluate expression="'proceed'" />
-
- <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions" />
- </action-state>
+ <transition on="proceed" to="LookupCredentials" />
+ </action-state>
- <action-state id="GeneratePublicKeyCredentialCreationOptions">
- <evaluate expression="PopulateWebAuthnRegistrationContext"/>
- <evaluate expression="LookupRegisteredCredentials"/>
- <evaluate expression="GenerateServerChallenge"/>
- <evaluate expression="AddUserId"/>
- <evaluate expression="AddDisplayName"/>
- <evaluate expression="AddResidentKeyRequirement"/>
- <evaluate expression="AddAuthenticatorAttachmentRequirement"/>
- <evaluate expression="AddAttestationConveyancePreference"/>
- <evaluate expression="AddUserVerificationRequired"/>
- <evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
+ <action-state id="LookupCredentials">
+ <evaluate expression="LookupCredentialsForUser"/>
+
<evaluate expression="'proceed'" />
- <transition on="proceed" to="DisplayWebAuthnView" />
- </action-state>
+ <transition on="proceed" to="ManagementView" />
+ </action-state>
- <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-register">
+ <view-state id="ManagementView" view="webauthn/webauthn-management">
<on-render>
<evaluate expression="environment" result="viewScope.environment" />
<evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
- <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext))" result="viewScope.webAuthnManContext" />
<evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPDigester')" result="requestScope.cspDigester" />
<evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('WebAuthnCSPNonce')" result="requestScope.cspNonce" />
<evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
@@ -76,41 +74,25 @@
<evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
</on-render>
- <transition on="finish" to="RegistrationComplete" />
- <transition on="addKey" to="AddKey" />
+ <transition on="again" to="UsernameSearchView" />
+ <transition on="finish" to="ManagementComplete" />
<transition on="deleteKey" to="DeleteKey" />
- </view-state>
-
- <action-state id="AddKey">
- <evaluate expression="ExtractAuthenticatorAttestationFromFormRequest"/>
- <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
- <evaluate expression="StorePublicKeyCredential"/>
- <evaluate expression="'proceed'" />
-
- <transition on="InvalidRegistration" to="GeneratePublicKeyCredentialCreationOptions">
- <!-- TODO externalise message bundle-->
- <set name="flashScope.registrationErrorOutcomes" value="'Key registration unsuccessful'"/>
- </transition>
- <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions">
- <!-- TODO externalise message bundle-->
- <set name="flashScope.registrationOutcomes" value="'Key was registered successfully'"/>
- </transition>
- </action-state>
+ </view-state>
<action-state id="DeleteKey">
<evaluate expression="ExtractKeyRemovalInformationFromFormRequest"/>
<evaluate expression="DeletePublicKeyCredential"/>
<evaluate expression="'proceed'" />
- <transition on="proceed" to="GeneratePublicKeyCredentialCreationOptions">
+ <transition on="proceed" to="LookupCredentials">
<!-- TODO externalise message bundle-->
- <set name="flashScope.registrationOutcomes" value="'Key was removed successfully'"/>
+ <set name="flashScope.managementOutcomes" value="'Key was removed successfully'"/>
</transition>
- </action-state>
+ </action-state>
- <end-state id="RegistrationComplete" view="webauthn/webauthn-register-end">
+ <end-state id="ManagementComplete" view="webauthn/webauthn-end">
<on-entry>
<evaluate expression="environment" result="requestScope.environment" />
<evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
@@ -124,7 +106,7 @@
</on-entry>
</end-state>
- <bean-import resource="webauthn-registration-beans.xml" />
+ <bean-import resource="webauthn-management-beans.xml" />
<bean-import resource="../../authn/WebAuthn/webauthn-abstract-beans.xml" />
</flow>
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 aa5f3d4..521e29f 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
@@ -10,7 +10,7 @@
default-init-method="initialize" default-destroy-method="destroy">
<bean id="shibboleth.AdminProfileId" class="java.lang.String"
- c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
+ c:_0="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential" />
<!-- Flow Functions -->
@@ -110,14 +110,9 @@
<bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
-
<bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.StorePublicKeyCredential"
- p:storageService-ref="shibboleth.authn.webauthn.StorageService"
p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository" />
- <!-- postconfig.xml? -->
- <bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
- class="org.opensaml.storage.impl.MemoryStorageService" p:cleanupInterval="PT10M" />
</beans>
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 fe13bc0..d193b7f 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
@@ -110,7 +110,7 @@
</action-state>
- <end-state id="RegistrationComplete" view="webauthn/webauthn-register-end">
+ <end-state id="RegistrationComplete" view="webauthn/webauthn-end">
<on-entry>
<evaluate expression="environment" result="requestScope.environment" />
<evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
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 13ba04e..74e05f9 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
@@ -87,6 +87,16 @@ idp.authn.webauthn.supportedPrincipals = \
# State the preference of the IdP during registration to receive an authenticator attestation. One-of 'none', 'indirect', 'direct', or 'enterprise'.
#idp.authn.webauthn.registration.attestationConveyancePreference = none
+
+#### Administrator properties
+
+#idp.authn.webauthn.admin.management.logging = WebAuthnCredentialManagement
+#idp.authn.webauthn.admin.management.accessPolicy = AccessByAdmin
+#idp.authn.webauthn.admin.management.resolveIdentityAttributes = true
+#idp.authn.webauthn.admin.management.authenticationFlows
+#idp.authn.webauthn.admin.management.postAuthenticationFlows
+#idp.authn.webauthn.admin.management.defaultAuthenticationMethod
+
#### Authentication properties
# Which type of flow is supported? Usernameless or passwordless
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
index 6e944e2..431e2c1 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/module.properties
@@ -40,7 +40,13 @@ idp.authn.WebAuthn.8.dest = edit-webapp/js/webauthn/webauthn-json.browser-ponyfi
idp.authn.WebAuthn.9.src = /net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-support.js
idp.authn.WebAuthn.9.dest = edit-webapp/js/webauthn/webauthn-support.js
-idp.authn.WebAuthn.10.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
-idp.authn.WebAuthn.10.dest = views/webauthn/webauthn-register-end.vm
+idp.authn.WebAuthn.10.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
+idp.authn.WebAuthn.10.dest = views/webauthn/webauthn-end.vm
+
+idp.authn.WebAuthn.11.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
+idp.authn.WebAuthn.11.dest = views/webauthn/webauthn-management.vm
+
+idp.authn.WebAuthn.12.src = /net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
+idp.authn.WebAuthn.12.dest = views/webauthn/webauthn-management-search.vm
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
index f898d35..429b5dc 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn-username.vm
@@ -1,5 +1,5 @@
##
-## Velocity Template for collection of username for Duo Passwordless use
+## Velocity Template for collection of username for WebAuthn Passwordless use
##
## Velocity context will contain the following properties
## flowExecutionUrl - the form action location
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 2ea24b2..efa7284 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
@@ -1,5 +1,5 @@
##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn authentication view
##
## Velocity context will contain the following properties
## flowExecutionUrl - the form action location
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
similarity index 58%
rename from webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
rename to webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
index 937d077..c66ba71 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-end.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-end.vm
@@ -1,5 +1,5 @@
##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn end state view
##
## Velocity context will contain the following properties
## flowExecutionUrl - the form action location
@@ -34,9 +34,35 @@
<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>
- #springMessageText("idp.webauthn.authn.register.ended", "Your registration session has ended")
+ <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
+ <div class="content">
+ #springMessageText("idp.webauthn.authn.register.ended", "Your registration session has ended")
+ </div>
</section>
<footer>
<div class="container container-footer">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
new file mode 100644
index 0000000..900bcfd
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management-search.vm
@@ -0,0 +1,71 @@
+##
+## Velocity Template for displaying the WebAuthn administrator view
+##
+## 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
+## webAuthnManContext = WebAuthn management context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## webAuthnEncoder - WebAuthnEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+##
+<!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">
+ <script type="text/javascript" src="$request.getContextPath()/js/webauthn/webauthn-support.js"></script>
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/webauthn.css">
+
+ </head>
+ <body>
+ <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" )" />
+ </header>
+ <section>
+
+ <div class="centre">
+ <div class="hidden output-message output--error" id="error_div">
+ <p id="error_message"></p>
+ </div>
+ <div>
+ <h1>#springMessageText("idp.webauthn.admin.search.title", "Search For Users")</h1>
+ <form id="username_search_form" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <label for="username_search">#springMessageText("idp.webauthn.admin.search.label", "Username")</label>
+ <input id="username_search" name="username_search" type="text"/>
+
+ <div class="grid">
+ <div class="grid-item">
+ <button id="search_button" type="submit" name="_eventId_proceed">#springMessageText("idp.webauthn.admin.button.search", "Lookup")</button>
+ <button id="finish_button" type="submit" name="_eventId_finish">#springMessageText("idp.webauthn.admin.button.finish", "Finish")</button>
+ </div>
+ </div>
+
+ </form>
+
+ </div>
+ </div>
+ </section>
+ </main>
+ <footer>
+ <div class="cc">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+ </body>
+</html>
\ No newline at end of file
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
new file mode 100644
index 0000000..c4ed5d2
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-management.vm
@@ -0,0 +1,125 @@
+##
+## Velocity Template for displaying the WebAuthn administrator view
+##
+## 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
+## webAuthnManContext = WebAuthn management context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## webAuthnEncoder - WebAuthnEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($debug = $environment.getProperty("idp.authn.webauthn.ui.debug", "false"))
+##
+<!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">
+ <script type="text/javascript" src="$request.getContextPath()/js/webauthn/webauthn-support.js"></script>
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()/css/webauthn.css">
+
+ </head>
+ <body>
+ <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" )" />
+ </header>
+ <section>
+ <div id="supportedDiv">
+ <div class="centre">
+ #if ($managementOutcomes)
+ <p id="reg-success-outcome" class="output-message output--success">$registrationOutcomes</p>
+ #end
+ #if ($managementErrorOutcomes)
+ <p id="reg-error-outcome" class="output-message output--error">$registrationErrorOutcomes</p>
+ #end
+ <div class="hidden output-message output--error" id="error_div">
+ <p id="error_message"></p>
+ </div>
+ <div>
+ <h1>Registered keys for '$encoder.encodeForHTML($webAuthnManContext.searchUsername)'</h1>
+ #if ($webAuthnManContext.foundCredentials)
+ <table>
+ <tr>
+ <th>#springMessageText("idp.webauthn.admin.table.header.userName", "User")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.keyName", "Key Name")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.authenticatorDescription", "Authenticator")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.transports", "Transports")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.passkey", "Passkey?")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.registrationTime", "Registration Time")</th>
+ <th>#springMessageText("idp.webauthn.admin.table.header.action", "Action")</th>
+ </tr>
+ #foreach($cred in $webAuthnManContext.foundCredentials)
+ <tr>
+ <td>$encoder.encodeForHTML($cred.username)</td>
+ <td>$encoder.encodeForHTML($cred.nickname)</td>
+ #if ($cred.authenticatorDescription)
+ <td>
+ #if ($cred.icon)
+ <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon) alt="authenticator-icon"/>
+ #end
+ $encoder.encodeForHTML($cred.authenticatorDescription)</td>
+ #else
+ <td>#springMessageText("idp.webauthn.admin.table.unknownCredential", "unknown")</td>
+ #end
+ <td>$encoder.encodeForHTML($webAuthnEncoder.formatTransports($cred.transports))</td>
+ <td>$encoder.encodeForHTML($webAuthnEncoder.formatDiscoverable($cred.isDiscoverable()))</td>
+ <td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.registrationTime))</td>
+ <td>
+ <form id="delete_key_form" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <input type="hidden" name="credentialId" value="$cred.credentialIdBase64Url"/>
+ <button class="webauthn-table-button" onclick="return confirm('#springMessageText("idp.webauthn.admin.credential.remove.confirm", "Are you sure")');" id="removeButton" type="submit" name="_eventId_deleteKey">
+ #springMessageText("idp.webauthn.admin.credential.remove", "Remove")</button>
+ </form>
+ </td>
+ </tr>
+ #end
+ </table>
+ #else
+ <div><span>#springMessageText("idp.webauthn.admin.noKeys", "There are no registered keys")</span></div>
+ #end
+
+ <br/>
+
+ <form id="finish_button_form" action="$flowExecutionUrl" method="post" class="inline">
+ #parse("csrf/csrf.vm")
+ <div class="grid">
+ <div class="grid-item">
+ <button id="searchButton" type="submit" name="_eventId_again">#springMessageText("idp.webauthn.admin.search.again", "Search Again")</button>
+ <button id="finish_button" type="submit" name="_eventId_finish">#springMessageText("idp.webauthn.admin.finish", "Finish")</button>
+ </div>
+ </div>
+
+ </form>
+
+
+
+ </div>
+
+ </div>
+ <div id="unsupportedDiv" class="hidden">
+ #springMessageText("idp.webauthn.admin.unsupported", "Your browser is not WebAuthn compatible")
+ </div>
+ </div>
+ </section>
+ </main>
+ <footer>
+ <div class="cc">
+ <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </div>
+ </body>
+</html>
\ No newline at end of file
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 6f93cde..c2981d4 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
@@ -1,5 +1,5 @@
##
-## Velocity Template for DisplayWebauthnView view-state
+## Velocity Template for displaying the WebAuthn user crential registration view
##
## Velocity context will contain the following properties
## flowExecutionUrl - the form action location
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
index 87cb832..50624af 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/IdPStorageServiceCredentialRespositoryTest.java
@@ -153,6 +153,165 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
}
+ @Test
+ public void testRemoveRegistrationByCredentialId() throws Exception {
+
+ final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+
+ var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ final var iterator = registrations.iterator();
+ final var credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+
+ repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+
+ registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 0);
+ }
+
+ @Test
+ public void testRemoveRegistrationByCredentialId_TwoRegistrations() throws Exception {
+
+ final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+
+ final CredentialRegistration registrationTwo = createRegistration("pdoe", "Poe Doe", "user-handle-2".getBytes());
+ repo.addRegistrationByUsername("pdoe", registrationTwo);
+
+ var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ var iterator = registrations.iterator();
+ var credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+
+ registrations = repo.getRegistrationsByUsername("pdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ iterator = registrations.iterator();
+ credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"pdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registrationTwo.getCredential().getCredentialId());
+
+ final int removalCount = repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+
+ assertEquals(removalCount, 1);
+
+ registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 0);
+
+ registrations = repo.getRegistrationsByUsername("pdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testRemoveRegistrationByCredentialId_TwoRegistrationsSameAccount() throws Exception {
+
+ final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ final CredentialRegistration registrationTwo = createRegistration("jdoe", "John Doe", "user-handle-2".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+ repo.addRegistrationByUsername("jdoe", registrationTwo);
+
+ var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 2);
+ final var iterator = registrations.iterator();
+ var credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+ credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registrationTwo.getCredential().getCredentialId());
+
+
+ final int removalCount =
+ repo.removeRegistrationByCredentialId(registrationTwo.getCredential().getCredentialId());
+
+ assertEquals(removalCount, 1);
+
+ registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ }
+
+ @Test
+ public void testRemoveRegistrationByCredentialId_TwoRegistrationsSameCredential() throws Exception {
+
+ final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+ // Both share the same registration, should not happen in practice
+ repo.addRegistrationByUsername("pdoe", registration);
+
+ var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ var iterator = registrations.iterator();
+ var credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+
+ registrations = repo.getRegistrationsByUsername("pdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ iterator = registrations.iterator();
+ credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+
+ final int removalCount = repo.removeRegistrationByCredentialId(registration.getCredential().getCredentialId());
+
+ assertEquals(removalCount, 2);
+
+ registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 0);
+
+ registrations = repo.getRegistrationsByUsername("pdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 0);
+ }
+
+ @Test
+ public void testRemoveRegistrationByCredentialId_NothingToRemove() throws Exception {
+
+ final CredentialRegistration registration = createRegistration("jdoe", "John Doe", "user-handle".getBytes());
+ repo.addRegistrationByUsername("jdoe", registration);
+
+ var registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+ final var iterator = registrations.iterator();
+ final var credReg = iterator.next();
+ assertEquals(credReg.getUsername(),"jdoe");
+ assertEquals(credReg.getCredential().getCredentialId(),
+ registration.getCredential().getCredentialId());
+
+ final int removalCount = repo.removeRegistrationByCredentialId(new ByteArray("new-cred".getBytes()));
+
+ assertEquals(removalCount, 0);
+
+ registrations = repo.getRegistrationsByUsername("jdoe");
+ assertNotNull(registrations);
+ assertEquals(registrations.size(), 1);
+
+ }
+
@Test
public void testRemoveRegistrationByUsername() throws Exception {
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
index 775a301..15a208b 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -46,6 +46,7 @@ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+import net.shibboleth.shared.collection.CollectionSupport;
/**
* In memory credential repository to use for testing.
@@ -176,4 +177,18 @@ public class InMemoryRegistrationStorage implements StorageServiceCredentialRepo
// TODO Fill this in if needed.
return true;
}
+
+ /** {@inheritDoc} */
+ @Override
+ public int removeRegistrationByCredentialId(final ByteArray credentialId) {
+ // TODO Fill this in if needed.
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<CredentialRegistration> getAllRegistrations() {
+ // TODO Fill this in if needed.
+ return CollectionSupport.emptySet();
+ }
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list