[java-idp-plugin-webauthn] branch main updated: Add unit tests

Phil Smart philip.smart at jisc.ac.uk
Wed Mar 20 17:11:44 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=26c070d3415fb863f932f5a2d2b4a3a2d2c20535

The following commit(s) were added to refs/heads/main by this push:
     new 26c070d  Add unit tests
26c070d is described below

commit 26c070d3415fb863f932f5a2d2b4a3a2d2c20535
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Mar 20 17:11:40 2024 +0000

    Add unit tests
---
 .../client/WebAuthnAuthenticationClient.java       |   2 +-
 .../context/WebAuthnAuthenticationContext.java     |  27 ---
 .../authn/webauthn/admin/impl/AddUserId.java       |   2 +-
 .../storage/impl/InMemoryRegistrationStorage.java  | 213 ---------------------
 .../AddAttestationConveyancePreferenceTest.java    | 108 +++++++++++
 .../AddAuthenticatorAttachmentRequirementTest.java |  87 +++++++++
 .../admin/impl/AddResidentKeyRequirementTest.java  |  87 +++++++++
 .../authn/webauthn/admin/impl/AddUserIdTest.java   | 188 ++++++++++++++++++
 .../impl/ExceptionThrowingMockWebAuthnClient.java  |  69 +++++++
 .../YubicoWebauthnAuthenticationClientTest.java    |   9 +-
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  |  77 ++++++++
 .../impl/AddUserVerificationRequirementTest.java   |  90 +++++++++
 ...reatePublicKeyCredentialRequestOptionsTest.java | 124 ++++++++++++
 .../impl/ValidatePublicKeyCredentialTest.java      |  48 +----
 .../impl/CredentialRegistrationSerializerTest.java |   4 +-
 ...IdPStorageServiceCredentialRespositoryTest.java |   3 +-
 .../storage/impl/InMemoryRegistrationStorage.java  | 170 ++++++++++++++++
 17 files changed, 1013 insertions(+), 295 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
index bbf3da3..a248160 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
@@ -37,7 +37,7 @@ public interface WebAuthnAuthenticationClient {
       *          
       * @param requestParams the options that should be present in the authentication request. 
       * 
-      * @return a PublicKeyCredentialRequestOptions object to supply the WebAuthn 'get' call
+      * @return a PublicKeyCredentialRequestOptions object to supply to the WebAuthn 'get' call
       * 
       * @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
       *         
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
index e5b026e..4c98aef 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -25,10 +25,6 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
    
     /** The public key credential request options for authentication.*/ 
     @Nullable private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
-    
-    // TODO maybe generate the JSON during the webflow action to populate the view
-    /** The public key credential request options for authentication represented as a JSON string.*/ 
-    @Nullable private String publicKeyCredentialRequestOptionsJSON;
 
     /**
      * Get the credential Id.
@@ -97,28 +93,5 @@ public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
         getAuthenticatorAssertionResponse() {
         return authenticatorAssertionResponse;
     }
-        
-    
-    /**
-     * Get the public key credential request options to use in JSON format.
-     * 
-     * @return the publicKeyCredentialRequestOptions in JSON format.
-     */
-    public String getPublicKeyCredentialRequestOptionsJSON() {
-        return publicKeyCredentialRequestOptionsJSON;
-    }
-    
-    /**
-     * Set the public key credential request options to use in JSON format.
-     * 
-     * @param requestOptionsJSON The options to set.
-     * 
-     * @return this context
-     */
-    public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptionsJSON(
-            @Nullable final String requestOptionsJSON) {
-        publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
-        return this;
-    }
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
index 08b7621..ec72a7b 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserId.java
@@ -88,7 +88,7 @@ public class AddUserId extends AbstractWebAuthnRegistrationAction {
         username = context.getUsername();
         if (username == null) {
             log.error("{} Username not available in registration context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
             return false;
         }
         
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
deleted file mode 100644
index 5a82ec2..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright (c) 2018, Yubico AB
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-// 1. Redistributions of source code must retain the above copyright notice, this
-//    list of conditions and the following disclaimer.
-//
-// 2. Redistributions in binary form must reproduce the above copyright notice,
-//    this list of conditions and the following disclaimer in the documentation
-//    and/or other materials provided with the distribution.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.yubico.webauthn.AssertionResult;
-import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
-
-import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
-
-/**
- * Use {@link IdPStorageServiceCredentialRespository}
- */
-// move to test package
- at Deprecated
-public class InMemoryRegistrationStorage implements StorageServiceCredentialRepository {
-
-  private final Cache<String, Set<CredentialRegistration>> storage =
-      CacheBuilder.newBuilder().maximumSize(1000).expireAfterAccess(1, TimeUnit.DAYS).build();
-
-  private static final Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class);
-
-  ////////////////////////////////////////////////////////////////////////////////
-  // The following methods are required by the CredentialRepository interface.
-  ////////////////////////////////////////////////////////////////////////////////
-
-  @Override
-  public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
-    return getRegistrationsByUsername(username).stream()
-        .map(
-            registration ->
-                PublicKeyCredentialDescriptor.builder()
-                    .id(registration.getCredential().getCredentialId())
-                    .transports(registration.getTransports())
-                    .build())
-        .collect(Collectors.toSet());
-  }
-
-  @Override
-  public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
-    return getRegistrationsByUserHandle(userHandle).stream()
-        .findAny()
-        .map(CredentialRegistration::getUsername);
-  }
-
-  @Override
-  public Optional<ByteArray> getUserHandleForUsername(final String username) {
-    return getRegistrationsByUsername(username).stream()
-        .findAny()
-        .map(reg -> reg.getUserIdentity().getId());
-  }
-
-  @Override
-  public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
-    final Optional<CredentialRegistration> registrationMaybe =
-        storage.asMap().values().stream()
-            .flatMap(Collection::stream)
-            .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId()))
-            .findAny();
-
-    logger.debug(
-        "lookup credential ID: {}, user handle: {}; result: {}",
-        credentialId,
-        userHandle,
-        registrationMaybe);
-    return registrationMaybe.map(
-        registration ->
-            RegisteredCredential.builder()
-                .credentialId(registration.getCredential().getCredentialId())
-                .userHandle(registration.getUserIdentity().getId())
-                .publicKeyCose(registration.getCredential().getPublicKeyCose())
-                .signatureCount(registration.getCredential().getSignatureCount())
-                .build());
-  }
-
-  @Override
-  public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
-    return Collections.unmodifiableSet(
-        storage.asMap().values().stream()
-            .flatMap(Collection::stream)
-            .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
-            .map(
-                reg ->
-                    RegisteredCredential.builder()
-                        .credentialId(reg.getCredential().getCredentialId())
-                        .userHandle(reg.getUserIdentity().getId())
-                        .publicKeyCose(reg.getCredential().getPublicKeyCose())
-                        .signatureCount(reg.getCredential().getSignatureCount())
-                        .build())
-            .collect(Collectors.toSet()));
-  }
-
-  ////////////////////////////////////////////////////////////////////////////////
-  // The following methods are specific to this demo application.
-  ////////////////////////////////////////////////////////////////////////////////
-
-  public boolean addRegistrationByUsername(final String username, final CredentialRegistration reg) {
-    try {
-      return storage.get(username, HashSet::new).add(reg);
-    } catch (final ExecutionException e) {
-      logger.error("Failed to add registration", e);
-      throw new RuntimeException(e);
-    }
-  }
-
-  public Set<CredentialRegistration> getRegistrationsByUsername(final String username) {
-    try {
-      return storage.get(username, HashSet::new);
-    } catch (final ExecutionException e) {
-      logger.error("Registration lookup failed", e);
-      throw new RuntimeException(e);
-    }
-  }
-
-  public Collection<CredentialRegistration> getRegistrationsByUserHandle(final ByteArray userHandle) {
-    return storage.asMap().values().stream()
-        .flatMap(Collection::stream)
-        .filter(
-            credentialRegistration ->
-                userHandle.equals(credentialRegistration.getUserIdentity().getId()))
-        .collect(Collectors.toList());
-  }
-
-  public void updateSignatureCount(final AssertionResult result) {
-    final CredentialRegistration registration =
-        getRegistrationByUsernameAndCredentialId(
-                result.getUsername(), result.getCredential().getCredentialId())
-            .orElseThrow(
-                () ->
-                    new NoSuchElementException(
-                        String.format(
-                            "Credential \"%s\" is not registered to user \"%s\"",
-                            result.getCredential().getCredentialId(), result.getUsername())));
-
-    final Set<CredentialRegistration> regs = storage.getIfPresent(result.getUsername());
-    regs.remove(registration);
-    regs.add(
-        registration.withCredential(
-            registration.getCredential().toBuilder()
-                .signatureCount(result.getSignatureCount())
-                .build()));
-  }
-
-  public Optional<CredentialRegistration> getRegistrationByUsernameAndCredentialId(
-      final String username, final ByteArray id) {
-    try {
-      return storage.get(username, HashSet::new).stream()
-          .filter(credReg -> id.equals(credReg.getCredential().getCredentialId()))
-          .findFirst();
-    } catch (final ExecutionException e) {
-      logger.error("Registration lookup failed", e);
-      throw new RuntimeException(e);
-    }
-  }
-
-  public boolean removeRegistrationByUsername(
-      final String username, final CredentialRegistration credentialRegistration) {
-    try {
-      return storage.get(username, HashSet::new).remove(credentialRegistration);
-    } catch (final ExecutionException e) {
-      logger.error("Failed to remove registration", e);
-      throw new RuntimeException(e);
-    }
-  }
-
-  public boolean removeAllRegistrations(final String username) {
-    storage.invalidate(username);
-    return true;
-  }
-
-  public boolean userExists(final String username) {
-    return !getRegistrationsByUsername(username).isEmpty();
-  }
-}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java
new file mode 100644
index 0000000..f1e1059
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAttestationConveyancePreferenceTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AttestationConveyancePreference;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for AddAttestationConveyancePreference
+ */
+public class AddAttestationConveyancePreferenceTest extends AbstractWebAuthnTest {
+    
+    private AddAttestationConveyancePreference addAction;
+    
+    private WebAuthnRegistrationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();
+        addAction = new AddAttestationConveyancePreference();
+        addAction.setWebAuthnClient(client);
+        addAction.setCredentialRepository(credentialRepo);
+    } 
+
+
+    @Test
+    public void testAddAttestationConveyancePreference_Default() throws ComponentInitializationException {
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAttestationConveyancePreference());
+        assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.NONE);        
+    }
+    
+    @Test
+    public void testAddAttestationConveyancePreference_Direct() throws ComponentInitializationException {
+        addAction.setAttestationConveyancePreference("direct");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAttestationConveyancePreference());
+        assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.DIRECT);        
+    }
+    
+    @Test
+    public void testAddAttestationConveyancePreference_Indirect() throws ComponentInitializationException {
+        addAction.setAttestationConveyancePreference("indirect");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAttestationConveyancePreference());
+        assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.INDIRECT);        
+    }
+    
+    @Test
+    public void testAddAttestationConveyancePreference_Enterprise() throws ComponentInitializationException {
+        addAction.setAttestationConveyancePreference("enterprise");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAttestationConveyancePreference());
+        assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.ENTERPRISE);        
+    }
+    
+    @Test
+    public void testAddAttestationConveyancePreference_None() throws ComponentInitializationException {
+        addAction.setAttestationConveyancePreference("none");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAttestationConveyancePreference());
+        assertEquals(context.getAttestationConveyancePreference(), AttestationConveyancePreference.NONE);        
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddAttestationConveyancePreference_Unknown() throws ComponentInitializationException {
+        addAction.setAttestationConveyancePreference("unknown");
+        addAction.initialize();
+        addAction.execute(src);        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java
new file mode 100644
index 0000000..bc02424
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddAuthenticatorAttachmentRequirementTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.AuthenticatorAttachment;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddAuthenticatorAttachmentRequirement}
+ */
+public class AddAuthenticatorAttachmentRequirementTest extends AbstractWebAuthnTest {
+    
+    private AddAuthenticatorAttachmentRequirement addAction;
+    
+    private WebAuthnRegistrationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();
+        addAction = new AddAuthenticatorAttachmentRequirement();
+        addAction.setWebAuthnClient(client);
+        addAction.setCredentialRepository(credentialRepo);
+    } 
+    
+    @Test
+    public void testAddAuthenticatorAttachmentRequirement_CrossPlatform() throws ComponentInitializationException {
+        addAction.setAuthenticatorAttachmentRequirement("cross-platform");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAuthenticatorAttachmentRequirement());
+        assertEquals(context.getAuthenticatorAttachmentRequirement(), AuthenticatorAttachment.CROSS_PLATFORM);        
+    }
+    
+    @Test
+    public void testAddAuthenticatorAttachmentRequirement_Platform() throws ComponentInitializationException {
+        addAction.setAuthenticatorAttachmentRequirement("platform");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getAuthenticatorAttachmentRequirement());
+        assertEquals(context.getAuthenticatorAttachmentRequirement(), AuthenticatorAttachment.PLATFORM);        
+    }
+    
+    @Test
+    public void testAddAuthenticatorAttachmentRequirement_Default() throws ComponentInitializationException {
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNull(context.getAuthenticatorAttachmentRequirement());        
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddAuthenticatorAttachmentRequirement_Uknown() throws ComponentInitializationException {
+        addAction.setAuthenticatorAttachmentRequirement("unkown");
+        addAction.initialize();
+        addAction.execute(src);      
+    }
+    
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java
new file mode 100644
index 0000000..4e9fcb5
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddResidentKeyRequirementTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.ResidentKeyRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddResidentKeyRequirement}
+ */
+public class AddResidentKeyRequirementTest extends AbstractWebAuthnTest {
+    
+    private AddResidentKeyRequirement addAction;
+    
+    private WebAuthnRegistrationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();
+        addAction = new AddResidentKeyRequirement();
+        addAction.setWebAuthnClient(client);
+        addAction.setCredentialRepository(credentialRepo);
+    } 
+    
+    @Test
+    public void testAddResidentKeyRequirement_Default() throws ComponentInitializationException {
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getResidentKeyRequirement());
+        assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.PREFERRED);        
+    }
+    
+    @Test
+    public void testAddResidentKeyRequirement_Discouraged() throws ComponentInitializationException {
+        addAction.setResidentKeyRequirement("discouraged");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getResidentKeyRequirement());
+        assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.DISCOURAGED);        
+    }
+    
+    @Test
+    public void testAddResidentKeyRequirement_Required() throws ComponentInitializationException {
+        addAction.setResidentKeyRequirement("required");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getResidentKeyRequirement());
+        assertEquals(context.getResidentKeyRequirement(), ResidentKeyRequirement.REQUIRED);        
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddResidentKeyRequirement_Unknown() throws ComponentInitializationException {
+        addAction.setResidentKeyRequirement("unknown");
+        addAction.initialize();
+        addAction.execute(src);        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
new file mode 100644
index 0000000..8fb4ad3
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AddUserIdTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.TreeSet;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddUserId}.
+ */
+public class AddUserIdTest extends AbstractWebAuthnTest {
+    
+    private AddUserId addAction;
+    
+    private WebAuthnRegistrationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnRegistrationContext();
+        addAction = new AddUserId();
+        addAction.setWebAuthnClient(client);
+        addAction.setCredentialRepository(credentialRepo);
+    }
+    
+    @Test
+    public void testNewUserId() throws ComponentInitializationException {
+        addAction.initialize();
+        
+        context.setUsername("jdoe");
+        
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getUserId());
+    }
+    
+    @Test
+    public void testNewUserId_CustomGenerator() throws ComponentInitializationException {
+        addAction.setUserIdGeneratorStrategy(input -> new byte[] {(byte)0xFF});
+        addAction.initialize();
+        
+        context.setUsername("jdoe");
+        
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getUserId());
+        final byte[] userId = context.getUserId();
+        assert userId != null;
+        assertEquals(userId.length,1);
+        assertEquals(userId[0], (byte)0xFF);
+    }
+    
+    @Test
+    public void testNewUserId_ExistingUserHandle() throws Exception {
+        
+        mockAuthenticator = new MockAuthenticator(RPID);
+        
+        final var user = UserIdentity.builder()
+                .name("jdoe")
+                .displayName("John Doe")
+                .id(ByteArray.fromBase64(USER_HANDLE_B64))
+                .build();
+        
+        final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
+        
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation = 
+                mockAuthenticator.createAuthenticatorAttestationResponse(CHALLENGE_B64, clientDataCreate, 
+                        Base64Support.decode(USER_HANDLE_B64));
+        
+        final var barray = ByteArray.fromBase64(USER_HANDLE_B64);
+        assert barray.getBase64().equals(USER_HANDLE_B64);
+        assert Arrays.equals(barray.getBytes(), Base64Support.decode(USER_HANDLE_B64));
+
+        
+        final RegisteredCredential credential = RegisteredCredential.builder()
+                .credentialId(attestation.getId())
+                .userHandle(ByteArray.fromBase64(USER_HANDLE_B64))
+                .publicKeyCose(attestation.getResponse().getParsedAuthenticatorData()
+                        .getAttestedCredentialData().get().getCredentialPublicKey())
+                .build();
+         
+         final var reg = CredentialRegistration.builder()
+                 .withUserIdentity(user)
+                 .withTransports(new TreeSet<AuthenticatorTransport>())
+                 .withRegistrationTime(Instant.now())
+                 .withCredential(credential)
+                 .withAttestationMetadata(CollectionSupport.emptySet())
+                 .withCredentialNickname("nickname")
+                 .withDiscoverable(Optional.of(Boolean.TRUE))
+                 .withUserVerified(true)
+                 .build();
+        
+        credentialRepo.addRegistrationByUsername("jdoe", reg);
+        addAction.initialize();
+        
+        context.setUsername("jdoe");
+        
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(context.getUserId());
+        final byte[] userId = context.getUserId();
+        assert userId != null;
+        assertEquals(userId.length,Base64Support.decode(USER_HANDLE_B64).length);
+        assertEquals(userId, Base64Support.decode(USER_HANDLE_B64));
+    }
+    
+    @Test
+    public void testNewUserId_NullId() throws ComponentInitializationException {
+        addAction.setUserIdGeneratorStrategy(input -> null);
+        addAction.initialize();
+        
+        context.setUsername("jdoe");
+        
+        final Event result = addAction.execute(src);
+        assertNotNull(result);
+        assert result != null;
+        assertEquals(result.getId(), "InvalidRegistration");
+    }
+    
+    @Test
+    public void testNewUserId_NoUsername() throws ComponentInitializationException {
+        addAction.setUserIdGeneratorStrategy(input -> new byte[] {(byte)0xFF});
+        addAction.initialize();
+        
+        context.setUsername(null);
+        
+        final Event result = addAction.execute(src);
+        assertNotNull(result);
+        assert result != null;
+        assertEquals(result.getId(), "InvalidRegistrationContext");
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testNewUserId_MoreThan64Bytes() throws ComponentInitializationException {
+        addAction.setUserIdGeneratorStrategy(input -> {
+            final Random random = new Random();
+            final byte[] byteArray = new byte[65];
+            random.nextBytes(byteArray);
+            return byteArray;
+        });
+        addAction.initialize();        
+        context.setUsername("jdoe");        
+        addAction.execute(src);
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java
new file mode 100644
index 0000000..176c35e
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/ExceptionThrowingMockWebAuthnClient.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
+
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.CredentialCreationOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.CredentialRequestOptionsParameters;
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.RegistrationFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthenticationClientException;
+
+/**
+ * Mock client that just throws exceptions
+ */
+public class ExceptionThrowingMockWebAuthnClient implements WebAuthnAuthenticationClient{
+    
+    @Override
+    public RegistrationResult validateAuthenticatorAttestationResponse(
+            final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions,
+            final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> 
+            authenticatorAttestationResponse)
+            throws RegistrationFailureException {
+        throw new RegistrationFailureException("Unable to validate authenticator attestation response");
+    }
+    
+    @Override
+    public AssertionResult validateAuthenticatorAssertionResponse(final String username, final byte[] userId,
+            final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
+            final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> 
+            authenticatorAssertionResponse)
+            throws AssertionFailureException {
+        throw new AssertionFailureException("Unable to validate authenticator assertion response");
+    }
+    
+    @Override
+    public PublicKeyCredentialCreationOptions createRegistrationRequest(
+            final CredentialCreationOptionsParameters creationOptions) throws WebAuthnAuthenticationClientException {
+        throw new WebAuthnAuthenticationClientException("Unable to create registration request");
+    }
+    
+    @Override
+    public PublicKeyCredentialRequestOptions createAuthenticationRequest(
+            final CredentialRequestOptionsParameters requestParams) throws WebAuthnAuthenticationClientException {
+        throw new WebAuthnAuthenticationClientException("Unable to create authentication request");
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
index e86adf0..b4c0cbc 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
@@ -49,6 +49,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
 import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.collection.CollectionSupport;
 
 /**
  * Tests for {@link YubicoWebAuthnAuthenticationClient}. To some extend this is testing the Yubico libraries work
@@ -187,7 +188,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                 .withTransports(new TreeSet<AuthenticatorTransport>())
                 .withRegistrationTime(Instant.now())
                 .withCredential(credential)
-                .withAttestationMetadata(null)
+                .withAttestationMetadata(CollectionSupport.emptySet())
                 .withCredentialNickname("Nickname")
                 .withDiscoverable(Optional.of(Boolean.TRUE))
                 .withUserVerified(true)
@@ -234,7 +235,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                 .withTransports(new TreeSet<AuthenticatorTransport>())
                 .withRegistrationTime(Instant.now())
                 .withCredential(credential)
-                .withAttestationMetadata(null)
+                .withAttestationMetadata(CollectionSupport.emptySet())
                 .withCredentialNickname("Nickname")
                 .withDiscoverable(Optional.of(Boolean.TRUE))
                 .withUserVerified(true)
@@ -279,7 +280,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                 .withTransports(new TreeSet<AuthenticatorTransport>())
                 .withRegistrationTime(Instant.now())
                 .withCredential(credential)
-                .withAttestationMetadata(null)
+                .withAttestationMetadata(CollectionSupport.emptySet())
                 .withCredentialNickname("Nickname")
                 .withDiscoverable(Optional.of(Boolean.TRUE))
                 .withUserVerified(true)
@@ -325,7 +326,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                 .withTransports(new TreeSet<AuthenticatorTransport>())
                 .withRegistrationTime(Instant.now())
                 .withCredential(credential)
-                .withAttestationMetadata(null)
+                .withAttestationMetadata(CollectionSupport.emptySet())
                 .withCredentialNickname("Nickname")
                 .withDiscoverable(Optional.of(Boolean.TRUE))
                 .withUserVerified(true)
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index 098391a..4bbc26b 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -18,6 +18,7 @@ import java.net.UnknownHostException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 import javax.annotation.Nonnull;
 
@@ -31,14 +32,25 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
 import com.yubico.webauthn.data.PublicKeyCredentialParameters;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+import com.yubico.webauthn.data.UserIdentity;
 
 import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 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.storage.impl.InMemoryRegistrationStorage;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.idp.profile.testing.RequestContextBuilder;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
@@ -79,6 +91,12 @@ public abstract class AbstractWebAuthnTest {
     /** The CBOR friendly json mapper.*/
     protected ObjectMapper jsonMapper;
     
+    /** The WebAuthn Client to test with.*/
+    protected WebAuthnAuthenticationClient client;
+    
+    /** A mocked credential repository to use.*/
+    protected StorageServiceCredentialRepository credentialRepo;
+    
     /** List of acceptable public key algorithms.*/
     @Nonnull protected final List<PublicKeyCredentialParameters> preferredPublickeyParams =
             CollectionSupport.listOf(
@@ -119,8 +137,67 @@ public abstract class AbstractWebAuthnTest {
         
         webAuthnRegContext = new WebAuthnRegistrationContext();
         prc.addSubcontext(webAuthnRegContext);
+        
+        //Move this test to one of the client, this should use a mock and less specific data types
+        final RelyingParty rp = RelyingParty.builder().identity(
+                RelyingPartyIdentity
+                .builder()
+                .id("idp.example.com")
+                .name("Demo IdP as a WebAuthn RP")
+                .build()).credentialRepository(new InMemoryRegistrationStorage())
+            .allowOriginPort(true)
+            .allowOriginSubdomain(true)
+            .build();
+        
+        final UserIdentity identity = 
+                UserIdentity.builder().name("test-user").displayName("test user")
+                .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
+        
+        final PublicKeyCredentialCreationOptions credentialCreationOptions =
+                PublicKeyCredentialCreationOptions.builder()
+                    .rp(rp.getIdentity())
+                    .user(identity)
+                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+                    .pubKeyCredParams(preferredPublickeyParams)
+                    .excludeCredentials(Optional.empty())
+                    .timeout(Optional.empty()).build();
+
+        webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
+        // TODO Should create a mock factory
+        final YubicoWebauthnClientFactory factory = new YubicoWebauthnClientFactory();
+        factory.setPreferredPublickeyParams(preferredPublickeyParams.stream().map(alg -> alg.getAlg().name()).toList());
+        factory.setCredentialRepository(new InMemoryRegistrationStorage());
+        factory.setRelyingPartyId("idp.example.com");
+        factory.setRelyingPartyName("Demo IdP as a WebAuthn RP");
+        factory.initialize();     
+        client = factory.getObject();
+        
+        // The im-memory repo is for testing only
+        credentialRepo = new InMemoryRegistrationStorage();
     }  
     
+    
+    /**
+     *  Add a WebAuthn registration context to the profile request context
+     */
+    protected WebAuthnRegistrationContext addWebAuthnRegistrationContext() {
+        return prc.ensureSubcontext(WebAuthnRegistrationContext.class);        
+    }
+    
+    /**
+     *  Add a Base WebAuthn registration context to the authentication context
+     */
+    protected BaseWebAuthnContext addBaseWebAuthnRegistrationContext() {
+        return prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(BaseWebAuthnContext.class);        
+    }
+    
+    /**
+     *  Add a WebAuthn authentication context to the authentication context
+     */
+    protected WebAuthnAuthenticationContext addWebAuthnAuthenticationContext() {
+        return prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(WebAuthnAuthenticationContext.class);        
+    }
+    
     /**
      * Create a running server that mimics responses from a WebAuthn Metadata Service.
      * Creates a new self-signed certificate.
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java
new file mode 100644
index 0000000..3040ac2
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AddUserVerificationRequirementTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+
+/**
+ * Tests for {@link AddUserVerificationRequirement}
+ */
+public class AddUserVerificationRequirementTest extends AbstractWebAuthnTest {
+    
+    private AddUserVerificationRequirement addAction;
+    
+    private BaseWebAuthnContext baseContext;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        baseContext = addBaseWebAuthnRegistrationContext();
+        addAction = new AddUserVerificationRequirement();
+        addAction.setWebAuthnClient(client);
+        addAction.setCredentialRepository(credentialRepo);
+    } 
+
+
+    @Test
+    public void testAddUserVerificationRequirement_Default() throws ComponentInitializationException {
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(baseContext.getUserVerificationRequirement());
+        assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.PREFERRED);
+        
+    }
+    
+    @Test
+    public void testAddUserVerificationRequirement_Required() throws ComponentInitializationException {
+        addAction.setUserVerificationRequirement("required");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(baseContext.getUserVerificationRequirement());
+        assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.REQUIRED);
+        
+    }
+    
+    @Test
+    public void testAddUserVerificationRequirement_Discouraged() throws ComponentInitializationException {
+        addAction.setUserVerificationRequirement("discouraged");
+        addAction.initialize();
+        final Event result = addAction.execute(src);
+        assertNull(result);
+        assertNotNull(baseContext.getUserVerificationRequirement());
+        assertEquals(baseContext.getUserVerificationRequirement(), UserVerificationRequirement.DISCOURAGED);
+        
+    }
+    
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddUserVerificationRequirement_Unknown() throws ComponentInitializationException {
+        addAction.setUserVerificationRequirement("unknown");
+        addAction.initialize();
+        addAction.execute(src);        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java
new file mode 100644
index 0000000..67e822b
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptionsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.util.Random;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.client.impl.ExceptionThrowingMockWebAuthnClient;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Test for {@link CreatePublicKeyCredentialRequestOptions}
+ */
+public class CreatePublicKeyCredentialRequestOptionsTest extends AbstractWebAuthnTest {
+    
+    private CreatePublicKeyCredentialRequestOptions action;
+    
+    private WebAuthnAuthenticationContext context;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        context = addWebAuthnAuthenticationContext();
+        
+        action = new CreatePublicKeyCredentialRequestOptions();
+        action.setWebAuthnClient(client);
+    } 
+    
+    @Test
+    public void testCreatePublicKeyCredentialRequestOptions() throws ComponentInitializationException {
+        
+        final Random random = new Random();
+        final byte[] challenge = new byte[64];
+        random.nextBytes(challenge);
+        context.setServerChallenge(challenge);
+        context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);
+        
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertNull(event);
+        assertNotNull(context.getPublicKeyCredentialRequestOptions());
+        final var options = context.getPublicKeyCredentialRequestOptions();
+        assert options != null;
+        assertEquals(options.getChallenge().compareTo(new ByteArray(context.getServerChallenge())),0);
+        assertNotNull(options.getRpId());
+        assertNotNull(options.getUserVerification());
+        
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testCreatePublicKeyCredentialRequestOptions_NoChallenge() throws ComponentInitializationException {
+
+        context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);    
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+        
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testCreatePublicKeyCredentialRequestOptions_NoUvRequirement() throws ComponentInitializationException {
+        final Random random = new Random();
+        final byte[] challenge = new byte[64];
+        random.nextBytes(challenge);
+        context.setServerChallenge(challenge);
+
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+        
+    }
+    
+    @SuppressWarnings("null")
+    @Test
+    public void testCreatePublicKeyCredentialRequestOptions_WebAuthnException() throws ComponentInitializationException {
+        final Random random = new Random();
+        final byte[] challenge = new byte[64];
+        random.nextBytes(challenge);
+        context.setServerChallenge(challenge);
+        context.setUserVerificationRequirement(UserVerificationRequirement.DISCOURAGED);
+        
+        action.setWebAuthnClient(new ExceptionThrowingMockWebAuthnClient());
+
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assertEquals(event.getId(),AuthnEventIds.AUTHN_EXCEPTION);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
index 3770d93..81f51b5 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
@@ -14,22 +14,10 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
-import java.util.Optional;
-
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.yubico.webauthn.RelyingParty;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
-import com.yubico.webauthn.data.RelyingPartyIdentity;
-import com.yubico.webauthn.data.UserIdentity;
-
 import net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse;
-import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
-import net.shibboleth.shared.codec.Base64Support;
 
 
 /**
@@ -47,41 +35,9 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
     @BeforeMethod
     public void setup() throws Exception {
         super.setup();
-        validator = new ValidateAuthenticatorAttestationResponse();
-        //Move this test to one of the client, this should use a mock and less specific data types
-        final RelyingParty rp = RelyingParty.builder().identity(
-                RelyingPartyIdentity
-                .builder()
-                .id("idp.example.com")
-                .name("Demo IdP as a WebAuthn RP")
-                .build()).credentialRepository(new InMemoryRegistrationStorage())
-            .allowOriginPort(true)
-            .allowOriginSubdomain(true)
-            .build();
-        
-        final UserIdentity identity = 
-                UserIdentity.builder().name("test-user").displayName("test user")
-                .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
-        
-        final PublicKeyCredentialCreationOptions credentialCreationOptions =
-                PublicKeyCredentialCreationOptions.builder()
-                    .rp(rp.getIdentity())
-                    .user(identity)
-                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
-                    .pubKeyCredParams(preferredPublickeyParams)
-                    .excludeCredentials(Optional.empty())
-                    .timeout(Optional.empty()).build();
-
-        webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
-        final YubicoWebauthnClientFactory factory = new YubicoWebauthnClientFactory();
-        factory.setPreferredPublickeyParams(preferredPublickeyParams.stream().map(alg -> alg.getAlg().name()).toList());
-        factory.setCredentialRepository(new InMemoryRegistrationStorage());
-        factory.setRelyingPartyId("idp.example.com");
-        factory.setRelyingPartyName("Demo IdP as a WebAuthn RP");
-        factory.initialize();     
-        final WebAuthnAuthenticationClient client = factory.getObject();
+        validator = new ValidateAuthenticatorAttestationResponse();        
         validator.setWebAuthnClient(client);
-        validator.setCredentialRepository(new InMemoryRegistrationStorage());
+        validator.setCredentialRepository(credentialRepo);
         validator.initialize();
     }
     
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
index 37403cf..123bb02 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistrationSerializerTest.java
@@ -62,7 +62,7 @@ public class CredentialRegistrationSerializerTest extends AbstractWebAuthnTest {
         user = UserIdentity.builder()
                 .name("jdoe")
                 .displayName("John Doe")
-                .id(new ByteArray("userhandle".getBytes()))
+                .id(ByteArray.fromBase64(USER_HANDLE_B64))
                 .build();
         
         final Map<String, String> clientDataCreate = createClientData("webauthn.create", ORIGIN, CHALLENGE_B64);
@@ -83,7 +83,7 @@ public class CredentialRegistrationSerializerTest extends AbstractWebAuthnTest {
                  .withTransports(new TreeSet<AuthenticatorTransport>())
                  .withRegistrationTime(Instant.now())
                  .withCredential(credential)
-                 .withAttestationMetadata(null)
+                 .withAttestationMetadata(CollectionSupport.emptySet())
                  .withCredentialNickname("nickname")
                  .withDiscoverable(Optional.of(Boolean.TRUE))
                  .withUserVerified(true)
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 abdf228..34746ed 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
@@ -45,6 +45,7 @@ import com.yubico.webauthn.data.UserIdentity;
 import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
 import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.shared.collection.CollectionSupport;
 
 /**
  * Tests for {@link IdPStorageServiceCredentialRespository}.
@@ -103,7 +104,7 @@ public class IdPStorageServiceCredentialRespositoryTest extends AbstractWebAuthn
                 .withTransports(new TreeSet<AuthenticatorTransport>())
                 .withRegistrationTime(Instant.now())
                 .withCredential(credential)
-                .withAttestationMetadata(null)
+                .withAttestationMetadata(CollectionSupport.emptySet())
                 .withCredentialNickname("nickname")
                 .withDiscoverable(Optional.of(Boolean.TRUE))
                 .withUserVerified(true)
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
new file mode 100644
index 0000000..31242ff
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -0,0 +1,170 @@
+// Copyright (c) 2018, Yubico AB
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
+
+/**
+ * In memory credential repository to use for testing.
+ */
+public class InMemoryRegistrationStorage implements StorageServiceCredentialRepository {
+
+    private final Cache<String, Set<CredentialRegistration>> storage = CacheBuilder.newBuilder().maximumSize(1000)
+            .expireAfterAccess(1, TimeUnit.DAYS).build();
+
+    private static final Logger logger = LoggerFactory.getLogger(InMemoryRegistrationStorage.class);
+
+    @Override
+    public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
+        return getRegistrationsByUsername(username).stream()
+                .map(registration -> PublicKeyCredentialDescriptor.builder()
+                        .id(registration.getCredential().getCredentialId()).transports(registration.getTransports())
+                        .build())
+                .collect(Collectors.toSet());
+    }
+
+    @Override
+    public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
+        return getRegistrationsByUserHandle(userHandle).stream().findAny().map(CredentialRegistration::getUsername);
+    }
+
+    @Override
+    public Optional<ByteArray> getUserHandleForUsername(final String username) {
+        return getRegistrationsByUsername(username).stream().findAny().map(reg -> reg.getUserIdentity().getId());
+    }
+
+    @Override
+    public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
+        final Optional<CredentialRegistration> registrationMaybe = storage.asMap().values().stream()
+                .flatMap(Collection::stream)
+                .filter(credReg -> credentialId.equals(credReg.getCredential().getCredentialId())).findAny();
+
+        logger.debug("lookup credential ID: {}, user handle: {}; result: {}", credentialId, userHandle,
+                registrationMaybe);
+        return registrationMaybe.map(registration -> RegisteredCredential.builder()
+                .credentialId(registration.getCredential().getCredentialId())
+                .userHandle(registration.getUserIdentity().getId())
+                .publicKeyCose(registration.getCredential().getPublicKeyCose())
+                .signatureCount(registration.getCredential().getSignatureCount()).build());
+    }
+
+    @Override
+    public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
+        return Collections.unmodifiableSet(storage.asMap().values().stream().flatMap(Collection::stream)
+                .filter(reg -> reg.getCredential().getCredentialId().equals(credentialId))
+                .map(reg -> RegisteredCredential.builder().credentialId(reg.getCredential().getCredentialId())
+                        .userHandle(reg.getUserIdentity().getId()).publicKeyCose(reg.getCredential().getPublicKeyCose())
+                        .signatureCount(reg.getCredential().getSignatureCount()).build())
+                .collect(Collectors.toSet()));
+    }
+
+    public boolean addRegistrationByUsername(final String username, final CredentialRegistration reg) {
+        try {
+            return storage.get(username, HashSet::new).add(reg);
+        } catch (final ExecutionException e) {
+            logger.error("Failed to add registration", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Set<CredentialRegistration> getRegistrationsByUsername(final String username) {
+        try {
+            return storage.get(username, HashSet::new);
+        } catch (final ExecutionException e) {
+            logger.error("Registration lookup failed", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Collection<CredentialRegistration> getRegistrationsByUserHandle(final ByteArray userHandle) {
+        return storage.asMap().values().stream().flatMap(Collection::stream)
+                .filter(credentialRegistration -> userHandle.equals(credentialRegistration.getUserIdentity().getId()))
+                .collect(Collectors.toList());
+    }
+
+    public void updateSignatureCount(final AssertionResult result) {
+        final CredentialRegistration registration = getRegistrationByUsernameAndCredentialId(result.getUsername(),
+                result.getCredential().getCredentialId())
+                        .orElseThrow(() -> new NoSuchElementException(
+                                String.format("Credential \"%s\" is not registered to user \"%s\"",
+                                        result.getCredential().getCredentialId(), result.getUsername())));
+
+        final Set<CredentialRegistration> regs = storage.getIfPresent(result.getUsername());
+        regs.remove(registration);
+        regs.add(registration.withCredential(
+                registration.getCredential().toBuilder().signatureCount(result.getSignatureCount()).build()));
+    }
+
+    public Optional<CredentialRegistration> getRegistrationByUsernameAndCredentialId(final String username,
+            final ByteArray id) {
+        try {
+            return storage.get(username, HashSet::new).stream()
+                    .filter(credReg -> id.equals(credReg.getCredential().getCredentialId())).findFirst();
+        } catch (final ExecutionException e) {
+            logger.error("Registration lookup failed", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean removeRegistrationByUsername(final String username,
+            final CredentialRegistration credentialRegistration) {
+        try {
+            return storage.get(username, HashSet::new).remove(credentialRegistration);
+        } catch (final ExecutionException e) {
+            logger.error("Failed to remove registration", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    public boolean removeAllRegistrations(final String username) {
+        storage.invalidate(username);
+        return true;
+    }
+
+    public boolean userExists(final String username) {
+        return !getRegistrationsByUsername(username).isEmpty();
+    }
+}

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


More information about the commits mailing list