[java-idp-plugin-webauthn] branch main updated: Add basic public key credential validation using Yubico client

Phil Smart philip.smart at jisc.ac.uk
Thu Nov 16 17:47:28 UTC 2023


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=85fd85542052626643aab715ecab22334f4473ad

The following commit(s) were added to refs/heads/main by this push:
     new 85fd855  Add basic public key credential validation using Yubico client
85fd855 is described below

commit 85fd85542052626643aab715ecab22334f4473ad
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Nov 16 17:47:21 2023 +0000

    Add basic public key credential validation using Yubico client
---
 .../AbstractWebAuthnAuthenticationAction.java      |  35 ++
 .../plugin/authn/webauthn/PublicKeyCredential.java |   9 +-
 .../webauthn/WebAuthnAuthenticationClient.java     |  18 +-
 .../context/WebAuthnAuthenticationContext.java     |  27 +-
 webauthn-impl/pom.xml                              |  11 +-
 .../impl/YubicoWebauthnAuthenticationClient.java   |  27 +-
 .../client/impl/YubicoWebauthnClientFactory.java   |   5 +-
 .../CreatePublicKeyCredentialCreationOptions.java  |   2 +-
 .../CreatePublicKeyCredentialRequestOptions.java   |   2 +-
 .../PopulateWebauthnAuthenticationContext.java     |  26 --
 .../webauthn/impl/ValidatePublicKeyCredential.java |  43 ++-
 .../storage/impl/CredentialRegistration.java       |  74 +++++
 .../storage/impl/DummyCredentialRepository.java    |  66 ----
 .../storage/impl/InMemoryRegistrationStorage.java  | 207 ++++++++++++
 .../YubicoWebauthnAuthenticationClientTest.java    | 145 +++++++++
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  | 133 ++++++++
 .../authn/webauthn/impl/MockAuthenticator.java     | 357 +++++++++++++++++++++
 .../impl/ValidatePublicKeyCredentialTest.java      | 115 +++++++
 18 files changed, 1170 insertions(+), 132 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
index 3d8a1c3..a9448cc 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
@@ -32,6 +32,7 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 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;
 
@@ -59,6 +60,30 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
     
     /** The Duo authentication Context.*/
     @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
+    
+    /** The WebAuthn client to use.*/
+    @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+    
+    
+    /**
+     * Set the WebAuthn client used to handle registration and authentication ceremonies.
+     * 
+     * @param webAuthnClient 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.*/
@@ -82,6 +107,16 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
                 Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
     }
     
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (webAuthnClient == null) {
+            throw new ComponentInitializationException("WebAuthn Client can not be null");
+        }
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
index 0bd7cbb..4f42dba 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 @ThreadSafe
 @JsonDeserialize(builder=PublicKeyCredential.Builder.class)
 @JsonIgnoreProperties(ignoreUnknown = true)
+ at Deprecated
 public final class PublicKeyCredential {
     
     @Nonnull private final String type;
@@ -23,7 +24,7 @@ public final class PublicKeyCredential {
     //no clientExtensionResults?
 
     
-    private PublicKeyCredential(Builder builder) {
+    private PublicKeyCredential(final Builder builder) {
         this.type = builder.type;
         this.id = builder.id;
         this.response = builder.response;
@@ -53,17 +54,17 @@ public final class PublicKeyCredential {
         private Builder() {
         }
 
-        public Builder withType(String type) {
+        public Builder withType(final String type) {
             this.type = type;
             return this;
         }
 
-        public Builder withId(String id) {
+        public Builder withId(final String id) {
             this.id = id;
             return this;
         }
 
-        public Builder withResponse(Response response) {
+        public Builder withResponse(final Response response) {
             this.response = response;
             return this;
         }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java
index 345243e..f4e5ff1 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java
@@ -3,6 +3,10 @@ package net.shibboleth.idp.plugin.authn.webauthn;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
 /**
  * A client that manages the entire webauthn authentication and registration ceremony.
  * 
@@ -48,6 +52,18 @@ public interface WebAuthnAuthenticationClient {
       * 
       * @return true if the assertion was verified successfully, false otherwise.
       */
-     boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse);       
+     boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse);
+
+
+    /**
+     * Validate a registration request.
+     * 
+     * @param request
+     * @param response
+     * @return 
+     */
+     //TODO use other validation method?
+     boolean validateRegistration(String request,
+            PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> response);       
 
 }
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 c976eb8..9eb0209 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
@@ -11,7 +11,6 @@ import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 
-import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.logic.Constraint;
@@ -42,16 +41,14 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     /** In a new context? TODO. The existing credential public key encoded in COSE_Key format if found.*/
     @Nullable private byte[] existingPublicKey;
     
+    //TODO would need to be bytes if Yubico agnostic (if we wanted that)
     /** An assertion response that is the result of an authentication.*/
     @Nullable 
     private PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>  assertionResponse;
     
     /** The public key credential creation options for registration.*/ 
     @Nullable private String publicKeyCredentialCreationOptions;
-    
-    /** The WebAuthn client to use.*/
-    //TODO is the context the correct place for this type of client
-    @Nullable private WebAuthnAuthenticationClient webAuthnClient;
+   
     
     /**
      * Set the server challenge which the client authenticator needs to sign.
@@ -195,6 +192,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
         return publicKeyCredential;
     }
     
+    // From creation of a credential
     public void setPublicKeyCredential(
             @Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
         publicKeyCredential = Constraint.isNotNull(credential, "Public Key Credential can not be null or empty");
@@ -205,24 +203,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     @Nullable public String getServerChallengeBase64() throws EncodingException {
         return Base64Support.encode(serverChallenge, false);
     }
-    
-    /**
-     * Set the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @param webAuthnClient The webauthnClient to set.
-     */
-    public void setWebAuthnClient(@Nullable final WebAuthnAuthenticationClient client) {
-        webAuthnClient = client;
-    }
-    
-    /**
-     * Get the WebAuthn client used to handle registration and authentication ceremonies.
-     * 
-     * @return the webAuthnClient.
-     */
-    @Nullable public WebAuthnAuthenticationClient getWebAuthnClient() {
-        return webAuthnClient;
-    }
+
     
     /**
      * Set the options used to create public key credentials. 
diff --git a/webauthn-impl/pom.xml b/webauthn-impl/pom.xml
index e9375cd..05f633b 100644
--- a/webauthn-impl/pom.xml
+++ b/webauthn-impl/pom.xml
@@ -31,7 +31,8 @@
             <artifactId>webauthn-server-core</artifactId>
             <scope>compile</scope>
         </dependency>
-        <dependency> <!-- note when we move to more yubico libs this will need to be runtime -->
+        <dependency> <!-- note when we move to more yubico libs this will need to
+            be runtime -->
             <groupId>com.upokecenter</groupId>
             <artifactId>cbor</artifactId>
             <scope>compile</scope>
@@ -41,7 +42,7 @@
             <groupId>com.fasterxml.jackson.dataformat</groupId>
             <artifactId>jackson-dataformat-cbor</artifactId>
             <scope>runtime</scope>
-        </dependency>        
+        </dependency>
         <!-- Service API and Plugin Description dependencies -->
         <dependency>
             <groupId>${idp.groupId}</groupId>
@@ -84,6 +85,12 @@
             <artifactId>jackson-datatype-jsr310</artifactId>
             <scope>provided</scope>
         </dependency>
+        <!-- TODO we should not need this for the cache long term if we switch out the storage-->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- Test dependencies -->
         <dependency>
             <groupId>${idp.groupId}</groupId>
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
index 3c41103..beaa84c 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
@@ -28,13 +28,18 @@ import org.slf4j.Logger;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.FinishRegistrationOptions;
 import com.yubico.webauthn.RelyingParty;
 import com.yubico.webauthn.StartAssertionOptions;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
 import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
 import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
 import com.yubico.webauthn.data.PublicKeyCredentialParameters;
 import com.yubico.webauthn.data.UserIdentity;
 import com.yubico.webauthn.data.UserVerificationRequirement;
+import com.yubico.webauthn.exception.RegistrationFailedException;
 
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.shared.logic.Constraint;
@@ -147,7 +152,7 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
 
     @Override
     public boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse) {
-        
+       
         if (jsonAssertionResponse == null || jsonAssertionResponse.isEmpty()) {
             log.warn("JSON Assertion Response is either null or empty, authentication can not be validated");
             return false;
@@ -155,6 +160,26 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
         return true;
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public boolean validateRegistration(final String request,
+            final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> response) {
+        
+        try {
+            final PublicKeyCredentialCreationOptions requestOptions = 
+                    PublicKeyCredentialCreationOptions.fromJson(request);
+            rp.finishRegistration(FinishRegistrationOptions.builder()
+                    .request(requestOptions)
+                    .response(response)
+                    .build());
+        } catch (final RegistrationFailedException | JsonProcessingException e) {
+           log.error("Public key credential can not be registered", e);
+           // Should throw this?
+           return false;
+        }
+        return true;
+    }
+
 
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
index b50a7b3..3481d49 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
@@ -26,7 +26,7 @@ import com.yubico.webauthn.RelyingParty;
 import com.yubico.webauthn.data.RelyingPartyIdentity;
 
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.DummyCredentialRepository;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
 import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
@@ -83,7 +83,8 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
                 .builder()
                 .id(getRelyingPartyId())
                 .name(getRelyingPartyName())
-                .build()).credentialRepository(new DummyCredentialRepository())
+                //Use inmemory for now
+                .build()).credentialRepository(new InMemoryRegistrationStorage())
         .allowOriginPort(isAllowOriginPort())
         .allowOriginSubdomain(isAllowOriginSubdomain())
         .build();
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
index 03bffdc..8bec478 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -44,7 +44,7 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAu
             @Nonnull final AuthenticationContext authenticationContext, 
             @Nonnull final WebAuthnAuthenticationContext context) {
         
-        final WebAuthnAuthenticationClient client = context.getWebAuthnClient();
+        final WebAuthnAuthenticationClient client = getWebAuthnClient();
         if (client == null) {
             log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
index 749f756..1068f35 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -41,7 +41,7 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
             @Nonnull final AuthenticationContext authenticationContext, 
             @Nonnull final WebAuthnAuthenticationContext context) {
         
-        final WebAuthnAuthenticationClient client = context.getWebAuthnClient();
+        final WebAuthnAuthenticationClient client = getWebAuthnClient();
         if (client == null) {
             log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
index b7eed7b..9bccc42 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
@@ -28,11 +28,8 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.authn.AbstractAuthenticationAction;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -62,9 +59,6 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
     /** Is the username required?*/
     private Predicate<ProfileRequestContext> usernameRequiredPredicate;
     
-    /** The Webauthn client.*/
-    @NonnullAfterInit private WebAuthnAuthenticationClient webauthnClient;
-    
 
     /** Constructor.*/
     public PopulateWebauthnAuthenticationContext() {
@@ -76,26 +70,7 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
         usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
         usernameRequiredPredicate = PredicateSupport.alwaysTrue();
     }
-    
-    @Override protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-
-        if (webauthnClient ==  null) {
-            throw new ComponentInitializationException("Webauthn client cannot be null");
-        }
 
-    }
-    
-    /**
-     * Set the WebAuthn client to use.
-     * 
-     * @param client the client.
-     */
-    public void setWebauthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
-        checkSetterPreconditions();        
-        webauthnClient = Constraint.isNotNull(client, "WebAuthn Client can not be null");
-    }
-    
     /**
      * @param flag The usernameRequired to set.
      */
@@ -144,7 +119,6 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
             return;
         }
         context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
-        context.setWebAuthnClient(webauthnClient);
 
         log.debug("Created Webauthn authentication context");
     }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
index 7c86f77..0b7258e 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
@@ -32,7 +32,10 @@ import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
 
 /**
  * Validate the public key registration attempt.
@@ -42,22 +45,52 @@ public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationA
     /** Class logger. */
     @Nonnull
     private final Logger log = LoggerFactory.getLogger(ValidatePublicKeyCredential.class);
+    
+    /** The stashed public key credential creation options used to create a new credential.*/
+    @NonnullBeforeExec @NotEmpty private String pkCredCreationOptions;
+    
+    /** The stashed authenticator response.*/
+    @NonnullBeforeExec 
+    private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkCred;
 
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext, 
+            @Nonnull final WebAuthnAuthenticationContext context) {
+       
+        pkCred = context.getPublicKeyCredential();    
+        if (pkCred == null) {
+            log.error("{} public key credential was null", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return false;
+        }
+        
+        pkCredCreationOptions = context.getPublicKeyCredentialCreationOptions();
+        if (StringSupport.trimOrNull(pkCredCreationOptions) == null) {
+            log.error("{} public key credential creation options was null", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return false;
+        }
+        
+        return super.doPreExecute(profileRequestContext, authenticationContext, context);
+    }
 
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext,
             @Nonnull final WebAuthnAuthenticationContext context) {
 
-        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkCred =
-                context.getPublicKeyCredential();
+        // TODO this should throw the error?
+        final boolean publicKeyCredentialIsValid = 
+                getWebAuthnClient().validateRegistration(context.getPublicKeyCredentialCreationOptions(), pkCred);
         
-        if (pkCred == null) {
-            log.error("{} public key credential was null", getLogPrefix());
+        if (!publicKeyCredentialIsValid) {            
+            log.error("{} public key credential creation options was invalid", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             return;
         }
-        // Validate the registration
         
         // If valid. Add back to context
         log.info("Public Key Registration was valid");
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java
new file mode 100644
index 0000000..09c0baf
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialRegistration.java
@@ -0,0 +1,74 @@
+/*
+ * 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.storage.impl;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.SortedSet;
+
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.UserIdentity;
+
+/**
+ * Influenced by the CredentialRegistration class in the Yubico demo libraries.
+ */
+public class CredentialRegistration {
+
+    UserIdentity userIdentity;
+    Optional<String> credentialNickname;
+    SortedSet<AuthenticatorTransport> transports;
+
+    Instant registrationTime;
+    RegisteredCredential credential;
+
+    Optional<Object> attestationMetadata;
+
+    public String getRegistrationTimestamp() {
+        return registrationTime.toString();
+    }
+
+    public String getUsername() {
+        return userIdentity.getName();
+    }
+
+    public UserIdentity getUserIdentity() {
+        return userIdentity;
+    }
+
+    public RegisteredCredential getCredential() {
+        return credential;
+    }
+    
+    public SortedSet<AuthenticatorTransport> getTransports() {
+        return transports;
+    }
+    
+    public CredentialRegistration withCredential(final RegisteredCredential newRegCred) {
+        final CredentialRegistration newReg = new CredentialRegistration();
+        newReg.attestationMetadata = attestationMetadata;
+        newReg.userIdentity = userIdentity;
+        newReg.registrationTime = registrationTime;
+        newReg.transports = transports;
+        newReg.credentialNickname = credentialNickname;
+        // With the new credential
+        newReg.credential = newRegCred;
+        return newReg;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java
deleted file mode 100644
index 9b60af7..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.storage.impl;
-
-import java.util.Optional;
-import java.util.Set;
-
-import com.yubico.webauthn.CredentialRepository;
-import com.yubico.webauthn.RegisteredCredential;
-import com.yubico.webauthn.data.ByteArray;
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
-
-/** A dummy CredentialRepository for testing.*/
-public class DummyCredentialRepository implements CredentialRepository{
-    
-    @Override
-    public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
-        return Set.of(RegisteredCredential.builder()
-                .credentialId(new ByteArray("credid".getBytes()))
-                .userHandle(new ByteArray("userhandle".getBytes()))
-                .publicKeyCose(new ByteArray("publickeycose".getBytes()))
-                .signatureCount(0)
-                .build());
-    }
-    
-    @Override
-    public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
-            return Optional.of(RegisteredCredential.builder()
-                    .credentialId(new ByteArray("credid".getBytes()))
-                    .userHandle(new ByteArray("userhandle".getBytes()))
-                    .publicKeyCose(new ByteArray("publickeycose".getBytes()))
-                    .signatureCount(0)
-                    .build());
-    }
-    
-    @Override
-    public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
-        return Optional.of("jdoe");
-    }
-    
-    @Override
-    public Optional<ByteArray> getUserHandleForUsername(final String username) {
-        return Optional.of(new ByteArray("credid".getBytes()));
-    }
-    
-    @Override
-    public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
-        return Set.of(
-                PublicKeyCredentialDescriptor.builder()
-                .id(new ByteArray("credid".getBytes()))
-                .build());
-    }
-
-}
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
new file mode 100644
index 0000000..aa58bec
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/InMemoryRegistrationStorage.java
@@ -0,0 +1,207 @@
+// 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.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+
+public class InMemoryRegistrationStorage implements CredentialRepository {
+
+  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 Collection<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/client/impl/YubicoWebauthnAuthenticationClientTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
new file mode 100644
index 0000000..c05bbda
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClientTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.impl.AbstractWebAuthnTest;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.shared.codec.Base64Support;
+
+/**
+ *
+ */
+public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest{
+    
+    private YubicoWebauthnAuthenticationClient client;
+    
+    private final static String CHALLENGE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+    
+    private final static String USER_HANDLE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+    
+    private String credentialCreationOptions;
+    
+    private final static String ORIGIN = "https://idp.example.com";
+    
+    private final static String RPID = "idp.example.com";
+
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        
+        final RelyingParty rp = RelyingParty.builder().identity(
+                RelyingPartyIdentity
+                .builder()
+                .id(RPID)
+                .name("Demo IdP as a WebAuthn RP")
+                .build()).credentialRepository(new InMemoryRegistrationStorage())
+            .allowOriginPort(true)
+            .allowOriginSubdomain(true)
+            .build();
+        client = new YubicoWebauthnAuthenticationClient(rp,jsonMapper);
+        
+        final UserIdentity identity = 
+                UserIdentity.builder().name("test-user").displayName("test user")
+                .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
+        
+        final PublicKeyCredentialCreationOptions options =
+                PublicKeyCredentialCreationOptions.builder()
+                    .rp(rp.getIdentity())
+                    .user(identity)
+                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+                    .pubKeyCredParams(preferredPublickeyParams)
+                    .excludeCredentials(Optional.empty())
+                    .timeout(Optional.empty()).build();
+        credentialCreationOptions = 
+                jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
+        
+    }
+    
+
+    @Test
+    public void testSuccesss() throws Exception {   
+        
+        mockAuthenticator = new MockAuthenticator(ORIGIN, RPID);
+        
+        final Map<String, String> clientDataJson = 
+                mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64); 
+        final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+    
+        final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+        
+        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, 
+                createPublicKeyCredential(attestationObject, clientDataJsonString));
+        
+        assertTrue(registrationIsValid);
+
+    }
+    
+    @Test
+    public void testFail_BadRpId() throws Exception {   
+        
+        mockAuthenticator = new MockAuthenticator(ORIGIN, "wrong-rpid.example.com");
+        
+        final Map<String, String> clientDataJson = 
+                mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64); 
+        final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+    
+        final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+        
+        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, 
+                createPublicKeyCredential(attestationObject, clientDataJsonString));
+        
+        assertFalse(registrationIsValid);
+    }
+    
+    @Test
+    public void testFail_BadOrigin() throws Exception {   
+        
+        mockAuthenticator = new MockAuthenticator("wrong-origin", RPID);
+        
+        final Map<String, String> clientDataJson = 
+                mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64); 
+        final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+    
+        final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+        
+        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, 
+                createPublicKeyCredential(attestationObject, clientDataJsonString));
+        
+        assertFalse(registrationIsValid);
+    }
+    
+    // Needs to do crypto checks
+
+}
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
new file mode 100644
index 0000000..280feeb
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.assertNotNull;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+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.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+import com.yubico.webauthn.data.PublicKeyCredentialParameters;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+
+/** Abstract class for tests that require context setup.*/
+public abstract class AbstractWebAuthnTest {
+    
+    /** The profile request context to use.*/
+    protected ProfileRequestContext prc;
+    
+    /** The request context to use.*/
+    protected RequestContext src;
+    
+    /** The webauthn authentication context.*/
+    protected WebAuthnAuthenticationContext webAuthnContext;
+    
+    /** The authentication context.*/
+    protected AuthenticationContext ac;
+    
+    /** A mock authenticator to use for creating Authenticator Attestations etc.*/
+    protected MockAuthenticator mockAuthenticator;
+    
+    /** The CBOR friendly json mapper.*/
+    protected ObjectMapper jsonMapper;
+    
+    /** List of acceptable public key algorithms.*/
+    protected final List<PublicKeyCredentialParameters> preferredPublickeyParams =
+            Collections.unmodifiableList(
+                Arrays.asList(
+                    PublicKeyCredentialParameters.ES256,
+                    PublicKeyCredentialParameters.EdDSA,
+                    PublicKeyCredentialParameters.ES384,
+                    PublicKeyCredentialParameters.ES512,
+                    PublicKeyCredentialParameters.RS256,
+                    PublicKeyCredentialParameters.RS384,
+                    PublicKeyCredentialParameters.RS512));
+    
+    
+    /** 
+     * Setup the various contexts.
+     * 
+     * @throws Exception on error
+     */
+    public void setup() throws Exception {
+        jsonMapper = JsonMapper.builder()
+                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
+                .serializationInclusion(Include.NON_ABSENT)
+                .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+                .addModule(new Jdk8Module())
+                .addModule(new JavaTimeModule())
+                .build();
+        
+        
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);    
+        webAuthnContext = new WebAuthnAuthenticationContext();
+        assert null != prc;
+        ac = new AuthenticationContext();
+        ac.setAuthenticatingAuthority("https://idp.example.com");       
+        assert null != ac;
+        prc.addSubcontext(ac);
+        assert null != webAuthnContext;
+        ac.addSubcontext(webAuthnContext);
+    }  
+    
+    
+    @Nonnull protected PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> 
+        createPublicKeyCredential(final Attestation attestationObject, final String clientDataJsonString) 
+                throws Exception{
+        
+        final var response = AuthenticatorAttestationResponse.builder()
+                .attestationObject(new ByteArray(attestationObject.getAttestationObjectCose()))
+                .clientDataJSON(new ByteArray(clientDataJsonString.getBytes()))
+                .build();
+        
+        final ClientRegistrationExtensionOutputs extOutputs =ClientRegistrationExtensionOutputs.builder().build();
+        final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential = 
+                PublicKeyCredential.<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>builder()
+                .id(new ByteArray(attestationObject.getCredentialIdBytes()))
+                .response(response)
+                .clientExtensionResults(extOutputs)
+                .build();
+        assertNotNull(credential);
+        assert credential != null;
+        return credential;
+    }
+     
+    
+
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
new file mode 100644
index 0000000..86e38ab
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/MockAuthenticator.java
@@ -0,0 +1,357 @@
+/*
+ * 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 java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
+import com.yubico.webauthn.data.AuthenticatorData;
+import com.yubico.webauthn.data.ByteArray;
+
+import COSE.AlgorithmID;
+import COSE.OneKey;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Mock Authenticator, to create public key credentials and attestations for a given (fixed) relying party.
+ * 
+ * <p> Does not support the attestation statement 'attStmt' (which provides data provenance information for the public 
+ * key of the attesting authority and authenticator).</p>
+ */
+public class MockAuthenticator {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(MockAuthenticator.class);
+    
+    /** The attestation statement format, default is 'none' as that is the only currently supported. */
+    @Nonnull @NotEmpty private final String fmt;
+    
+    /** 
+     * The relying party identifier. A valid domain string. The origin's effective domain 
+     * (the host's domain name, no scheme and no port).
+     */
+    @Nonnull @NotEmpty private final String rpId;
+    
+    /** The full RP Origin.*/
+    @Nonnull @NotEmpty private final String origin;
+    
+    /** CBOR Mapper.*/
+    @Nonnull private final ObjectMapper cborMapper;
+    
+    
+    /**
+     * 
+     * Constructor.
+     * 
+     * <p>Creates a new public/private key pair per instantiation.</p>
+     *
+     *  @param the relying party's origin.
+     * @param relyingPartyId the relying party identifier, a valid domain string. The origin's effective domain 
+     *              (the host's domain name, no scheme and no port).
+     * @throws Exception on error creating this authenticator.
+     */
+    public MockAuthenticator(@Nonnull @NotEmpty final String rpOrigin, 
+            @Nonnull @NotEmpty final String relyingPartyId) throws Exception {
+        rpId = Constraint.isNotEmpty(relyingPartyId, "relyingPartyId can not be null");
+        origin = Constraint.isNotEmpty(rpOrigin, "rpOrigin can not be null");;
+        fmt = "none";
+        cborMapper = new ObjectMapper(new CBORFactory()).setBase64Variant(Base64Variants.MODIFIED_FOR_URL);
+        
+    }
+    
+    /**
+     * Create a WebAuthn public key credential attestation object. A new attestation is created each time.
+     *  
+     * @throws Exception on error.
+     */
+    public Attestation createCredentialAttestationObject() throws Exception {
+        
+        final OneKey createdKey = OneKey.generateKey(AlgorithmID.ECDSA_256);
+        assert createdKey != null;
+        
+        final String aaguidHex = generateRandomIdentifierHex(32);
+        final String credentialIdHex = generateRandomIdentifierHex(32);
+        
+        final byte[] attestationObject = createAttestationObject(createdKey, aaguidHex, credentialIdHex);
+        return new Attestation(createdKey, aaguidHex, credentialIdHex, attestationObject);
+        
+    }
+    
+    
+    /**
+     * Create a client data JSON object as a Java Map.
+     * 
+     * @param challenge the challenge
+     * @param type the type
+     * @return the client JSON map.
+     */
+    public Map<String, String> createClientDataJson(@Nonnull @NotEmpty final String type, 
+            @Nonnull @NotEmpty final String challenge){
+        final HashMap<String, String> obj = new HashMap<>();
+        obj.put("challenge",challenge);
+        obj.put("origin", origin);
+        obj.put("type", type);
+        return obj;        
+    }
+
+    
+    /**
+     * Create the CBOR encoded version of the JSON attestation object. 
+     * See https://www.w3.org/TR/webauthn-2/#sctn-attestation.
+     * 
+     * 
+     * @param createdKey the key to use as the basis of this attestation object (and the new key to register).
+     * @param credentialId the credential identifier
+     * @param aaguidHex the Authenticator Attestation GUID
+     * 
+     * @return the attestation object CBOR encoded.
+     * 
+     * @throws Exception on error
+     */
+    private byte[] createAttestationObject(final OneKey createdKey, final String aaguidHex, 
+            final String credentialId) throws Exception{
+        final HashMap<String, Object> attObj = new HashMap<>();
+        attObj.put("authData", createAuthData(createdKey, aaguidHex, credentialId).getBytes().getBytes());
+        attObj.put("fmt", fmt);
+        final HashMap<String, String> attStmt = new HashMap<>();
+//        attStmt.put("sig", createSignature());
+//        attStmt.put("x5c", createX5c());
+        attObj.put("attStmt", attStmt);
+        return cborMapper.writeValueAsBytes(attObj);
+    }
+
+
+    /**
+     * Create the authenticator data part of the attestation object. 
+     * See https://www.w3.org/TR/webauthn-2/#sctn-attested-credential-data.
+     * 
+     * @param createdKey the key to use as the basis of this attestation object (and the new key to register).
+     * @param credentialId 
+     * @param aaguidHex 
+     * 
+     * @return the authData
+     * @throws NoSuchAlgorithmException on error. 
+     */
+    private AuthenticatorData createAuthData(final OneKey createdKey, final String aaguidHex, 
+            final String credentialId) throws Exception {
+        final byte[] rpIdHash = createRpIdHash();
+        final byte[] flags = createFlags(true, true, true, false);
+        final byte[] signCount = createSignCount(1);
+        final byte[] attestedCredentialData = createAttestedCredentialData(createdKey, aaguidHex, credentialId);
+        
+        final byte[] authDataCombined = new byte[rpIdHash.length + flags.length + signCount.length 
+                                                 + attestedCredentialData.length];
+        
+        System.arraycopy(rpIdHash, 0, authDataCombined, 0, rpIdHash.length);
+        System.arraycopy(flags, 0, authDataCombined, rpIdHash.length, flags.length);
+        System.arraycopy(signCount, 0, authDataCombined, rpIdHash.length + flags.length, signCount.length);
+        System.arraycopy(attestedCredentialData, 0, authDataCombined, rpIdHash.length + flags.length + signCount.length, 
+                attestedCredentialData.length);
+        
+        
+        final ByteArray authData = new ByteArray(authDataCombined);
+        final AuthenticatorData data = new AuthenticatorData(authData);
+        log.debug("Created Authenticator Data for RP, '{}' RPHash '{}', SignCounter '{}':  '{}'", rpId, 
+                data.getRpIdHash().getHex(), data.getSignatureCounter(), data);
+        return data;
+    }
+    
+    /**
+     * Create the authenticator flags set. See https://www.w3.org/TR/webauthn-2/#authenticator-data.
+     * 
+     * @param up User present flag
+     * @param uv User verified flag
+     * @param atIncluded attested credential data included flag (should be true for create credentials)
+     * @param extDataIncluded extension data included flag
+     * 
+     * @return a byte of the flags. Should only be one byte.
+     */
+    private byte[] createFlags(final boolean up, final boolean uv, final boolean atIncluded, 
+            final boolean extDataIncluded) {
+        final BitSet bits = new BitSet();
+        
+        if (up) bits.set(0);
+        if (uv) bits.set(2);
+        if (atIncluded) bits.set(6);
+        if (extDataIncluded) bits.set(7);
+
+        return bits.toByteArray();        
+    }
+    
+    private byte[] createAttestedCredentialData(final OneKey createdKey, 
+            final String aaguidHex, final String credentialId) throws Exception {
+        final byte[] aaguid = createAaguid(aaguidHex);
+        final byte[] credentialIdAndLength = createCredentialId(credentialId); 
+        // OneKey is from the COSE-JAVA lib that Yubico use, but is not perhaps well maintained
+        log.debug("Key is type {}", createdKey);
+        final byte[] coseKeyAsCborBytes = createdKey.AsCBOR().EncodeToBytes();
+        
+        final byte[] attestedCredentialDataCombined = new byte[aaguid.length + credentialIdAndLength.length 
+                                                               + coseKeyAsCborBytes.length];
+        
+        System.arraycopy(aaguid, 0, attestedCredentialDataCombined, 0, aaguid.length);
+        System.arraycopy(credentialIdAndLength, 0, attestedCredentialDataCombined, aaguid.length, 
+                credentialIdAndLength.length);
+        System.arraycopy(coseKeyAsCborBytes, 0, attestedCredentialDataCombined, 
+                aaguid.length + credentialIdAndLength.length, coseKeyAsCborBytes.length);
+        
+        
+        return attestedCredentialDataCombined;
+    }
+    
+    private byte[] createAaguid(final String aaguidHex) throws DecoderException {
+        assertEquals(aaguidHex.length(), 32);
+        final byte[] aaguidBytes = Hex.decodeHex(aaguidHex.toCharArray());
+        assertEquals(aaguidBytes.length, 16);
+        return aaguidBytes;
+        
+    }
+    
+    private byte[] createSignCount(final int count){
+        return ByteBuffer.allocate(4).putInt(count).array();
+    }
+    
+    private byte[] createCredentialId(final String credentialId) throws DecoderException {
+        final byte[] credentialIdBytes = Hex.decodeHex(credentialId.toCharArray());
+        final int credentialIdLength = credentialIdBytes.length;
+           
+        final byte[] credentialIdLengthBytes = new byte[2];
+        credentialIdLengthBytes[0] = (byte) ((credentialIdLength >>> 8) & 0xFF);
+        credentialIdLengthBytes[1] = (byte) (credentialIdLength & 0xFF);
+        assertEquals(credentialIdLengthBytes.length, 2);
+        
+        final byte[] credentialIdCombined = new byte[credentialIdBytes.length + credentialIdLengthBytes.length];
+        
+        System.arraycopy(credentialIdLengthBytes, 0, credentialIdCombined, 0, credentialIdLengthBytes.length);
+        System.arraycopy(credentialIdBytes, 0, credentialIdCombined, credentialIdLengthBytes.length, 
+                credentialIdBytes.length);
+        return credentialIdCombined;
+        
+    }
+   
+    
+    /**
+     * Create a SHA-256 hash of the relying party ID.
+     * 
+     * @return the SHA-25 hash of the relying party ID.
+     * 
+     * @throws NoSuchAlgorithmException on error
+     */
+    private byte[] createRpIdHash() throws NoSuchAlgorithmException {
+        final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        final byte[] encodedhash = digest.digest(rpId.getBytes(StandardCharsets.UTF_8));
+        return encodedhash;
+    }
+    
+    /**
+     * Generates a random identifier in Hex format.
+     *  
+     * @param length the length of the parameter.
+     * 
+     * @return the randomly generated value.
+     */
+    @Nonnull static String generateRandomIdentifierHex(@Nonnull final Integer length) {
+        final SecureRandom secureRandom = new SecureRandom();
+        final StringBuilder sb = new StringBuilder();
+        while(sb.length() < length){
+            sb.append(Integer.toHexString(secureRandom.nextInt()));
+        }
+        return sb.toString().substring(0, length);
+    }
+    
+    /** Simple attestion return type so the caller can access the underlying values easily.*/
+    public class Attestation {
+        
+        private final OneKey key;
+        private final String credentialIdHex;
+        private final String aaguidHex;
+        private final byte[] attestationObjectCose;
+        
+        /**
+         * Constructor.
+         *
+         * @param key the key
+         * @param credentialIdHex the credential Id
+         * @param aaguidHex the aaguid
+         * @param attestationObjectCose the attestation object
+         */
+        public Attestation(final OneKey key, final String credentialIdHex, final String aaguidHex, 
+                final byte[] attestationObjectCose) {
+            super();
+            this.key = key;
+            this.credentialIdHex = credentialIdHex;
+            this.aaguidHex = aaguidHex;
+            this.attestationObjectCose = attestationObjectCose;
+        }
+
+        /**
+         * @return Returns the key.
+         */
+        public OneKey getKey() {
+            return key;
+        }
+
+        /**
+         * @return Returns the credentialIdHex.
+         */
+        public String getCredentialIdHex() {
+            return credentialIdHex;
+        }
+        
+        public byte[] getCredentialIdBytes() throws DecoderException {
+            return Hex.decodeHex(credentialIdHex.toCharArray());
+        }
+        
+        public byte[] getAaguidBytes() throws DecoderException {
+            return Hex.decodeHex(aaguidHex.toCharArray());
+        }
+
+        /**
+         * @return Returns the aaguidHex.
+         */
+        public String getAaguidHex() {
+            return aaguidHex;
+        }
+
+        /**
+         * @return Returns the attestationObjectCose.
+         */
+        public byte[] getAttestationObjectCose() {
+            return attestationObjectCose;
+        }
+        
+        
+    }
+
+}
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
new file mode 100644
index 0000000..a435aab
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredentialTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.assertNull;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.MockAuthenticator.Attestation;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.shared.codec.Base64Support;
+
+
+/**
+ * Tests for {@link ValidatePublicKeyCredential}.
+ */
+public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
+    
+    private ValidatePublicKeyCredential validator;
+    
+    private final static String CHALLENGE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+    
+    private final static String USER_HANDLE_B64 = "dGhpc2lzYWNoYWxsZW5nZQ==";
+   
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        validator = new ValidatePublicKeyCredential();
+        //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 options =
+                PublicKeyCredentialCreationOptions.builder()
+                    .rp(rp.getIdentity())
+                    .user(identity)
+                    .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
+                    .pubKeyCredParams(preferredPublickeyParams)
+                    .excludeCredentials(Optional.empty())
+                    .timeout(Optional.empty()).build();
+        final String credentialCreationOptions = 
+                jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
+        webAuthnContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
+                
+        final WebAuthnAuthenticationClient client = new YubicoWebauthnAuthenticationClient(rp, jsonMapper);
+        validator.setWebAuthnClient(client);
+        validator.initialize();
+    }
+    
+    @Test
+    public void testSuccesss() throws Exception {   
+        
+        mockAuthenticator = new MockAuthenticator("https://idp.example.com", "idp.example.com");
+        
+        final Map<String, String> clientDataJson = 
+                mockAuthenticator.createClientDataJson("webauthn.create", CHALLENGE_B64); 
+        final String clientDataJsonString = new ObjectMapper().writeValueAsString(clientDataJson);
+    
+        final Attestation attestationObject = mockAuthenticator.createCredentialAttestationObject();
+        webAuthnContext.setPublicKeyCredential(createPublicKeyCredential(attestationObject, clientDataJsonString));
+        
+        final Event event = validator.execute(src);
+        assertNull(event);
+    }
+    
+   
+   
+    // Helper method to convert a byte array to a hex string
+    public static String bytesToHex(final byte[] bytes) {
+        final StringBuilder sb = new StringBuilder();
+        for (final byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+
+}

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


More information about the commits mailing list