[java-idp-plugin-webauthn] branch main updated: Further basic registration and authentication flow

Phil Smart philip.smart at jisc.ac.uk
Fri Dec 1 14:46:35 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=b1eb3e5ffd362af78cd39adf8bda48702b7972ac

The following commit(s) were added to refs/heads/main by this push:
     new b1eb3e5  Further basic registration and authentication flow
b1eb3e5 is described below

commit b1eb3e5ffd362af78cd39adf8bda48702b7972ac
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Dec 1 14:46:33 2023 +0000

    Further basic registration and authentication flow
    
     - Add validation to assertion and attestation responses
     - Move to Yubico types for webauthn interface
     - Improve actions
---
 .../AbstractWebAuthnAuthenticationAction.java      |   8 +-
 ...ava => AbstractWebAuthnRegistrationAction.java} |  91 ++++-----
 .../authn/webauthn/AssertionFailureException.java  |  65 +++++++
 .../plugin/authn/webauthn/CredentialPublicKey.java |  98 ++++++++++
 .../webauthn/RegistrationFailureException.java     |  65 +++++++
 .../webauthn/WebAuthnAuthenticationClient.java     |  57 +++---
 .../webauthn/context/BaseWebAuthnContext.java      |  88 +++++++++
 .../context/WebAuthnAuthenticationContext.java     | 177 +++++------------
 .../context/WebAuthnRegistrationContext.java       | 142 ++++++++++++++
 .../CreatePublicKeyCredentialCreationOptions.java  |  74 +++++---
 ...ctAuthenticatorAttestationFromFormRequest.java} |  20 +-
 .../impl/GenerateUserHandle.java}                  |  31 +--
 .../impl/PopulateWebAuthnRegistrationContext.java} |  68 +++----
 .../admin/impl/StorePublicKeyCredential.java       | 210 +++++++++++++++++++++
 .../ValidateAuthenticatorAttestationResponse.java  |  55 +++---
 .../impl/YubicoWebauthnAuthenticationClient.java   | 158 +++++++---------
 .../client/impl/YubicoWebauthnClientFactory.java   |  53 ++++--
 .../CreatePublicKeyCredentialRequestOptions.java   |  41 +++-
 ...ractAuthenticatorAssertionFromFormRequest.java} |  24 ++-
 .../webauthn/impl/GenerateServerChallenge.java     |  56 +++++-
 ... => PopulateWebAuthnAuthenticationContext.java} |   6 +-
 .../webauthn/impl/ValidateWebAuthnAssertion.java   |  34 ++--
 .../storage/impl/CredentialPublicKeyHolder.java    |  91 +++++++++
 .../storage/impl/CredentialRegistration.java       |   1 +
 .../storage/impl/StorePublicKeyCredential.java     | 161 ----------------
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  13 +-
 .../webauthn-registration-beans.xml                |  59 +++---
 .../webauthn-registration-flow.xml                 |   9 +-
 .../authn/WebAuthn/webauthn-abstract-beans.xml     |  10 +-
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  19 +-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |   4 +-
 .../YubicoWebauthnAuthenticationClientTest.java    |  64 +++----
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  |   7 +
 .../impl/ValidatePublicKeyCredentialTest.java      |   8 +-
 34 files changed, 1348 insertions(+), 719 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 a9448cc..d81e6ee 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
@@ -58,7 +58,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
     /** Lookup strategy to locate the webauthn authentication context. */
     @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
     
-    /** The Duo authentication Context.*/
+    /** The WebAuthn authentication Context.*/
     @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
     
     /** The WebAuthn client to use.*/
@@ -95,7 +95,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
     
     
     /**
-     * Set Duo authentication context lookup strategy to use.
+     * Set WebAuthn authentication context lookup strategy to use.
      * 
      * @param strategy lookup strategy
      */
@@ -104,7 +104,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
         checkSetterPreconditions();
 
         webauthnContextLookupStrategy = 
-                Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+                Constraint.isNotNull(strategy, "WebAuthnContextLookuplookup strategy cannot be null");
     }
     
     /** {@inheritDoc} */
@@ -128,7 +128,7 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
       
         webauthnContext = webauthnContextLookupStrategy.apply(profileRequestContext);
         if (webauthnContext == null) {
-            log.warn("{} No Webauthn context returned by lookup strategy",getLogPrefix());
+            log.warn("{} No WebAuthn authentication context returned by lookup strategy",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
             return false;
             
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/AbstractWebAuthnRegistrationAction.java
similarity index 55%
copy from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
copy to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnRegistrationAction.java
index a9448cc..017d9e8 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/AbstractWebAuthnRegistrationAction.java
@@ -23,13 +23,13 @@ import javax.annotation.Nonnull;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import net.shibboleth.idp.authn.AbstractAuthenticationAction;
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -37,29 +37,29 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * <p>A base class for webauthn authentication related actions.</p>
+ * <p>A base class for WebAuthn registration related admin actions.</p>
  * 
- * <p>In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
- * and makes available the {@link WebAuthnAuthenticationContext}.</p>
+ * <p>In addition to the work performed by {@link AbstractProfileAction}, this action also looks up
+ * and makes available the {@link WebAuthnRegistrationContext}.</p>
  * 
- * <p>Webauthn authentication action implementations should override the
- * {@link #doExecute(ProfileRequestContext, AuthenticationContext, WebAuthnAuthenticationContext)} 
- * method.</p>
+ * <p>WebAuthn registration action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, WebAuthnRegistrationContext)} method.</p>
  * 
- * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(ProfileRequestContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
  */
-public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
+public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfileAction {
     
     /** Class logger. */
-    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnAuthenticationAction.class);
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnRegistrationAction.class);
     
-    /** Lookup strategy to locate the webauthn authentication context. */
-    @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+    /** Lookup strategy to locate the webauthn registration context. */
+    @Nonnull 
+    private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
     
-    /** The Duo authentication Context.*/
-    @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
+    /** The WebAuthn registration Context.*/
+    @NonnullBeforeExec private WebAuthnRegistrationContext webauthnRegistrationContext;
     
     /** The WebAuthn client to use.*/
     @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
@@ -87,23 +87,22 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
         
     
     /** Constructor.*/
-    protected AbstractWebAuthnAuthenticationAction() {
-        //prc -> ac -> dc
-        webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
-                compose(new ChildContextLookup<>(AuthenticationContext.class));
+    protected AbstractWebAuthnRegistrationAction() {
+        //prc -> WebAuthnContext
+        webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
     }
     
     
     /**
-     * Set Duo authentication context lookup strategy to use.
+     * Set WebAuthn registration context lookup strategy to use.
      * 
      * @param strategy lookup strategy
      */
-    public void setWebauthnContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+    public void setWebauthnRegistrationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
         checkSetterPreconditions();
 
-        webauthnContextLookupStrategy = 
+        webauthnRegistrationContextLookupStrategy = 
                 Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
     }
     
@@ -119,50 +118,44 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
     
     /** {@inheritDoc} */
     @Override
-    protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+    protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+        if (!super.doPreExecute(profileRequestContext)) {
             return false;
         }
       
-        webauthnContext = webauthnContextLookupStrategy.apply(profileRequestContext);
-        if (webauthnContext == null) {
-            log.warn("{} No Webauthn context returned by lookup strategy",getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+        webauthnRegistrationContext = webauthnRegistrationContextLookupStrategy.apply(profileRequestContext);
+        if (webauthnRegistrationContext == null) {
+            log.warn("{} No WebAuthn registration context returned by lookup strategy",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
             
         }        
-        return doPreExecute(profileRequestContext, authenticationContext, webauthnContext);
+        return doPreExecute(profileRequestContext, webauthnRegistrationContext);
     }
     
     /**
-     * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext, 
-     * WebAuthnAuthenticationContext)} to perform the actual authentication. Implementations can not 
-     * override this method.
+     * Delegates to {@link #doExecute(ProfileRequestContext, WebAuthnAuthenticationContext)} to perform the 
+     * actual action. Implementations can not override this method.
      * 
      * @param profileRequestContext the current IdP profile request context
-     * @param authenticationContext the current authentication context
      */
     @Override
-    protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+    protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        doExecute(profileRequestContext,authenticationContext, webauthnContext);
+        doExecute(profileRequestContext, webauthnRegistrationContext);
     }
     
     /**
-     * Performs this authentication action's pre-execute step. Default implementation just returns true.
+     * Performs this admin action's pre-execute step. Default implementation just returns true.
      * 
      * @param profileRequestContext the current IdP profile request context
-     * @param authenticationContext the current authentication context
-     * @param context the webauthn authentication context
+     * @param context the WebAuthn registration context
      * 
      * @return true iff execution should continue
      */
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
         return true;
     }
     
@@ -171,12 +164,10 @@ public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthe
      * should override this method.
      * 
      * @param profileRequestContext the current IdP profile request context
-     * @param authenticationContext the current authentication context
-     * @param context the webauthn authentication context
+     * @param context the WebAuthn registration context
      */
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
         
     }
 
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java
new file mode 100644
index 0000000..a678eaa
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AssertionFailureException.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * Exception that is throw if an assertion is not valid.
+ */
+public class AssertionFailureException extends WebAuthnAuthenticationClientException {
+    
+    /** Generated serial UID.*/
+    private static final long serialVersionUID = 1266841324008931444L;
+
+    /**
+     * Constructor.
+     *
+     */
+    public AssertionFailureException() {
+        super();
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param cause exception to be wrapped by this one
+     */
+    public AssertionFailureException(final String message, final Throwable cause) {
+        super(message, cause);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public AssertionFailureException(final String message) {
+        super(message);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cause exception to be wrapped by this one
+     */
+    public AssertionFailureException(final Throwable cause) {
+        super(cause);
+        
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java
new file mode 100644
index 0000000..db67b76
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/CredentialPublicKey.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * The public key portion of the asymmetric credential key pair created by the authenticator during registration.
+ * 
+ * <p>This is stored and later used to validate assertions generated in authentication ceremonies.</p>
+ */
+public class CredentialPublicKey {
+    
+    /** 
+     * Identifies the public key credential source and its authentication assertions. Generated by the 
+     * authenticator and is an opaque random byte array.
+     */
+    @Nonnull private final byte[] credentialId;
+    
+    /** The user ID as specified by the IdP that maps a credential public key to a user.*/
+    @Nonnull private final byte[] userHandle;
+    
+    /** The COSE_Key encoded credential public key used to verify authentication assertions.*/
+    @Nonnull private final byte[] publicKeyCose;
+    
+    /** The current signature count for this credential from the authenticator.*/
+    private final long signatureCount;
+
+    /**
+     * Constructor.
+     *
+     * @param credentialId the identifier of the credential. 
+     * @param userHandle the user ID that maps this credential public key to a user.
+     * @param publicKeyCose the public key in COSE_Key format
+     * @param signatureCount the signature counter for this credential.
+     */
+    public CredentialPublicKey(@Nonnull final byte[] credId, @Nonnull final byte[] handle, 
+            @Nonnull final byte[] keyCose, final long signCount) {
+        super();
+        credentialId = Constraint.isNotNull(credId, "CredentialID can not be null");
+        userHandle =  Constraint.isNotNull(handle, "UserHandle can not be null");
+        publicKeyCose = Constraint.isNotNull(keyCose, "PublicKey in COSE_Key format can not be null");
+        signatureCount = signCount;
+    }
+
+    /**
+     * Get the credential's identifier. 
+     * 
+     * @return the credentialId.
+     */
+    @Nonnull public byte[] getCredentialId() {
+        return credentialId;
+    }
+
+    /**
+     * Get the user handle of this identifier. 
+     * 
+     * @return the userHandle.
+     */
+    @Nonnull public byte[] getUserHandle() {
+        return userHandle;
+    }
+
+    /**
+     * Get the public key in COSE_Key format. 
+     * 
+     * @return the publicKeyCose.
+     */
+    @Nonnull public byte[] getPublicKeyCose() {
+        return publicKeyCose;
+    }
+
+    /**
+     * Get the signature count from the authenticator. 
+     * 
+     * @return the signatureCount.
+     */
+    public long getSignatureCount() {
+        return signatureCount;
+    }
+    
+    
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java
new file mode 100644
index 0000000..ec6731a
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/RegistrationFailureException.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * Exception that is throw if a public key credential registration is not valid.
+ */
+public class RegistrationFailureException extends WebAuthnAuthenticationClientException {
+    
+    /** Generated serial UID.*/
+    private static final long serialVersionUID = 1266841324008931444L;
+
+    /**
+     * Constructor.
+     *
+     */
+    public RegistrationFailureException() {
+        super();
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param cause exception to be wrapped by this one
+     */
+    public RegistrationFailureException(final String message, final Throwable cause) {
+        super(message, cause);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public RegistrationFailureException(final String message) {
+        super(message);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cause exception to be wrapped by this one
+     */
+    public RegistrationFailureException(final Throwable cause) {
+        super(cause);
+        
+    }
+
+}
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 bbcbc5d..53b820e 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
@@ -4,18 +4,25 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.ThreadSafe;
 
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
 /**
  * A client that manages the entire webauthn authentication and registration ceremony.
  * 
+ * <p>Tied to the Yubico library data model, other clients will need to be adapted to support this model, and provide
+ * translations between their internal representation and the types required.</p>
+ * 
  * <p>Implementations must be thread-safe</p>
  */
-//TODO integrate this with the CredentialValidator interface?
 @ThreadSafe
 public interface WebAuthnAuthenticationClient {
     
     
      /**
-      * Create a JSON serialized PublicKeyCredentialRequestOptions.
+      * Create a PublicKeyCredentialRequestOptions for the WebAuthn 'get' call to generate an authentication assertion.
       * 
       * @param username the username of the username that has been pre-identified. Can be null
       *                 if no user has been identified, and the IdP is requesting the client discover
@@ -23,61 +30,65 @@ public interface WebAuthnAuthenticationClient {
       * @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
       * @param the challenge used when creating new credentials
       * 
-      * @return a JSON serialized PublicKeyCredentialRequestOptions object. 
+      * @return a PublicKeyCredentialRequestOptions object
       * 
       * @throws WebAuthnAuthenticationClientException if there is an error generating the authentication request
       *         
       */
-    //TODO how do we guarantee this is JSON, or just return the Yubico object?
-     @Nonnull String createAuthenticationRequest(@Nullable final String username, @Nullable final byte[] userHandle, 
-             @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
+     @Nonnull PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username, 
+             @Nullable final byte[] userHandle, @Nonnull final byte[] challenge) 
+                     throws WebAuthnAuthenticationClientException;
      
      /**
-      * Create a JSON serialized PublicKeyCredentialCreationOptions.
+      * Create a PublicKeyCredentialCreationOptions for the WebAuthn 'create' call to generate a registration 
+      * attestation.
       * 
       * @param username the username of the username that has been pre-identified. Can be {@code null}
       *                 if no user has been identified, and the IdP is requesting the client discover
       *                 the credential.  
       * @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
-      * @param the challenge used when creating new credentials
+      * @param the challenge to sign when creating new credentials
       * 
-      * @return a JSON serialized PublicKeyCredentialCreationOptions object. Can be {@code null} if
-      *         one could not be created.
+      * @return a PublicKeyCredentialCreationOptions object to supply the WebAuthn 'create' call. 
       *         
       * @throws WebAuthnAuthenticationClientException if there is an error generating the creation request
       */
-    //TODO how do we guarantee this is JSON, or just return the Yubico object?
-     @Nullable String createRegistrationRequest(@Nullable final String username, @Nullable final byte[] userHandle, 
-             @Nonnull final byte[] challenge) throws WebAuthnAuthenticationClientException;
+     @Nonnull PublicKeyCredentialCreationOptions createRegistrationRequest(@Nullable final String username, 
+             @Nullable final byte[] userHandle, @Nonnull final byte[] challenge) 
+                     throws WebAuthnAuthenticationClientException;
      
      
      /**
-      * Validate the Authenticator Assertion Response. 
+      * Validate the Authenticator Assertion Response from an authentication request
       * 
       * @param usernmae ....TODO
       * @param userHandle ....TODO
       * @param publicKeyCredentialRequestOptions the options used when generating an assertion for authentication.
       * @param authenticatorAssertionResponse the JSON representation of the assertion response.
       * 
-      * @return true if the assertion was verified successfully, false otherwise.
+      * @return an assertion result if the assertion was valid.
       * 
-      * @throws WebAuthnAuthenticationClientException if there is an error during validation
+      * @throws WebAuthnAuthenticationClientException if the assertion is not valid
       */
-     boolean validateAuthenticatorAssertionResponse(@Nullable final String username, 
-             @Nullable final byte[] userHandle, @Nonnull final String publicKeyCredentialRequestOptions, 
+     AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username, 
+             @Nullable final byte[] userHandle, 
+             @Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions, 
              @Nonnull final String authenticatorAssertionResponse) 
-             throws WebAuthnAuthenticationClientException;
+             throws AssertionFailureException;
 
 
     /**
-     * Validate a registration request.
+     * Validate the Authenticator Attestation Response from a registration request.
      * 
      * @param publicKeyCredentialCreationOptions the options used when requesting a new public key credential
      * @param authenticatorAttestationResponse the response to the client's request to create a public key credential
      * 
-     * @return true if the registration is valid, false otherwise.
+     * @return a registration result of the registration was valid
+     * 
+     * @throws RegistrationFailureException if the registration is not valid
      */
-     boolean validateRegistration(@Nonnull final String publicKeyCredentialCreationOptions, 
-             @Nonnull final String authenticatorAttestationResponse);       
+     RegistrationResult validateAuthenticatorAttestationResponse(
+             @Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions, 
+             @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException;       
 
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
new file mode 100644
index 0000000..4cf3a25
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/BaseWebAuthnContext.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * The base WebAuthn context for both registration and authentication ceremonies.
+ */
+public class BaseWebAuthnContext extends BaseContext {
+    
+    /** The challenge sent to the authenticator in both registration and authentication ceremonies.*/
+    @Nullable private byte[] serverChallenge;  
+    
+    /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+    @Nullable private byte[] userHandle;
+    
+    /**
+     * Get the server challenge sent to the authenticator.
+     * 
+     * @return the server challenge.
+     */
+    @Nullable public byte[] getServerChallenge() {
+        return serverChallenge;
+    }
+    
+    //TODO throw in a context, this could be null (which is bad here?)
+    @SuppressWarnings("null")
+    @Nullable public String getServerChallengeBase64() throws EncodingException {
+        return Base64Support.encode(serverChallenge, false);
+    }
+    
+    /**
+     * Set the server challenge which forms part of the information the client authenticator needs to sign.
+     * 
+     * @param challenge the challenge, must not be empty and must be minimum 16 bytes long.
+     * 
+     * @return this context.
+     */
+    @Nonnull public BaseWebAuthnContext setServerChallenge(@Nonnull final byte[] challenge) {
+        Constraint.isNotEmpty(challenge,"Challenge can not be null or empty");
+        Constraint.isGreaterThan(16, challenge.length, "Challenge must be at least 16 bytes");
+        serverChallenge = challenge;
+        return this;
+    }
+    
+    
+    /**
+     * Set the user handle used to map public key credentials to user accounts. Maximum 64 bytes 
+     * 
+     * @param handle The userHandle to set.
+     */
+    public BaseWebAuthnContext setUserHandle(@Nonnull final byte[] handle) {
+        Constraint.isNotEmpty(handle,"UserHandle can not be null or empty");
+        Constraint.isLessThan(65, handle.length, "UserHandle must be maximum 64 bytes");
+        userHandle = handle;
+        return this;
+    }
+    
+    /**
+     * Get the userId used to map public key credentials to user accounts.
+     * 
+     * @return the userHandle.
+     */
+    public byte[] getUserHandle() {
+        return userHandle;
+    }
+
+}
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 83a9f9b..7729c59 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
@@ -4,18 +4,14 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
-import org.opensaml.messaging.context.BaseContext;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
 
-import net.shibboleth.shared.codec.Base64Support;
-import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.logic.Constraint;
 
 
-//TODO if we use a client which supports more than just Yubico (not too realistic) then we need to generalise these types
+/** Authentication context for processing WebAuthn Authentication Ceremonies. */
 @NotThreadSafe
-public final class WebAuthnAuthenticationContext extends BaseContext {
-    
-    @Nullable private byte[] serverChallenge;  
+public final class WebAuthnAuthenticationContext extends BaseWebAuthnContext {
         
     /** The  credential public key encoded in COSE_Key format.*/
     @Nullable private byte[] publicKey;
@@ -26,56 +22,19 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     /** The original username. */
     @Nullable private String username;
     
-    /** In a new context? TODO. But for now an existing credId if found.*/
-    @Nullable private byte[] existingCredentialId;
-    
-    /** In a new context? TODO. The existing credential public key encoded in COSE_Key format if found.*/
-    @Nullable private byte[] existingPublicKey;    
-    
     /** An assertion response that is the result of an authentication.*/
     @Nullable private String authenticatorAssertionResponse;
-    
-    /** An assertion response that is the result of an authentication.*/
-    @Nullable private String authenticatorAttestationResponse;
-    
-    /** The public key credential creation options for registration.*/ 
-    @Nullable private String publicKeyCredentialCreationOptions;
    
     /** The public key credential request options for authentication.*/ 
-    @Nullable private String publicKeyCredentialRequestOptions;
+    @Nullable private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
     
-    /**
-     * Set the server challenge which the client authenticator needs to sign.
-     * 
-     * @param challenge the challenge, must not be empty and must be minimum 16 bytes long.
-     * 
-     * @return this context.
-     */
-    @Nonnull public WebAuthnAuthenticationContext setServerChallenge(@Nonnull final byte[] challenge) {
-        Constraint.isNotEmpty(challenge,"Challenge can not be null or empty");
-        Constraint.isGreaterThan(16, challenge.length, "Challenge must be at least 16 bytes");
-        serverChallenge = challenge;
-        return this;
-    }
-        
-    /**
-     * Get the attestation response as a result of creating a new credential.
-     * 
-     * @return Returns the authenticator attestation response.
-     */
-    @Nullable public String getAuthenticatorAttestationResponse() {
-        return authenticatorAttestationResponse;
-    }
-    
-    /**
-     * Set the attestation response as a result of creating a new credential.
-     * 
-     * @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
-     */
-    public void setAuthenticatorAttestationResponse(@Nullable final String attestation) {
-        authenticatorAttestationResponse = attestation;
-    }
+    // 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;
     
+    /** The userhandle supplied to the authenticator during registration. As generated by the IdP.*/
+    @Nullable private byte[] userHandle;
+  
     /**
      * Gets the username.
      * 
@@ -98,23 +57,6 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
         return this;
     }
     
-    /**
-     * Get the server challenge for the authenticator to sign.
-     * 
-     * @return the server challenge.
-     */
-    @Nullable public byte[] getServerChallenge() {
-        return serverChallenge;
-    }
-    
-    /**
-     * Get the public key in COSE_Key format.
-     * 
-     * @return the public key.
-     */
-    @Nullable public byte[] getPublicKey() {
-        return publicKey;
-    }
     
     /**
      * Set the public key, as a byte array, in COSE_Key format.
@@ -125,45 +67,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
         publicKey = Constraint.isNotNull(key, "Public key can not be null");
         return this;
     }
-    
-    /**
-     * Get the existing public key in COSE_Key format.
-     * 
-     * @return the public key.
-     */
-    @Nullable public byte[] getExsitingPublicKey() {
-        return publicKey;
-    }
-    
-    /**
-     * Set the existing public key, as a byte array, in COSE_Key format.
-     * 
-     * @param key the public key in COSE_Key format.
-     */
-    public WebAuthnAuthenticationContext setExistingPublicKey(@Nonnull final byte[] key) {
-        publicKey = Constraint.isNotNull(key, "Public key can not be null");
-        return this;
-    }
-    
-    /**
-     * Get the existing credential Id.
-     * 
-     * @return the credential Id.
-     */
-    public byte[] getExistingCredentialId() {
-        return credentialId;
-    }
-    
-    /**
-     * Set the existing credential Id.
-     * 
-     * @param id the credential Id.
-     */
-    public WebAuthnAuthenticationContext setExistingCredentialId(@Nonnull final byte[] id) {
-        credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
-        return this;
-    }
-    
+
     /**
      * Get the credential Id.
      * 
@@ -182,39 +86,17 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
         credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
         return this;
     }
-    
-    //TODO throw in a context, this could be null (which is bad here?)
-    @SuppressWarnings("null")
-    @Nullable public String getServerChallengeBase64() throws EncodingException {
-        return Base64Support.encode(serverChallenge, false);
-    }
 
     
-    /**
-     * Set the options used to create public key credentials. 
-     * 
-     * @param options The publicKeyCredentialCreationOptions to set.
-     */
-    public void setPublicKeyCredentialCreationOptions(@Nullable final String options) {
-        publicKeyCredentialCreationOptions = options;
-    }
-    
-    /**
-     * Get the options used to create public key credentials.
-     * 
-     * @return the publicKeyCredentialCreationOptions.
-     */
-    @Nullable public String getPublicKeyCredentialCreationOptions() {
-        return publicKeyCredentialCreationOptions;
-    }
-    
     /**
      * Set the public key credential request options for authentication.
      * 
      * @param the credential request options to set.
      */
-    public void setPublicKeyCredentialRequestOptions(@Nullable final String options) {
+    public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptions(
+            @Nullable final PublicKeyCredentialRequestOptions options) {
         publicKeyCredentialRequestOptions = options;
+        return this;
     }
     
     /**
@@ -222,7 +104,7 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
      * 
      * @return the public key credential request options.
      */
-    @Nullable public String getPublicKeyCredentialRequestOptions() {
+    @Nullable public PublicKeyCredentialRequestOptions getPublicKeyCredentialRequestOptions() {
         return publicKeyCredentialRequestOptions;
     }
 
@@ -231,8 +113,9 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
      *  
      * @param assertion The authenticator assertion response to set.
      */
-    public void setAuthenticatorAssertionResponse(@Nullable final String assertion) {
+    public WebAuthnAuthenticationContext setAuthenticatorAssertionResponse(@Nullable final String assertion) {
         authenticatorAssertionResponse = assertion;
+        return this;
     }
     
     /**
@@ -243,4 +126,30 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     @Nullable public String getAuthenticatorAssertionResponse() {
         return authenticatorAssertionResponse;
     }
+        
+    /**
+     * Get the userId used to map public key credentials to user accounts.
+     * 
+     * @return the userHandle.
+     */
+    @Override
+    public byte[] getUserHandle() {
+        return userHandle;
+    }
+    
+    /**
+     * @return Returns the publicKeyCredentialRequestOptionsJSON.
+     */
+    public String getPublicKeyCredentialRequestOptionsJSON() {
+        return publicKeyCredentialRequestOptionsJSON;
+    }
+    
+    /**
+     * @param publicKeyCredentialRequestOptionsJSON The publicKeyCredentialRequestOptionsJSON to set.
+     */
+    public WebAuthnAuthenticationContext setPublicKeyCredentialRequestOptionsJSON(
+            @Nullable final String requestOptionsJSON) {
+        publicKeyCredentialRequestOptionsJSON = requestOptionsJSON;
+        return this;
+    }
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
new file mode 100644
index 0000000..d2b7348
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
@@ -0,0 +1,142 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
+
+/** Registration context for processing WebAuthn Registration Ceremonies. */
+ at NotThreadSafe
+public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
+
+    /** The original username. */
+    @Nullable private String username;
+    
+    /** In a new context? TODO. But for now an existing credId if found.*/
+    @Deprecated
+    @Nullable private byte[] existingCredentialId;
+    
+    /** In a new context? TODO. The existing credential public key encoded in COSE_Key format if found.*/
+    @Deprecated
+    @Nullable private byte[] existingPublicKey;    
+    
+    /** An assertion response that is the result of an authentication.*/
+    @Nullable private String authenticatorAttestationResponse;
+    
+    /** The public key credential creation options for registration.*/ 
+    @Nullable private PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions;
+    
+    /** The public key credential creation options for registration represented as a JSON string.*/ 
+    @Nullable private String publicKeyCredentialCreationOptionsJSON;
+           
+    /** 
+     * The credential public key which is the result of registration of a new key pair generated
+     * by the authenticator.
+     */
+    @Nullable private RegistrationResult registrationResult;
+    
+
+    /**
+     * Get the attestation response as a result of creating a new credential.
+     * 
+     * @return Returns the authenticator attestation response.
+     */
+    @Nullable public String getAuthenticatorAttestationResponse() {
+        return authenticatorAttestationResponse;
+    }
+    
+    /**
+     * Set the attestation response as a result of creating a new credential.
+     * 
+     * @param authenticatorAttestationResponse The authenticatorAttestationResponse to set.
+     */
+    public WebAuthnRegistrationContext setAuthenticatorAttestationResponse(@Nullable final String attestation) {
+        authenticatorAttestationResponse = attestation;
+        return this;
+    }
+    
+    /**
+     * Gets the username.
+     * 
+     * @return the username
+     */
+    @Nullable public String getUsername() {
+        return username;
+    }
+
+    /**
+     * Sets the username and resets the transformed version to be identical.
+     * 
+     * @param name the username
+     * 
+     * @return this context
+     */
+    @Nonnull public WebAuthnRegistrationContext setUsername(@Nullable final String name) {
+        username = name;
+        //transformedUsername = name;
+        return this;
+    }
+    
+    
+
+    
+    /**
+     * Set the options used to create public key credentials. 
+     * 
+     * @param options The publicKeyCredentialCreationOptions to set.
+     */
+    public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptions(
+            @Nullable final PublicKeyCredentialCreationOptions options) {
+        publicKeyCredentialCreationOptions = options;
+        return this;
+    }
+    
+    /**
+     * Get the options used to create public key credentials.
+     * 
+     * @return the publicKeyCredentialCreationOptions.
+     */
+    @Nullable public PublicKeyCredentialCreationOptions getPublicKeyCredentialCreationOptions() {
+        return publicKeyCredentialCreationOptions;
+    }
+    
+    /**
+     * Set the registration result which is the result of registration of a new key pair generated
+     * by the authenticator.
+     * 
+     * @param registrationResult The registrationResult to set.
+     */
+    public WebAuthnRegistrationContext setRegistrationResult(@Nullable final RegistrationResult result) {
+        registrationResult = result;
+        return this;
+    }
+    
+    /**
+     * Get the credential public key which is the result of registration of a new key pair generated
+     * by the authenticator
+     * 
+     * @return Returns the registrationResult.
+     */
+    public RegistrationResult getRegistrationResult() {
+        return registrationResult;
+    }
+    
+    /**
+     * @return Returns the publicKeyCredentialCreationOptionsJSON.
+     */
+    public String getPublicKeyCredentialCreationOptionsJSON() {
+        return publicKeyCredentialCreationOptionsJSON;
+    }
+    
+    /**
+     * @param publicKeyCredentialCreationOptionsJSON The publicKeyCredentialCreationOptionsJSON to set.
+     */
+    public WebAuthnRegistrationContext setPublicKeyCredentialCreationOptionsJSON(final String optionsJSON) {
+        publicKeyCredentialCreationOptionsJSON = optionsJSON;
+        return this;
+    }
+
+}
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/admin/impl/CreatePublicKeyCredentialCreationOptions.java
similarity index 58%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CreatePublicKeyCredentialCreationOptions.java
index df58513..addcb34 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/admin/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -13,10 +13,7 @@
  */
 
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
-
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import javax.annotation.Nonnull;
 
@@ -24,26 +21,57 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
 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.AbstractWebAuthnRegistrationAction;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
  * Action to create a PublicKeyCredentialCreationOptions from the parameters in the WebAuthn context.
  */
-public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAuthenticationAction {
+public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnRegistrationAction {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialCreationOptions.class);
     
+    //TODO move out where we do this
+    /** The JSON object mapper used to JSONify webauthn objects. */
+    @NonnullAfterInit private ObjectMapper jsonObjectMapper;
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (jsonObjectMapper == null) {
+            throw new ComponentInitializationException("JSON Object Mapper can not be null");
+        }
+    }
+    
+    
+    /**
+     * Set the JSON object mapper to use.
+     * 
+     * @param mapper The jsonObjectMapper to set.
+     */
+    public void setJsonObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        jsonObjectMapper = Constraint.isNotNull(mapper, "JsonObjectMapper can not be null");
+    }       
+  
+    
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext, 
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
         
         final WebAuthnAuthenticationClient client = getWebAuthnClient();
         final byte[] challenge = context.getServerChallenge();
@@ -54,31 +82,19 @@ public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAu
         }
         
         try {                
-            final String pkCredCreationOptions = 
-                    client.createRegistrationRequest(context.getUsername(), generateUserHandle(), challenge);
-            //verify correct JSON response?
+            final PublicKeyCredentialCreationOptions pkCredCreationOptions = 
+                    client.createRegistrationRequest(context.getUsername(), context.getUserHandle(), challenge);
             context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
+            //convert to JSON
+            context.setPublicKeyCredentialCreationOptionsJSON(
+                    jsonObjectMapper.writeValueAsString(pkCredCreationOptions));
+            
             log.debug("{} Created PublicKeyCredentialCreationOptions '{}'",getLogPrefix(), pkCredCreationOptions);
-        } catch (final NoSuchAlgorithmException | WebAuthnAuthenticationClientException e) {
+        } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
             log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
             return;
         }        
     }
-    
-    /**
-     * Generate a 32 bytes user handle challenge of sufficient entropy. Must be at maximum
-     * 64 bytes long.
-     * 
-     * @return the challenge in bytes
-     * 
-     * @throws NoSuchAlgorithmException if no secure random algorithm is available
-     */
-    @Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {        
-        final byte[] bytes = new byte[32];
-        SecureRandom.getInstanceStrong().nextBytes(bytes);
-        log.trace("{} Generated '{}' byte challenge",getLogPrefix(), bytes.length);
-        return bytes;
-    }
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
similarity index 87%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
index 2d893da..30551b9 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractAuthenticatorAttestationFromFormRequest.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -28,9 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 
 import jakarta.servlet.http.HttpServletRequest;
 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.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -40,15 +39,15 @@ import net.shibboleth.shared.primitive.StringSupport;
 
 
 /**
- * An action that derives the PublicKeyCredential from a form parameter.
+ * An action that extracts the AuthenticatorAttestationResponse from the incoming HTTP request.
  */
-public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+public class ExtractAuthenticatorAttestationFromFormRequest extends AbstractWebAuthnRegistrationAction {
 
     /** Default token code field name. */
-    @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyCredential";
+    @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "authenticatorAttestation";
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialFromFormRequest.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractAuthenticatorAttestationFromFormRequest.class);
     
     /** Name of header. */
     @Nonnull @NotEmpty private String fieldName;
@@ -57,7 +56,7 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
     @NonnullAfterInit private ObjectMapper objectMapper;
     
     /** Constructor. */
-    public ExtractPublicKeyCredentialFromFormRequest() {
+    public ExtractAuthenticatorAttestationFromFormRequest() {
         fieldName = DEFAULT_FIELD_NAME;
     }
     
@@ -94,8 +93,7 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
     
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
         
         final HttpServletRequest request = getHttpServletRequest();
         if (request == null) {
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/GenerateUserHandle.java
similarity index 73%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/GenerateUserHandle.java
index bd874f2..9b8bea7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/GenerateUserHandle.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
@@ -27,9 +27,8 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
 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.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -41,22 +40,20 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
  * @post See above.
  */
-public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+public class GenerateUserHandle extends AbstractWebAuthnRegistrationAction {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateUserHandle.class);
    
 
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
         
         try {
-            //TODO check the spec, this should be a a buffersource, maybe the JS is generating it as one 
-            final byte[] challenge = generateChallenge();
-            log.trace("Generated challenge {}",challenge);
-            context.setServerChallenge(challenge);
+            final byte[] userHandle = generateUserHandle();
+            log.trace("Generated userHandle '{}'",userHandle);
+            context.setUserHandle(userHandle);
             
         } catch (final NoSuchAlgorithmException e) {
             log.error("Could not generate a challenge",e);
@@ -65,14 +62,18 @@ public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationActio
     }
     
     /**
-     * Generate a 32 bytes randomized challenge of sufficient entropy. Must be at least 16 bytes long.
+     * Generate a 64 bytes randomized challenge of sufficient entropy. Must be at least 32 bytes long.
+     * 
+     * </p>This could contain some form of state if required, but must not contain retrievable PII.</p>
+     * 
+     * @see <a href="https://www.w3.org/TR/webauthn-2/#sctn-user-handle-privacy">user handle</a>
      * 
      * @return the challenge in bytes
      * 
      * @throws NoSuchAlgorithmException if no secure random algorithm is available
      */
-    @Nonnull private byte[] generateChallenge() throws NoSuchAlgorithmException {        
-        final byte[] bytes = new byte[32];
+    @Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {        
+        final byte[] bytes = new byte[64];
         SecureRandom.getInstanceStrong().nextBytes(bytes);
         log.trace("Generated '{}' byte challenge",bytes.length);
         return bytes;
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/admin/impl/PopulateWebAuthnRegistrationContext.java
similarity index 53%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
index 9bccc42..588a3b1 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/admin/impl/PopulateWebAuthnRegistrationContext.java
@@ -13,10 +13,9 @@
  */
 
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import java.util.function.Function;
-import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 
@@ -26,16 +25,14 @@ import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import net.shibboleth.idp.authn.AbstractAuthenticationAction;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
 import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * An action to create (or lookup) and populate the {@link WebauthnAuthenticationContext} 
+ * An action to create (or lookup) and populate the {@link WebAuthnRegistrationContext} 
  * with the ... FIXME appropriate for this request. 
  * 
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
@@ -44,47 +41,38 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
  * @post See above.
  */
-public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnRegistrationContext.class);
     
-    /** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
-    @Nonnull 
-    private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
+    /** Strategy used to locate or create the {@link WebAuthnRegistrationContext} to populate. */
+    @Nonnull private 
+    Function<ProfileRequestContext,WebAuthnRegistrationContext> webAuthnRegistrationContextCreationStrategy;
     
-    /** Lookup strategy for username to match against Duo identity. */
+    /** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
     @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
     
-    /** Is the username required?*/
-    private Predicate<ProfileRequestContext> usernameRequiredPredicate;
-    
-
     /** Constructor.*/
-    public PopulateWebauthnAuthenticationContext() {
-        //default creates webauthn authentication context under authentication context.
-        webauthnAuthContextCreationStrategy =
-                new ChildContextLookup<>(WebAuthnAuthenticationContext.class, true).
-                compose(new ChildContextLookup<>(AuthenticationContext.class));
+    public PopulateWebAuthnRegistrationContext() {
+        // Default creates a WebAuthn registration context under the profile request context.
+        webAuthnRegistrationContextCreationStrategy =
+                new ChildContextLookup<>(WebAuthnRegistrationContext.class, true);
         
         usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
-        usernameRequiredPredicate = PredicateSupport.alwaysTrue();
-    }
 
-    /**
-     * @param flag The usernameRequired to set.
-     */
-    public void setUsernameRequired(final boolean flag) {
-        checkSetterPreconditions();
-        usernameRequiredPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
     }
     
     /**
-     * @param usernameRequiredPredicate The usernameRequiredPredicate to set.
+     * Set the strategy used to lookup or create the WebAuthn registration context.
+     * 
+     * @param strategy The strategy to set.
      */
-    public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+    public void setWebAuthnRegistrationContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, WebAuthnRegistrationContext> strategy) {
         checkSetterPreconditions();
-        usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+        webAuthnRegistrationContextCreationStrategy = Constraint.isNotNull(
+                strategy,"WebAuthnRegistrationContextCreationStrategy can not be null");
     }
     
     /**
@@ -101,26 +89,26 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
     
 
     /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
         
-        final WebAuthnAuthenticationContext context = webauthnAuthContextCreationStrategy.apply(profileRequestContext);
+        final WebAuthnRegistrationContext context = 
+                webAuthnRegistrationContextCreationStrategy.apply(profileRequestContext);
         if (context == null) {
-            log.error("{} Error creating WebauthnAuthenticationContext", getLogPrefix());
+            log.error("{} Error creating WebAuthnRegistrationContext", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
         
         final String username = usernameLookupStrategy.apply(profileRequestContext);
-        if (username == null && usernameRequiredPredicate.test(profileRequestContext)) {
-            log.error("{} Error creating WebauthnAuthenticationContext, no username found", getLogPrefix());
+        if (username == null) {
+            log.error("{} Error creating WebAuthnRegistrationContext, no username found", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return;
         }
         context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
 
-        log.debug("Created Webauthn authentication context");
+        log.debug("Created WebAuthn registration context for user '{}'", context.getUsername());
     }
     
     
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
new file mode 100644
index 0000000..1d5ba1a
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.TreeSet;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.StorageSerializer;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.AuthenticatorTransport;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.UserIdentity;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistration;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.WebauthnPublicKeyCredentialStorageSerializer;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
+
+    /** Backing service. */
+    @NonnullAfterInit
+    private StorageService storageService;    
+    
+    /** The credential respository to store valid credentials in.*/
+    // TODO replace with an adaptor to the storage service?
+    @NonnullAfterInit private CredentialRepository credentialRepository;
+
+    /** Storage record serializer. */
+    @Nonnull
+    private final StorageSerializer<WebAuthnPublicKeyCredentialRecord> serializer;
+
+    /** Constructor. */
+    public StorePublicKeyCredential() {
+        serializer = new WebauthnPublicKeyCredentialStorageSerializer();
+    }
+    
+    /**
+     * Set the credential repository used to store the valid webauthn credential.
+     *  
+     * @param credRepository The credRepository to set.
+     */
+    public void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+        checkSetterPreconditions();
+        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
+    }
+
+
+    /**
+     * Set the {@link StorageService} back-end to use.
+     * 
+     * @param storage
+     *            the back-end to use
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+
+        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+        final StorageCapabilities caps = storageService.getCapabilities();
+        Constraint.isTrue(caps.isServerSide(), "StorageService cannot be client-side");
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (credentialRepository == null) {
+            throw new ComponentInitializationException("Credential respository can not be null");
+        }
+
+        if (storageService == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+    }
+
+    // TODO maybe not needed.
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+        return true;
+    }
+
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+
+        final String username = context.getUsername();
+
+        if (username == null) {
+            log.error("Unable to find username in registration response");
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return;
+        }        
+        final RegistrationResult registrationResult = context.getRegistrationResult();
+        if (registrationResult == null) {
+            log.error("Unable to find registration information in registration response");
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return;
+        } 
+        try {
+            final RegisteredCredential credential = RegisteredCredential.builder()
+                    .credentialId(registrationResult.getKeyId().getId())
+                    .userHandle(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+                    .publicKeyCose(registrationResult.getPublicKeyCose())
+                    .build();
+            
+            final UserIdentity user = UserIdentity.builder()
+                    .name(username)
+                    .displayName(username)
+                    .id(ByteArray.fromBase64("bm90YWhhbmRsZQ=="))
+                    .build();
+            
+            // TODO fixup the record we will use to store registrations
+            final CredentialRegistration registration = new CredentialRegistration(user, Optional.of("Nickanme"), 
+                    new TreeSet<AuthenticatorTransport>(), Instant.now(), credential, Optional.empty());
+            
+            // TODO should not need a cast here when we sort out the storage
+            if (credentialRepository instanceof final InMemoryRegistrationStorage inMemoryRepo) {
+                inMemoryRepo.addRegistrationByUsername(username, registration);
+                log.debug("{} Added public key credential registration for user '{}' and key '{}' ", 
+                        getLogPrefix(), username, registrationResult.getKeyId().getId().getBase64Url());
+            } else {
+                log.debug("{} Unsupported credential repository type", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+                return;
+            }
+        } catch (final Exception e) {
+            log.error("{} Unable to store registration for key '{}'",getLogPrefix(), 
+                    registrationResult.getKeyId().getId().getBase64Url(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        
+
+        // TODO one key per user, and overwrite the old one, this is obviously all
+        // wrong?
+//        try {
+//            final StorageRecord<Object> existing = storageService.read("webauthn-keys", username);
+//            if (existing != null) {
+//                final boolean updated = storageService.update("webauthn-keys", username,
+//                        new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
+//                if (updated) {
+//                    log.debug("Updated webauthn-keys for user {}", context.getUsername());
+//                } else {
+//                    log.error("Unable to update public key registration");
+//                    // TODO Event type is wrong
+//                    ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+//                    return;
+//                }
+//            } else {
+//                final boolean created = storageService.create("webauthn-keys", username,
+//                        new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
+//                if (created) {
+//                    log.debug("Stored webauthn-keys for user {}", context.getUsername());
+//                } else {
+//                    log.error("Unable to store public key registration");
+//                    // TODO Event type is wrong
+//                    ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+//                    return;
+//                }
+//            }
+//
+//        } catch (final IOException e) {
+//            log.error("Unable to store public key registration", e);
+//            // TODO Event type is wrong
+//            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+//            return;
+//        }
+
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
similarity index 63%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
index cee6f8f..12e3bc2 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateAuthenticatorAttestationResponse.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ValidateAuthenticatorAttestationResponse.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
 
 import javax.annotation.Nonnull;
 
@@ -23,26 +23,29 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+
 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.idp.plugin.authn.webauthn.AbstractWebAuthnRegistrationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
 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.
+ * Validate the public key registration attempt. If valid store it inside the credential repository.
  */
-public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAuthenticationAction {
+public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnRegistrationAction {
 
     /** Class logger. */
     @Nonnull
     private final Logger log = LoggerFactory.getLogger(ValidateAuthenticatorAttestationResponse.class);
     
     /** The stashed public key credential creation options used to create a new credential.*/
-    @NonnullBeforeExec @NotEmpty private String pkCredCreationOptions;
+    @NonnullBeforeExec @NotEmpty private PublicKeyCredentialCreationOptions pkCredCreationOptions;
     
     /** The stashed authenticator response.*/
     @NonnullBeforeExec @NotEmpty private String attestation;
@@ -51,8 +54,11 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAu
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext, 
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
+        
+        if (!super.doPreExecute(profileRequestContext, context)) {
+            return false;
+        }
        
         attestation = context.getAuthenticatorAttestationResponse();
         if (StringSupport.trimOrNull(attestation) == null) {
@@ -62,39 +68,30 @@ public class ValidateAuthenticatorAttestationResponse extends AbstractWebAuthnAu
         }
         
         pkCredCreationOptions = context.getPublicKeyCredentialCreationOptions();
-        if (StringSupport.trimOrNull(pkCredCreationOptions) == null) {
+        if (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);
+        return true;
     }
 
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+            @Nonnull final WebAuthnRegistrationContext context) {
 
-        // TODO this should throw the error?
-        final boolean publicKeyCredentialIsValid = 
-                getWebAuthnClient().validateRegistration(pkCredCreationOptions, attestation);
-        
-        if (!publicKeyCredentialIsValid) {            
-            log.error("{} public key credential creation options was invalid", getLogPrefix());
+        try {            
+            final RegistrationResult credentialPublicKey = 
+                getWebAuthnClient().validateAuthenticatorAttestationResponse(pkCredCreationOptions, attestation);            
+            // If valid. Add back to context
+            context.setRegistrationResult(credentialPublicKey);
+            log.info("Public Key Registration was valid");
+        } catch (final RegistrationFailureException e) {
+            log.error("{} public key credential creation options was invalid", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             return;
         }
-        
-        // If valid. Add back to context
-        log.info("Public Key Registration was valid");
-        //now set the credential data e.g. created public key and keyID onto the context.
-//        final ByteArray credId = pkCred.getResponse().getAttestation().getAuthenticatorData()
-//                .getAttestedCredentialData().get().getCredentialId();
-//        final ByteArray credPublicKey = pkCred.getResponse().getAttestation().getAuthenticatorData()
-//                .getAttestedCredentialData().get().getCredentialPublicKey();
-//        context.setCredentialId(credId.getBytes());
-//        context.setPublicKey(credPublicKey.getBytes());
     }
 
 }
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 f78bad6..a0b268e 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
@@ -26,11 +26,12 @@ import javax.annotation.concurrent.ThreadSafe;
 
 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.AssertionResult;
 import com.yubico.webauthn.FinishAssertionOptions;
 import com.yubico.webauthn.FinishRegistrationOptions;
+import com.yubico.webauthn.RegistrationResult;
 import com.yubico.webauthn.RelyingParty;
 import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
 import com.yubico.webauthn.data.ByteArray;
@@ -41,16 +42,17 @@ import com.yubico.webauthn.data.PublicKeyCredentialParameters;
 import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
 import com.yubico.webauthn.data.UserIdentity;
 import com.yubico.webauthn.data.UserVerificationRequirement;
-import com.yubico.webauthn.exception.AssertionFailedException;
 import com.yubico.webauthn.exception.RegistrationFailedException;
 
+import net.shibboleth.idp.plugin.authn.webauthn.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Yuibico implementation of a {@link WebAuthnAuthenticationClient}.
+ * Yubico implementation of a {@link WebAuthnAuthenticationClient}.
  * 
  * <p>Thread-safe, only a single instance is required.</p>
  */
@@ -92,24 +94,23 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
     }
 
     @Override
-    public String createAuthenticationRequest(@Nullable final String username, final byte[] userHandle, 
-            final byte[] challenge) throws WebAuthnAuthenticationClientException {
-        try {
-            //set default to preferred.
-            UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
-            if (username == null) {
-                //then require user verification? makes sense, but is that part of the spec?
-                userVerificationRequirement = UserVerificationRequirement.REQUIRED;
-            }
+    public PublicKeyCredentialRequestOptions createAuthenticationRequest(@Nullable final String username, 
+            final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
+ 
+        //set default to preferred.
+        UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+        if (username == null) {
+            //then require user verification? makes sense, but is that part of the spec?
+            userVerificationRequirement = UserVerificationRequirement.REQUIRED;
+        }
 //            final AssertionRequest assertion =
 //                    rp.startAssertion(StartAssertionOptions.builder()
 //                            .username(Optional.ofNullable(username))
 //                            .userVerification(userVerificationRequirement)
 //                            .build());
-            final PublicKeyCredentialRequestOptions pkcro =
-                    PublicKeyCredentialRequestOptions.builder()
-                        .challenge(new ByteArray(challenge))
-                        .rpId(rp.getIdentity().getId())
+        final PublicKeyCredentialRequestOptions request = PublicKeyCredentialRequestOptions.builder()
+                    .challenge(new ByteArray(challenge))
+                    .rpId(rp.getIdentity().getId())
 //                        .allowCredentials(
 //                            OptionalUtil.orElseOptional(
 //                                    startAssertionOptions.getUsername(),
@@ -124,38 +125,36 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
 //                            startAssertionOptions
 //                                .getExtensions()
 //                                .merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
-                        .userVerification(userVerificationRequirement)
-                        .timeout(Optional.of(60000l))
-                        .build();
-            
-            return om.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
+                    .userVerification(userVerificationRequirement)
+                    .timeout(Optional.of(60000l))
+                    .build();
+        if (request == null) {
+            throw new WebAuthnAuthenticationClientException("Unable to build public key credential request options");
         }
-        catch (final JsonProcessingException e) {
-            throw new WebAuthnAuthenticationClientException(e);
-        }        
+        return request;
+      
     }
     
     /** {@inheritDoc} */
     @Override
-    public String createRegistrationRequest(final String username, final byte[] userHandle, final byte[] challenge) 
-            throws WebAuthnAuthenticationClientException {
-        try {
-            //set default to preferred.
-            UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
-            if (username == null) {
-                //then require user verification? makes sense, but is that part of the spec?
-                userVerificationRequirement = UserVerificationRequirement.REQUIRED;
-            }
-            final UserIdentity identity = 
-                    UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
-            
-            final PublicKeyCredentialCreationOptions options =
-                    PublicKeyCredentialCreationOptions.builder()
-                        .rp(rp.getIdentity())
-                        .user(identity)
-                        .challenge(new ByteArray(challenge))
-                        .pubKeyCredParams(preferredPublickeyParams)
-                        .excludeCredentials(Optional.empty())
+    public PublicKeyCredentialCreationOptions createRegistrationRequest(final String username, 
+            final byte[] userHandle, final byte[] challenge) throws WebAuthnAuthenticationClientException {
+       
+        //set default to preferred.
+        UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+        if (username == null) {
+            //then require user verification? makes sense, but is that part of the spec?
+            userVerificationRequirement = UserVerificationRequirement.REQUIRED;
+        }
+        final UserIdentity identity = 
+                UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
+        
+        final PublicKeyCredentialCreationOptions creation = PublicKeyCredentialCreationOptions.builder()
+                    .rp(rp.getIdentity())
+                    .user(identity)
+                    .challenge(new ByteArray(challenge))
+                    .pubKeyCredParams(preferredPublickeyParams)
+                    .excludeCredentials(Optional.empty())
 //                        .excludeCredentials(
 //                            credentialRepository.getCredentialIdsForUsername(
 //                                startRegistrationOptions.getUser().getName()))
@@ -168,28 +167,23 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
 //                                        .appidExclude(appId)
 //                                        .credProps()
 //                                        .build()))
-                        .timeout(Optional.empty()).build();
-            return om.writerWithDefaultPrettyPrinter().writeValueAsString(options);
+                    .timeout(Optional.empty()).build();
+        if (creation == null) {
+            throw new WebAuthnAuthenticationClientException("Unable to build public key credential creation options");
         }
-        catch (final JsonProcessingException e) {
-            throw new WebAuthnAuthenticationClientException(e);
-        }        
+        return creation;
     }
 
     @Override
-    public boolean validateAuthenticatorAssertionResponse(@Nullable final String username, 
+    public AssertionResult validateAuthenticatorAssertionResponse(@Nullable final String username, 
             @Nullable final byte[] userHandle,
-            @Nonnull final String publicKeyCredentialRequestOptions,
-            @Nonnull final String authenticatorAssertionResponse) throws WebAuthnAuthenticationClientException {
+            @Nonnull final PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
+            @Nonnull final String authenticatorAssertionResponse) throws AssertionFailureException {
         
         try {
             
-            final PublicKeyCredentialRequestOptions request = 
-                    om.readValue(publicKeyCredentialRequestOptions, PublicKeyCredentialRequestOptions.class);
-            
             final AssertionRequest requestAssertion = AssertionRequest.builder()
-                .publicKeyCredentialRequestOptions(request)
-                // TODO these
+                .publicKeyCredentialRequestOptions(publicKeyCredentialRequestOptions)
                 .userHandle(Optional.ofNullable(userHandle != null ? new ByteArray(userHandle) : null))
                 .username(Optional.ofNullable(username))
                 .build();
@@ -198,49 +192,43 @@ public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticatio
                     PublicKeyCredential.parseAssertionResponseJson(authenticatorAssertionResponse);
             log.trace("Client Data '{}'",pkCred.getResponse().getClientData());
             log.trace("Signature '{}'",pkCred.getResponse().getSignature());
-            rp.finishAssertion(FinishAssertionOptions.builder()
+            final AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
                     .request(requestAssertion)
                     .response(pkCred)
                     .build());
+            if (result == null) {
+                throw new AssertionFailureException("Unable to validate authenticator assertion");
+            }
+            if (!result.isSuccess()) {
+                // TODO why do they use both success and throw an exception?
+                // I think this is *always* true if valid, and will throw if not valid. But just in case.
+                throw new AssertionFailureException("Authenticator assertion was not valid");
+            }
+            return result;
             
-        }  catch (final AssertionFailedException e) {
-            log.error("Public key assertion (authentication) is not valid", e);
-            return false;
-        } catch (final Exception e) {           
-            throw new WebAuthnAuthenticationClientException(e);            
-        }
-                
-        return true;
+        }  catch (final Exception e) {
+            throw new AssertionFailureException(e);
+        }                
     }
 
     /** {@inheritDoc} */
     @Override
-    public boolean validateRegistration(final String publicKeyCredentialCreationOptions, 
-            final String authenticatorAttestationResponse) {
+    public RegistrationResult validateAuthenticatorAttestationResponse(
+            @Nonnull final PublicKeyCredentialCreationOptions publicKeyCredentialCreationOptions, 
+            @Nonnull final String authenticatorAttestationResponse) throws RegistrationFailureException {
         
-        try {
-            final PublicKeyCredentialCreationOptions requestOptions = 
-                    PublicKeyCredentialCreationOptions.fromJson(publicKeyCredentialCreationOptions);
-            
-            log.debug("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
+        try {            
+            log.trace("Public Key Credential to validate '{}'", authenticatorAttestationResponse);
             final var publicKeyRegistration = 
-                    PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);
-            
+                    PublicKeyCredential.parseRegistrationResponseJson(authenticatorAttestationResponse);   
             
-            rp.finishRegistration(FinishRegistrationOptions.builder()
-                    .request(requestOptions)
+            return rp.finishRegistration(FinishRegistrationOptions.builder()
+                    .request(publicKeyCredentialCreationOptions)
                     .response(publicKeyRegistration)
                     .build());
-            // TODO do we need the result?
+
         } catch (final RegistrationFailedException | IOException e) {
-           log.error("Public key attestation (registration) is not valid", e);
-           // Should throw this?
-           return false;
+           throw new RegistrationFailureException(e);
         }
-        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 a03463d..522c09c 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
@@ -19,15 +19,15 @@ import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
 
-import org.springframework.beans.factory.BeanInitializationException;
 import org.springframework.beans.factory.FactoryBean;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.CredentialRepository;
 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.InMemoryRegistrationStorage;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
@@ -35,7 +35,6 @@ import net.shibboleth.shared.logic.Constraint;
 /**
  * Spring factory beans for creating a {@link YubicoWebauthnAuthenticationClient}.
  */
-//TODO do we need a factory abstract for this type of initialisation e.g. not runtime.
 @ThreadSafe
 public class YubicoWebauthnClientFactory extends AbstractInitializableComponent 
             implements FactoryBean<WebAuthnAuthenticationClient> {
@@ -53,7 +52,11 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
     @GuardedBy("this") private boolean allowOriginSubdomain;
     
     /** The JSON object mapper used to JSONify webauthn objects. */
-    @GuardedBy("this") @Nullable private ObjectMapper om;
+    @GuardedBy("this") @Nullable private ObjectMapper om;    
+    
+    /** The credential repository to store valid credentials in.*/
+    // TODO replace with an adaptor to the storage service?
+    @GuardedBy("this") @NonnullAfterInit private CredentialRepository credentialRepository;
     
     /** Constructor.*/
     public YubicoWebauthnClientFactory() {
@@ -73,10 +76,12 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
         if (om ==  null) {
             throw new ComponentInitializationException("ObjectMapper cannot be null");
         }
+        if (credentialRepository == null) {
+            throw new ComponentInitializationException("Credential repository cannot be null");
+        }
 
     }
     
-    
     @Override
     public WebAuthnAuthenticationClient getObject() throws Exception {
         final RelyingParty rp = RelyingParty.builder().identity(
@@ -85,18 +90,35 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
                 .id(getRelyingPartyId())
                 .name(getRelyingPartyName())
                 //Use inmemory for now
-                .build()).credentialRepository(new InMemoryRegistrationStorage())
+                .build()).credentialRepository(getCredentialRepository())
         .allowOriginPort(isAllowOriginPort())
         .allowOriginSubdomain(isAllowOriginSubdomain())
         .build();
         
         assert rp != null;
-        final ObjectMapper localMapper =  getObjectMapper();
-        if (localMapper == null) {
-            // Should not happen after init
-            throw new BeanInitializationException("Object mapper can not be null");
-        }        
-        return new YubicoWebauthnAuthenticationClient(rp,localMapper);
+        return new YubicoWebauthnAuthenticationClient(rp, getObjectMapper());
+    }
+    
+    /**
+     * Get the credential repository used to store the valid webauthn credential.
+     * 
+     * @return the credential repository.
+     */
+    @Nonnull public synchronized CredentialRepository getCredentialRepository() {
+        checkComponentActive();
+        assert credentialRepository != null;
+        return credentialRepository;
+    }
+    
+    
+    /**
+     * Set the credential repository used to store the valid webauthn credential.
+     *  
+     * @param repository The credential repository to set.
+     */
+    public synchronized void setCredentialRepository(@Nonnull final CredentialRepository repository) {
+        checkSetterPreconditions();
+        credentialRepository = Constraint.isNotNull(repository, "Credential respository can not be null");
     }
 
     @Override
@@ -124,8 +146,9 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
      * 
      * @return the objectMapper;
      */
-    private synchronized ObjectMapper getObjectMapper() {
+    @Nonnull private synchronized ObjectMapper getObjectMapper() {
         checkComponentActive();
+        assert om != null;
         return om;
     }
     /**
@@ -211,8 +234,6 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
         checkSetterPreconditions(); 
         allowOriginSubdomain = allow;
     }
-
-
-    
+ 
 
 }
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 12ed08e..5e6c42f 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
@@ -21,12 +21,19 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
 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.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -37,6 +44,31 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialRequestOptions.class);
     
+    //TODO move out where we do this
+    /** The JSON object mapper used to JSONify webauthn objects. */
+    @NonnullAfterInit private ObjectMapper jsonObjectMapper;
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (jsonObjectMapper == null) {
+            throw new ComponentInitializationException("JSON Object Mapper can not be null");
+        }
+    }
+    
+    
+    /**
+     * Set the JSON object mapper to use.
+     * 
+     * @param mapper The jsonObjectMapper to set.
+     */
+    public void setJsonObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        jsonObjectMapper = Constraint.isNotNull(mapper, "JsonObjectMapper can not be null");
+    }  
+    
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext, 
@@ -53,13 +85,14 @@ public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAut
                 
         try {
             //TODO userhandle needs to be pulled out.
-            final String pkCredRequestOptions = client.createAuthenticationRequest(context.getUsername(), 
-                    null, challenge);
-            //verify correct JSON response?
+            final PublicKeyCredentialRequestOptions pkCredRequestOptions = 
+                    client.createAuthenticationRequest(context.getUsername(), null, challenge);
             context.setPublicKeyCredentialRequestOptions(pkCredRequestOptions);
             
+            context.setPublicKeyCredentialRequestOptionsJSON(jsonObjectMapper.writeValueAsString(pkCredRequestOptions));
+            
             log.debug("{} Created PublicKeyCredentialRequestOptions: '{}'",getLogPrefix(), pkCredRequestOptions);
-        } catch (final WebAuthnAuthenticationClientException e) {
+        } catch (final WebAuthnAuthenticationClientException | JsonProcessingException e) {
             log.error("{} Unable to generate PublicKeyCredentialRequestOptions",getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
             return;
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
similarity index 81%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
index e55c766..df8fc73 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractAuthenticatorAssertionFromFormRequest.java
@@ -40,15 +40,15 @@ import net.shibboleth.shared.primitive.StringSupport;
 
 
 /**
- * An action that derives the PublicKeyAssertion from a form parameter.
+ * An action that extracts the AuthenticatorAssertionResponse from the incoming HTTP request.
  */
-public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+public class ExtractAuthenticatorAssertionFromFormRequest extends AbstractWebAuthnAuthenticationAction {
 
     /** Default token code field name. */
     @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyAssertion";
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyAssertionFromFormRequest.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractAuthenticatorAssertionFromFormRequest.class);
     
     /** Name of header. */
     @NonnullAfterInit @NotEmpty private String fieldName;
@@ -57,7 +57,7 @@ public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAu
     @NonnullAfterInit private ObjectMapper objectMapper;
     
     /** Constructor. */
-    public ExtractPublicKeyAssertionFromFormRequest() {       
+    public ExtractAuthenticatorAssertionFromFormRequest() {       
         fieldName = DEFAULT_FIELD_NAME;
     }
     
@@ -103,18 +103,26 @@ public class ExtractPublicKeyAssertionFromFormRequest extends AbstractWebAuthnAu
             return;
         }
         
-        final String pkCredJson = extractAuthenticatorResponse(request);   
-        log.trace("Public Key Assertion in JSON is '{}'",pkCredJson);
+        final String pkCredJson = extractAuthenticatorAssertionResponse(request);         
         if (pkCredJson == null) {
+            log.debug("Public key assertion not found in HTTP request: '{}'",pkCredJson);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             return;
-        }                
+        }   
+        log.trace("Public key assertion found: '{}'",pkCredJson);
        
         context.setAuthenticatorAssertionResponse(pkCredJson);
 
     }
 
-    @Nullable private String extractAuthenticatorResponse(@Nonnull final HttpServletRequest httpRequest) {
+    /**
+     * Extract the public key assertion response from the HTTP request parameters.
+     * 
+     * @param httpRequest the http request
+     * 
+     * @return the raw, unformatted, authenticator assertion response
+     */
+    @Nullable private String extractAuthenticatorAssertionResponse(@Nonnull final HttpServletRequest httpRequest) {
         return httpRequest.getParameter(fieldName);
     }
     
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
index bd874f2..a6b8490 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
@@ -19,17 +19,21 @@ package net.shibboleth.idp.plugin.authn.webauthn.impl;
 
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
-import net.shibboleth.idp.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.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
@@ -41,26 +45,60 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
  * @post See above.
  */
-public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+public class GenerateServerChallenge extends AbstractProfileAction {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+    
+    /** Lookup strategy to locate the WebAuthn base context. */
+    @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnBaseContextLookupStrategy;
+    
+    @NonnullBeforeExec private BaseWebAuthnContext context;
+    
+    /** Constructor.*/
+    protected GenerateServerChallenge() {
+        //prc -> ac -> base context
+        webAuthnBaseContextLookupStrategy = new ChildContextLookup<>(BaseWebAuthnContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
+    }
+    
+    /**
+     * @param webauthnBaseContextLookupStrategy The webauthnBaseContextLookupStrategy to set.
+     */
+    public void setWebAuthnBaseContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, BaseWebAuthnContext> strategy) {
+        checkSetterPreconditions();
+        webAuthnBaseContextLookupStrategy = Constraint.isNotNull(strategy,
+                "WebAuthnBaseContextLookupStrategy can not be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        context = webAuthnBaseContextLookupStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.warn("{} WebAuthnBaseContext is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        return true;
+    }
    
 
     /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
         try {
-            //TODO check the spec, this should be a a buffersource, maybe the JS is generating it as one 
             final byte[] challenge = generateChallenge();
             log.trace("Generated challenge {}",challenge);
             context.setServerChallenge(challenge);
             
         } catch (final NoSuchAlgorithmException e) {
             log.error("Could not generate a challenge",e);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
         }        
     }
     
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
similarity index 96%
rename from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
rename to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebAuthnAuthenticationContext.java
index 9bccc42..9849f6d 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
@@ -44,10 +44,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
  * @post See above.
  */
-public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+public class PopulateWebAuthnAuthenticationContext extends AbstractAuthenticationAction {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebAuthnAuthenticationContext.class);
     
     /** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
     @Nonnull 
@@ -61,7 +61,7 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
     
 
     /** Constructor.*/
-    public PopulateWebauthnAuthenticationContext() {
+    public PopulateWebAuthnAuthenticationContext() {
         //default creates webauthn authentication context under authentication context.
         webauthnAuthContextCreationStrategy =
                 new ChildContextLookup<>(WebAuthnAuthenticationContext.class, true).
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
index e76de02..293eb15 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -10,12 +10,15 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
+
 import net.shibboleth.idp.authn.AbstractValidationAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+import net.shibboleth.idp.plugin.authn.webauthn.AssertionFailureException;
 import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
-import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClientException;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
@@ -24,7 +27,9 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * See https://w3c.github.io/webauthn/#sctn-verifying-assertion
+ * An action that validates a WebAuthn Authenticator Assertion that results from a call to 'get' (authentication).
+ * 
+ * @see https://w3c.github.io/webauthn/#sctn-verifying-assertion
  */
 public class ValidateWebAuthnAssertion extends AbstractValidationAction {
     
@@ -43,7 +48,7 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
     @NonnullAfterInit private WebAuthnAuthenticationClient webAuthnClient;    
     
     /** The options used to create the authentication request.*/
-    @NonnullBeforeExec private String publicKeyCredentialRequestOptions;
+    @NonnullBeforeExec private PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions;
     
     /** Constructor. */
     public ValidateWebAuthnAssertion() {
@@ -100,7 +105,9 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
         
         final String assertion = context.getAuthenticatorAssertionResponse();
         
+        // TODO these need to be set from lookup earlier in the context
         context.setUsername("not-the-username");
+        context.setUserHandle("notahandle".getBytes());
         
         if (assertion == null) {
             log.warn("{} No authenticator assertion found, {} can not authenticate ", 
@@ -111,19 +118,12 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
             return; 
         }
         try {
-            final boolean valid = webAuthnClient.validateAuthenticatorAssertionResponse(context.getUsername(), 
-                    null, publicKeyCredentialRequestOptions, assertion);
-            if (!valid) {
-                log.warn("{} Authenticator assertion was not valid for '{}', authentication failed", 
-                        getLogPrefix(),context.getUsername());
-                handleError(profileRequestContext, authenticationContext, AuthnEventIds.NO_CREDENTIALS, 
-                        AuthnEventIds.INVALID_CREDENTIALS);
-                recordFailure(profileRequestContext);              
-                return; 
-            } else {
-                log.debug("{} Authenticator assertion was valid for '{}'", getLogPrefix(), context.getUsername());
-            }
-        } catch (final WebAuthnAuthenticationClientException e) {
+            final AssertionResult result = webAuthnClient.validateAuthenticatorAssertionResponse(
+                    context.getUsername(), context.getUserHandle(), publicKeyCredentialRequestOptions, assertion);
+            
+            buildAuthenticationResult(profileRequestContext, authenticationContext);
+            
+        } catch (final AssertionFailureException e) {
             log.warn("{} Error validating authenticator assertion for '{}'", 
                     getLogPrefix(),context.getUsername(), e);
             handleError(profileRequestContext, authenticationContext, "InvalidResponseType",
@@ -131,8 +131,6 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
             recordFailure(profileRequestContext);              
             return; 
         }
-
-        buildAuthenticationResult(profileRequestContext, authenticationContext);
         
     }
     
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java
new file mode 100644
index 0000000..91ecf95
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/CredentialPublicKeyHolder.java
@@ -0,0 +1,91 @@
+/*
+ * 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.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.plugin.authn.webauthn.CredentialPublicKey;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A holder for the {@link CredentialPublicKey} which includes additional information required for storage of the 
+ * public key information.
+ */
+public class CredentialPublicKeyHolder {
+    
+    /** 
+     * The credential public key component of the credential key pair created by an authenticator during 
+     * registration.
+     */
+    @Nonnull private final CredentialPublicKey credentialPublicKey;
+    
+    @Nonnull private final Instant registrationTime;
+    
+    @Nonnull private final String username;
+    
+    /** The user ID as specified by the IdP that maps a credential public key to a user. */
+    @Nonnull private final byte[] userHandle;
+    
+    //SortedSet<AuthenticatorTransport> transports;
+    
+    //Optional<Object> attestationMetadata;
+    
+    
+    /**
+     * Constructor.
+     *
+     * @param publicKey the credential public key
+     */
+    public CredentialPublicKeyHolder(@Nonnull final CredentialPublicKey publicKey, 
+            @Nonnull final Instant regTime, @Nonnull @NotEmpty final String uname,
+            @Nonnull final byte[] id) {
+        credentialPublicKey = Constraint.isNotNull(publicKey, "Credential public key can not be null");
+        registrationTime =  Constraint.isNotNull(regTime, "Registration time can not be null");
+        username = Constraint.isNotEmpty(uname, "Username can not be null or empty");
+        userHandle = Constraint.isNotEmpty(id, "UserHandle (ID) can not be null or empty");
+    }
+    
+    /**
+     * Get the credential public key.
+     * 
+     * @return Returns the credentialPublicKey.
+     */
+    public CredentialPublicKey getCredentialPublicKey() {
+        return credentialPublicKey;
+    }
+    
+    @Nonnull public String getRegistrationTimestamp() {
+        return registrationTime.toString();
+    }
+    
+    @Nonnull public byte[] getId() {
+        return userHandle;
+    }
+
+    @Nonnull public String getUsername() {
+        return username;
+    }
+
+    @Nonnull public CredentialPublicKey getCredential() {
+        return credentialPublicKey;
+    }
+    
+//    public SortedSet<AuthenticatorTransport> getTransports() {
+//        return transports;
+//    }
+}
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
index 9c70e3f..1a0a0cc 100644
--- 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
@@ -87,5 +87,6 @@ public class CredentialRegistration {
         newReg.credential = newRegCred;
         return newReg;
     }
+    
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
deleted file mode 100644
index 5952ac9..0000000
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * 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.io.IOException;
-
-import javax.annotation.Nonnull;
-
-import org.apache.commons.codec.binary.Hex;
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.storage.StorageCapabilities;
-import org.opensaml.storage.StorageRecord;
-import org.opensaml.storage.StorageSerializer;
-import org.opensaml.storage.StorageService;
-import org.slf4j.Logger;
-
-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.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
-import net.shibboleth.shared.logic.Constraint;
-import net.shibboleth.shared.primitive.LoggerFactory;
-
-//TODO extend appropriate class to get WebauthnAuthenticationContext
-public class StorePublicKeyCredential extends AbstractWebAuthnAuthenticationAction {
-
-    /** Class logger. */
-    @Nonnull
-    private final Logger log = LoggerFactory.getLogger(StorePublicKeyCredential.class);
-
-    /** Backing service. */
-    @NonnullAfterInit
-    private StorageService storageService;
-
-    /** Storage record serializer. */
-    @Nonnull
-    private final StorageSerializer<WebAuthnPublicKeyCredentialRecord> serializer;
-
-    /** Constructor. */
-    public StorePublicKeyCredential() {
-        serializer = new WebauthnPublicKeyCredentialStorageSerializer();
-    }
-
-    /**
-     * Set the {@link StorageService} back-end to use.
-     * 
-     * @param storage
-     *            the back-end to use
-     */
-    public void setStorageService(@Nonnull final StorageService storage) {
-        checkSetterPreconditions();
-
-        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
-        final StorageCapabilities caps = storageService.getCapabilities();
-        Constraint.isTrue(caps.isServerSide(), "StorageService cannot be client-side");
-    }
-
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-
-        if (storageService == null) {
-            throw new ComponentInitializationException("StorageService cannot be null");
-        }
-    }
-
-    // TODO maybe not needed.
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
-        return true;
-    }
-
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext,
-            @Nonnull final WebAuthnAuthenticationContext context) {
-
-        final String username = context.getUsername();
-        final byte[] publicKey = context.getPublicKey();
-        if (username == null) {
-            log.error("Unable to find username in registration response");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return;
-        }
-        if (publicKey == null) {
-            log.error("Unable to find public key in registration response");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return;
-        }
-        final byte[] credentialId = context.getCredentialId();
-        if (credentialId == null) {
-            log.error("Unable to find credential Id in registration response");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return;
-        }
-        final String hexEncodedCredentialId = Hex.encodeHexString(credentialId);
-        if (hexEncodedCredentialId == null) {
-            log.error("Unable to encode credential Id in registration response");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return;
-        }
-
-        // TODO one key per user, and overwrite the old one, this is obviously all
-        // wrong?
-        try {
-            final StorageRecord<Object> existing = storageService.read("webauthn-keys", username);
-            if (existing != null) {
-                final boolean updated = storageService.update("webauthn-keys", username,
-                        new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
-                if (updated) {
-                    log.debug("Updated webauthn-keys for user {}", context.getUsername());
-                } else {
-                    log.error("Unable to update public key registration");
-                    // TODO Event type is wrong
-                    ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-                    return;
-                }
-            } else {
-                final boolean created = storageService.create("webauthn-keys", username,
-                        new WebAuthnPublicKeyCredentialRecord(publicKey, credentialId), serializer, null);
-                if (created) {
-                    log.debug("Stored webauthn-keys for user {}", context.getUsername());
-                } else {
-                    log.error("Unable to store public key registration");
-                    // TODO Event type is wrong
-                    ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-                    return;
-                }
-            }
-
-        } catch (final IOException e) {
-            log.error("Unable to store public key registration", e);
-            // TODO Event type is wrong
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
-            return;
-        }
-
-    }
-
-}
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 6498db1..89ffa59 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -83,15 +83,22 @@
          </property>
     </bean>
     
-     <!-- Singleton clients -->
+     <!-- Singleton clients and repositories -->
+     <!--  TODO configure these with getbeans and properties -->
     <bean id="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" scope="singleton"
         class="net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory"
         p:relyingPartyId="%{idp.authn.webauthn.relyingPartyId}" 
         p:relyingPartyName="Shibboleth"
         p:allowOriginPort ="%{idp.authn.webauthn.allowOriginPort:false}"
         p:allowOriginSubdomain ="%{idp.authn.webauthn.allowOriginSubdomain:false}"
-        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
-        
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" 
+        p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository"/>
+    
+    <!--  TODO replace with our own -->
+    <bean id="shibboleth.authn.webauthn.DefaultCredentialRepository" scope="singleton"
+        class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage"/>
+    
+           
      <!-- 
      
         Create a default object mapper. Setup should not change once injected.
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
index 77f0484..4734659 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -12,35 +12,46 @@
     <bean id="shibboleth.AdminProfileId" class="java.lang.String"
         c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
 
-    <bean id="PopulateWebauthnAuthenticationContext" scope="prototype" 
-        parent="AbstractPopulateWebauthnAuthenticationContext"
-        p:usernameRequiredPredicate="true">
+    <!-- TODO Should this been populating an authentication context for an admin flow? -->
+    <bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
         <property name="usernameLookupStrategy">
-            <bean id="UsernameFromAuthenticationContextLookupStrategy" 
-            class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy"/>
+            <bean id="UsernameFromAuthenticationContextLookupStrategy"
+                class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy" />
         </property>
     </bean>
     
-    <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnAuthenticationAction"
-        class=" net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialCreationOptions"/>
-        
-    <bean id="ExtractPublicKeyCredentialFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialFromFormRequest"
+    <bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
+
+    <bean id="GenerateServerChallenge" parent="AbstractGenerateServerChallenge"
+        p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"/>
+
+
+    <bean id="GenerateUserHandle" parent="AbstractWebAuthnAuthenticationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.GenerateUserHandle" />
+
+    <bean id="CreatePublicKeyCredentialCreationOptions" parent="AbstractWebAuthnRegistrationAction"
+        class=" net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CreatePublicKeyCredentialCreationOptions" />
+
+    <bean id="ExtractAuthenticatorAttestationFromFormRequest" parent="AbstractWebAuthnRegistrationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractAuthenticatorAttestationFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
         p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
-        
-        
-    <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnAuthenticationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateAuthenticatorAttestationResponse" />
-        
-        
-    <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnAuthenticationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.StorePublicKeyCredential" 
-        p:storageService-ref="shibboleth.authn.webauthn.StorageService"/>
-
-    <!--  postconfig.xml? -->
+
+
+    <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
+
+
+    <bean id="StorePublicKeyCredential" parent="AbstractWebAuthnRegistrationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.StorePublicKeyCredential"
+        p:storageService-ref="shibboleth.authn.webauthn.StorageService"
+        p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository" />
+
+    <!-- postconfig.xml? -->
     <bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
-        class="org.opensaml.storage.impl.MemoryStorageService"
-        p:cleanupInterval="PT10M" />
-        
+        class="org.opensaml.storage.impl.MemoryStorageService" p:cleanupInterval="PT10M" />
+
 </beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
index 6d86fb1..ecbef86 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -24,8 +24,9 @@
     </action-state>   
     
      <action-state id="PopulateWebAuthnContext">
-        <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+        <evaluate expression="PopulateWebAuthnRegistrationContext"/>
         <evaluate expression="GenerateServerChallenge"/>
+        <evaluate expression="GenerateUserHandle"/>
         <evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="DisplayWebAuthnView" />    
@@ -36,7 +37,7 @@
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
-            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="viewScope.webauthnContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext))" result="viewScope.webauthnRegContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="viewScope.authenticationErrorContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="viewScope.authenticationWarningContext" />
@@ -49,8 +50,8 @@
     </view-state>
     
    <action-state id="ExtractPublicKeyCredential">
-        <evaluate expression="ExtractPublicKeyCredentialFromFormRequest"/>
-        <evaluate expression="ValidatePublicKeyCredential"/>
+        <evaluate expression="ExtractAuthenticatorAttestationFromFormRequest"/>
+        <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
         <evaluate expression="StorePublicKeyCredential"/>
         
         <evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
index 338d3d2..8e6b455 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
@@ -9,15 +9,15 @@
 
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <!-- Parent beans -->
-    <bean id="AbstractPopulateWebauthnAuthenticationContext" scope="prototype" abstract="true"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext"/>
-        
+    <!-- Parent beans -->        
     <bean id="AbstractWebAuthnAuthenticationAction" scope="prototype" abstract="true"
         p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
+        
+    <bean id="AbstractWebAuthnRegistrationAction" scope="prototype" abstract="true"
+        p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
 
     <!-- Generic beans -->
-    <bean id="GenerateServerChallenge" parent="AbstractWebAuthnAuthenticationAction"
+    <bean id="AbstractGenerateServerChallenge" scope="prototype" abstract="true"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge" />
 
     
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 24012c2..6a99cc6 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -9,16 +9,27 @@
 
     default-init-method="initialize" default-destroy-method="destroy">
 
-     <bean id="PopulateWebauthnAuthenticationContext" scope="prototype" 
-        parent="AbstractPopulateWebauthnAuthenticationContext"
+     <bean id="PopulateWebAuthnAuthenticationContext" scope="prototype" 
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext"
         p:usernameRequiredPredicate="false">
     </bean>
     
+    <bean id="shibboleth.ChildLookup.WebAuthnAuthenticationContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext) }" />
+
+    <bean id="GenerateServerChallenge" parent="AbstractGenerateServerChallenge">
+        <property name="webAuthnBaseContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
+                c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+        </property>
+    </bean>    
+    
     <bean id="CreatePublicKeyCredentialRequestOptions" parent="AbstractWebAuthnAuthenticationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"/>        
         
-    <bean id="ExtractPublicKeyAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyAssertionFromFormRequest"
+    <bean id="ExtractAuthenticatorAssertionFromFormRequest" parent="AbstractWebAuthnAuthenticationAction"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractAuthenticatorAssertionFromFormRequest"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
         p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
 
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index 5942fec..dc26b5c 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -3,7 +3,7 @@
     parent="authn.abstract, authn/conditions">
 
     <action-state id="PopulateWebauthnContext">
-        <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+        <evaluate expression="PopulateWebAuthnAuthenticationContext"/>
         <evaluate expression="GenerateServerChallenge"/>
         <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
         <evaluate expression="'proceed'" />
@@ -29,7 +29,7 @@
     </view-state>
      
     <action-state id="AuthenticatePublicKeyCredential">
-        <evaluate expression="ExtractPublicKeyAssertionFromFormRequest"/>
+        <evaluate expression="ExtractAuthenticatorAssertionFromFormRequest"/>
         <evaluate expression="ValidateWebAuthnAssertion"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="proceed" />
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 69ddf0d..d07a1da 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
@@ -14,7 +14,7 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
 
-import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
 import java.time.Instant;
@@ -29,7 +29,9 @@ import org.slf4j.Logger;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.yubico.webauthn.AssertionResult;
 import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.RegistrationResult;
 import com.yubico.webauthn.RelyingParty;
 import com.yubico.webauthn.data.AuthenticatorTransport;
 import com.yubico.webauthn.data.ByteArray;
@@ -39,6 +41,8 @@ import com.yubico.webauthn.data.RelyingPartyIdentity;
 import com.yubico.webauthn.data.UserIdentity;
 import com.yubico.webauthn.data.UserVerificationRequirement;
 
+import net.shibboleth.idp.plugin.authn.webauthn.AssertionFailureException;
+import net.shibboleth.idp.plugin.authn.webauthn.RegistrationFailureException;
 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;
@@ -72,9 +76,9 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
     
     private YubicoWebauthnAuthenticationClient client;   
     
-    private String credentialCreationOptions;
+    private PublicKeyCredentialCreationOptions credentialCreationOptions;
     
-    private String credentialRequestOptions;
+    private PublicKeyCredentialRequestOptions credentialRequestOptions;
     
     
     
@@ -105,7 +109,7 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                 UserIdentity.builder().name(USERNAME).displayName("test user")
                 .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
         
-        final PublicKeyCredentialCreationOptions options =
+        credentialCreationOptions =
                 PublicKeyCredentialCreationOptions.builder()
                     .rp(rp.getIdentity())
                     .user(userIdentity)
@@ -113,11 +117,8 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                     .pubKeyCredParams(preferredPublickeyParams)
                     .excludeCredentials(Optional.empty())
                     .timeout(Optional.empty()).build();
-        credentialCreationOptions = 
-                jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
-        
-        
-        final PublicKeyCredentialRequestOptions pkcro =
+
+        credentialRequestOptions =
                 PublicKeyCredentialRequestOptions.builder()
                     .challenge(new ByteArray(Base64Support.decode(CHALLENGE_B64)))
                     .rpId(rp.getIdentity().getId())
@@ -125,8 +126,6 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                     .timeout(Optional.of(60000l))
                     .build();
         
-        credentialRequestOptions = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(pkcro);
-        
     }
     
 
@@ -142,13 +141,14 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                         Base64Support.decode(USER_HANDLE_B64));
         
         final var attestationJson = jsonMapper.writeValueAsString(attestation);
-        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+        final RegistrationResult registration = 
+                client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
         
-        assertTrue(registrationIsValid);
+        assertNotNull(registration);
 
     }
     
-    @Test
+    @Test(expectedExceptions = RegistrationFailureException.class)
     public void testValidateRegistration_Fail_BadRpId() throws Exception {   
         
         mockAuthenticator = new MockAuthenticator("wrong-rpid.example.com");
@@ -160,12 +160,13 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                         Base64Support.decode(USER_HANDLE_B64));
         
         final var attestationJson = jsonMapper.writeValueAsString(attestation);
-        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+        final RegistrationResult registration = 
+                client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
         
-        assertFalse(registrationIsValid);
+        assertNotNull(registration);
     }
     
-    @Test
+    @Test(expectedExceptions = RegistrationFailureException.class)
     public void testValidateRegistrationFail_BadOrigin() throws Exception {   
         
         mockAuthenticator = new MockAuthenticator(RPID);
@@ -177,9 +178,9 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
                         Base64Support.decode(USER_HANDLE_B64));
         
         final var attestationJson = jsonMapper.writeValueAsString(attestation);
-        final boolean registrationIsValid = client.validateRegistration(credentialCreationOptions, attestationJson);
+        final RegistrationResult registration = 
+                client.validateAuthenticatorAttestationResponse(credentialCreationOptions, attestationJson);
 
-        assertFalse(registrationIsValid);
     }
     
     @Test
@@ -213,14 +214,15 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
         
         final var assertionJson = jsonMapper.writeValueAsString(assertion);
         log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
-        final boolean valid = 
+        final AssertionResult result = 
                 client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
                 credentialRequestOptions, assertionJson);
-        assertTrue(valid);
+        assertNotNull(result);
+        assertTrue(result.isSuccess());
         
     }
     
-    @Test
+    @Test(expectedExceptions = AssertionFailureException.class)
     public void testValidateAuthentication_Fail_WrongOrigin() throws Exception {   
         
         mockAuthenticator = new MockAuthenticator(RPID);
@@ -252,14 +254,12 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
         
         final var assertionJson = jsonMapper.writeValueAsString(assertion);
         log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
-        final boolean valid = 
-                client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
+        client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
                 credentialRequestOptions, assertionJson);
-        assertFalse(valid);
         
     }
     
-    @Test
+    @Test(expectedExceptions = AssertionFailureException.class)
     public void testValidateAuthentication_Fail_WrongOperationType() throws Exception {   
         
         mockAuthenticator = new MockAuthenticator(RPID);
@@ -291,14 +291,12 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
         
         final var assertionJson = jsonMapper.writeValueAsString(assertion);
         log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
-        final boolean valid = 
-                client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
+        client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
                 credentialRequestOptions, assertionJson);
-        assertFalse(valid);
-        
+           
     }
     
-    @Test
+    @Test(expectedExceptions = AssertionFailureException.class)
     public void testValidateAuthentication_Fail_BadSignature_DifferentKey() throws Exception {   
         
         mockAuthenticator = new MockAuthenticator(RPID);
@@ -331,10 +329,8 @@ public class YubicoWebauthnAuthenticationClientTest extends AbstractWebAuthnTest
         
         final var assertionJson = jsonMapper.writeValueAsString(assertion);
         log.debug("AuthenticatorAssertionResponse: '{}'",assertionJson);
-        final boolean valid = 
-                client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
+        client.validateAuthenticatorAssertionResponse(USERNAME, Base64Support.decode(USER_HANDLE_B64), 
                 credentialRequestOptions, assertionJson);
-        assertFalse(valid);
         
     }
     
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 8abc3cb..5e03116 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
@@ -32,6 +32,7 @@ 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.context.WebAuthnRegistrationContext;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.idp.profile.testing.RequestContextBuilder;
 
@@ -47,6 +48,9 @@ public abstract class AbstractWebAuthnTest {
     /** The webauthn authentication context.*/
     protected WebAuthnAuthenticationContext webAuthnContext;
     
+    /** The webauthn registration context.*/
+    protected WebAuthnRegistrationContext webAuthnRegContext;
+    
     /** The authentication context.*/
     protected AuthenticationContext ac;
     
@@ -94,6 +98,9 @@ public abstract class AbstractWebAuthnTest {
         prc.addSubcontext(ac);
         assert null != webAuthnContext;
         ac.addSubcontext(webAuthnContext);
+        
+        webAuthnRegContext = new WebAuthnRegistrationContext();
+        prc.addSubcontext(webAuthnRegContext);
     }  
     
     
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 9634247..8b6a85c 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
@@ -26,6 +26,7 @@ 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.admin.impl.ValidateAuthenticatorAttestationResponse;
 import net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
 import net.shibboleth.shared.codec.Base64Support;
@@ -62,7 +63,7 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
                 UserIdentity.builder().name("test-user").displayName("test user")
                 .id(new ByteArray(Base64Support.decode(USER_HANDLE_B64))).build();
         
-        final PublicKeyCredentialCreationOptions options =
+        final PublicKeyCredentialCreationOptions credentialCreationOptions =
                 PublicKeyCredentialCreationOptions.builder()
                     .rp(rp.getIdentity())
                     .user(identity)
@@ -70,9 +71,8 @@ public class ValidatePublicKeyCredentialTest extends AbstractWebAuthnTest{
                     .pubKeyCredParams(preferredPublickeyParams)
                     .excludeCredentials(Optional.empty())
                     .timeout(Optional.empty()).build();
-        final String credentialCreationOptions = 
-                jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(options);
-        webAuthnContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
+
+        webAuthnRegContext.setPublicKeyCredentialCreationOptions(credentialCreationOptions);
                 
         final WebAuthnAuthenticationClient client = new YubicoWebauthnAuthenticationClient(rp, jsonMapper);
         validator.setWebAuthnClient(client);

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


More information about the commits mailing list