[java-idp-plugin-webauthn] branch main updated: Add admin flow and authentication flow project structure

Phil Smart philip.smart at jisc.ac.uk
Wed Nov 1 17:48:30 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=51ca8dda068d8557fc238fb4587f5b06a27039be

The following commit(s) were added to refs/heads/main by this push:
     new 51ca8dd  Add admin flow and authentication flow project structure
51ca8dd is described below

commit 51ca8dda068d8557fc238fb4587f5b06a27039be
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Nov 1 17:48:23 2023 +0000

    Add admin flow and authentication flow project structure
    
     - Authentication flow chooser requires MFA configuration to operate
---
 pom.xml                                            |  53 ++++--
 webauthn-api/pom.xml                               |  25 +++
 .../AbstractWebAuthnAuthenticationAction.java      | 148 +++++++++++++++
 .../plugin/authn/webauthn/PublicKeyCredential.java |  78 ++++++++
 .../idp/plugin/authn/webauthn/Response.java        |  64 +++++++
 .../webauthn/WebauthnAuthenticationClient.java     |  39 ++++
 .../WebauthnAuthenticationClientException.java     |  51 +++++
 .../WebauthnAuthenticationClientFactory.java       |  20 ++
 ...bAuthnCredentialRegistrationFlowDescriptor.java |  39 ++++
 .../context/WebAuthnAuthenticationContext.java     | 200 ++++++++++++++++++++
 .../context/WebAuthnPublicKeyCredentialRecord.java |  49 +++++
 webauthn-impl/pom.xml                              |  62 +++++-
 .../impl/YubicoWebauthnAuthenticationClient.java   | 100 ++++++++++
 .../client/impl/YubicoWebauthnClientFactory.java   | 208 +++++++++++++++++++++
 .../CreatePublicKeyCredentialRequestOptions.java   |  72 +++++++
 .../ExtractPublicKeyAssertionFromFormRequest.java  | 138 ++++++++++++++
 .../ExtractPublicKeyCredentialFromFormRequest.java | 139 ++++++++++++++
 .../webauthn/impl/GenerateServerChallenge.java     |  82 ++++++++
 .../PopulateWebauthnAuthenticationContext.java     | 127 +++++++++++++
 ...ameFromAuthenticationContextLookupStrategy.java |  55 ++++++
 .../webauthn/impl/ValidatePublicKeyCredential.java |  73 ++++++++
 .../webauthn/impl/ValidateWebAuthnAssertion.java   |  81 ++++++++
 .../storage/impl/DummyCredentialRepository.java    |  66 +++++++
 .../storage/impl/StorePublicKeyCredential.java     | 161 ++++++++++++++++
 ...bauthnPublicKeyCredentialStorageSerializer.java |  59 ++++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  42 +++++
 .../webauthn-registration-beans.xml                |  43 +++++
 .../webauthn-registration-flow.xml                 |  78 ++++++++
 .../authn/WebAuthn/webauthn-abstract-beans.xml     |  54 ++++++
 .../idp/flows/authn/WebAuthn/webauthn-beans.xml    |  30 ++-
 .../idp/flows/authn/WebAuthn/webauthn-flow.xml     |  38 +++-
 .../authn/WebAuthnFlowChooser/webauthn-flow.xml    |  18 ++
 .../idp/plugin/authn/webauthn/js/webauthn-shib.js  | 163 ++++++++++++++++
 .../plugin/authn/webauthn/js/webauthn-yubico.js    | 171 +++++++++++++++++
 .../plugin/authn/webauthn/views/webauthn-authn.vm  |  72 +++++++
 .../authn/webauthn/views/webauthn-register.vm      |  73 ++++++++
 .../authn/webauthn/views/webauthn-registered.vm    |  57 ++++++
 .../authn/webauthn/views/webauthn-selector.vm      |  66 +++++++
 38 files changed, 3059 insertions(+), 35 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9297a47..3c6f578 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,17 +31,21 @@
         <shib-metadata.version>5.0.0</shib-metadata.version>
         <yubico.groupId>com.yubico</yubico.groupId>
         <yubico-webauthn.version>2.5.0</yubico-webauthn.version>
-        <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <jackson-dataformat-cbor.verison>2.15.3</jackson-dataformat-cbor.verison>
+        <com-upokecenter.version>4.5.2</com-upokecenter.version>
+        <checkstyle.configLocation>
+            ${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
     <distributionManagement>
         <site>
             <id>site</id>
-            <url>scp:${shibboleth.site.deploy.url}java-idp-plugin-oidc-rp/${project.version}/</url>
+            <url>
+                scp:${shibboleth.site.deploy.url}java-idp-plugin-oidc-rp/${project.version}/</url>
         </site>
     </distributionManagement>
-    
-     <modules>
+
+    <modules>
         <module>webauthn-impl</module>
         <module>webauthn-api</module>
         <!-- All DIST modules must come after the -api and -impl modules -->
@@ -49,8 +53,10 @@
     </modules>
 
     <scm>
-        <connection>scm:git:https://git.shibboleth.net/git/java-${project.artifactId}</connection>
-        <developerConnection>scm:git:git at git.shibboleth.net:java-${project.artifactId}</developerConnection>
+        <connection>
+            scm:git:https://git.shibboleth.net/git/java-${project.artifactId}</connection>
+        <developerConnection>
+            scm:git:git at git.shibboleth.net:java-${project.artifactId}</developerConnection>
         <url>https://git.shibboleth.net/view/?p=java-${project.artifactId}.git</url>
     </scm>
 
@@ -59,7 +65,7 @@
         <dependency>
             <groupId>${slf4j.groupId}</groupId>
             <artifactId>slf4j-api</artifactId>
-             <scope>provided</scope>
+            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.testng</groupId>
@@ -95,6 +101,12 @@
                 <artifactId>webauthn-server-core</artifactId>
                 <version>${yubico-webauthn.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.upokecenter</groupId>
+                <artifactId>cbor</artifactId>
+                <version>${com-upokecenter.version}</version>
+                <scope>runtime</scope>
+            </dependency>
             <!-- Import Dependencies: Shib-Metadata/Attribute/Profile/Shared -->
             <dependency>
                 <groupId>${shib-shared.groupId}</groupId>
@@ -140,9 +152,17 @@
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+            <!-- Jackson BOM -->
+            <dependency>
+                <groupId>com.fasterxml.jackson</groupId>
+                <artifactId>jackson-bom</artifactId>
+                <version>${jackson.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
-    
+
     <build>
         <plugins>
             <plugin>
@@ -150,12 +170,17 @@
                 <artifactId>maven-javadoc-plugin</artifactId>
                 <configuration>
                     <links combine.children="append">
-                        <link>${javadoc.url}/java-shib-shared/${shib-shared.version}</link>
+                        <link>
+                            ${javadoc.url}/java-shib-shared/${shib-shared.version}</link>
                         <link>${javadoc.url}/java-opensaml/${opensaml.version}</link>
-                        <link>${javadoc.url}/java-shib-metadata/${shib-metadata.version}</link>
-                        <link>${javadoc.url}/java-shib-attribute/${shib-attribute.version}</link>
-                        <link>${javadoc.url}/java-shib-profile/${shib-profile.version}</link>
-                        <link>${javadoc.url}/java-identity-provider/${idp.version}</link>
+                        <link>
+                            ${javadoc.url}/java-shib-metadata/${shib-metadata.version}</link>
+                        <link>
+                            ${javadoc.url}/java-shib-attribute/${shib-attribute.version}</link>
+                        <link>
+                            ${javadoc.url}/java-shib-profile/${shib-profile.version}</link>
+                        <link>
+                            ${javadoc.url}/java-identity-provider/${idp.version}</link>
                     </links>
                 </configuration>
             </plugin>
@@ -173,5 +198,5 @@
                 </configuration>
             </plugin>
         </plugins>
-    </build>   
+    </build>
 </project>
diff --git a/webauthn-api/pom.xml b/webauthn-api/pom.xml
index 3a7d102..39d2b7e 100644
--- a/webauthn-api/pom.xml
+++ b/webauthn-api/pom.xml
@@ -25,11 +25,36 @@
         distribution. -->
 
         <!-- Provided dependencies -->
+         <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.yubico</groupId>
+            <artifactId>webauthn-server-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
             <artifactId>jsr305</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-admin-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
     
     <build>
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
new file mode 100644
index 0000000..3d8a1c3
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/AbstractWebAuthnAuthenticationAction.java
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+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.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.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * <p>A base class for webauthn authentication related 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>Webauthn authentication action implementations should override the
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext, WebAuthnAuthenticationContext)} 
+ * method.</p>
+ * 
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
+ * @post <pre>AuthenticationContext.getSubcontext(WebAuthnAuthenticationContext.class) != null</pre>
+ */
+public abstract class AbstractWebAuthnAuthenticationAction extends AbstractAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnAuthenticationAction.class);
+    
+    /** Lookup strategy to locate the webauthn authentication context. */
+    @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+    
+    /** The Duo authentication Context.*/
+    @NonnullBeforeExec private WebAuthnAuthenticationContext webauthnContext;
+        
+    
+    /** Constructor.*/
+    protected AbstractWebAuthnAuthenticationAction() {
+        //prc -> ac -> dc
+        webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
+    }
+    
+    
+    /**
+     * Set Duo authentication context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setWebauthnContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+        checkSetterPreconditions();
+
+        webauthnContextLookupStrategy = 
+                Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            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);
+            return false;
+            
+        }        
+        return doPreExecute(profileRequestContext, authenticationContext, webauthnContext);
+    }
+    
+    /**
+     * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext, 
+     * WebAuthnAuthenticationContext)} to perform the actual authentication. 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) {
+        
+        doExecute(profileRequestContext,authenticationContext, webauthnContext);
+    }
+    
+    /**
+     * Performs this authentication 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
+     * 
+     * @return true iff execution should continue
+     */
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        return true;
+    }
+    
+    /**
+     * Performs this webauthn authentication action using the supplied webauthn context. Implementations
+     * should override this method.
+     * 
+     * @param profileRequestContext the current IdP profile request context
+     * @param authenticationContext the current authentication context
+     * @param context the webauthn authentication context
+     */
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
new file mode 100644
index 0000000..0bd7cbb
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/PublicKeyCredential.java
@@ -0,0 +1,78 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+ at Immutable
+ at ThreadSafe
+ at JsonDeserialize(builder=PublicKeyCredential.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public final class PublicKeyCredential {
+    
+    @Nonnull private final String type;
+    
+    @Nonnull private final String id;
+
+    @Nonnull private final Response response;
+    
+    //no clientExtensionResults?
+
+    
+    private PublicKeyCredential(Builder builder) {
+        this.type = builder.type;
+        this.id = builder.id;
+        this.response = builder.response;
+    }
+
+    /**
+     * Creates builder to build {@link PublicKeyCredential}.
+     * @return created builder
+     */
+    
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder to build {@link PublicKeyCredential}.
+     */
+    @JsonPOJOBuilder(buildMethodName = "build",withPrefix = "with")
+    @JsonIgnoreProperties(ignoreUnknown = true)
+    public static final class Builder {
+        private String type;
+
+        private String id;
+
+        private Response response;
+
+        private Builder() {
+        }
+
+        public Builder withType(String type) {
+            this.type = type;
+            return this;
+        }
+
+        public Builder withId(String id) {
+            this.id = id;
+            return this;
+        }
+
+        public Builder withResponse(Response response) {
+            this.response = response;
+            return this;
+        }
+
+        public PublicKeyCredential build() {
+            return new PublicKeyCredential(this);
+        }
+    }
+    
+    
+    
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java
new file mode 100644
index 0000000..90e38bb
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/Response.java
@@ -0,0 +1,64 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+ at Immutable
+ at ThreadSafe
+ at JsonDeserialize(builder=Response.Builder.class)
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class Response {
+    
+    /** Base64 encoded attestation Object.*/
+    //TODO should be a byte array converted from the b64 in the JSON
+    @Nonnull private final String attestationObject;
+    
+    /** JSON client data base64 encoded.*/
+    @Nonnull private final String clientDataJSON;
+
+    private Response(Builder builder) {
+        this.attestationObject = builder.attestationObject;
+        this.clientDataJSON = builder.clientDataJSON;
+    }
+
+    /**
+     * Creates builder to build {@link Response}.
+     * @return created builder
+     */    
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Builder to build {@link Response}.
+     */
+    public static final class Builder {
+        private String attestationObject;
+
+        private String clientDataJSON;
+
+        private Builder() {
+        }
+
+        public Builder withAttestationObject(String attestationObject) {
+            this.attestationObject = attestationObject;
+            return this;
+        }
+
+        public Builder withClientDataJSON(String clientDataJSON) {
+            this.clientDataJSON = clientDataJSON;
+            return this;
+        }
+
+        public Response build() {
+            return new Response(this);
+        }
+    }
+    
+    
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
new file mode 100644
index 0000000..de36c42
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
@@ -0,0 +1,39 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A client that manages the entire webauthn authentication ceremony, 
+ * see {@link https://www.w3.org/TR/webauthn/#authentication-ceremony}.
+ * 
+ * <p>Implementations must be thread-safe</p>
+ */
+//TODO integrate this with the CredentialValidator interface?
+ at ThreadSafe
+public interface WebauthnAuthenticationClient {
+    
+    
+     /**
+      * Create a JSON serialized PublicKeyCredentialRequestOptions.
+      * 
+      * @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
+      *                 the credential.  
+      * @return a JSON serialized PublicKeyCredentialRequestOptions object. Can be null if
+      *         one could not be created.
+      */
+    //TODO how do we guarantee this is JSON, or just return the Yubico object?
+     @Nullable String createAuthenticationRequest(@Nullable final String username);
+     
+     
+     /**
+      * Validate the Authenticator Assertion Response. 
+      * 
+      * @param jsonAssertionResponse the JSON representation of the assertion response.
+      * 
+      * @return true if the assertion was verified successfully, false otherwise.
+      */
+     boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse);       
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
new file mode 100644
index 0000000..7e79120
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientException.java
@@ -0,0 +1,51 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+/**
+ *  An exception to signal an error condition during execution or creation of a Webauthn client.
+ */
+public class WebauthnAuthenticationClientException extends Exception {
+    
+    /** Serial UID. */
+    private static final long serialVersionUID = -2380145079984333546L;
+
+    /**
+     * Constructor.
+     *
+     */
+    public WebauthnAuthenticationClientException() {
+        super();
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param cause exception to be wrapped by this one
+     */
+    public WebauthnAuthenticationClientException(final String message, final Throwable cause) {
+        super(message, cause);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public WebauthnAuthenticationClientException(final String message) {
+        super(message);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cause exception to be wrapped by this one
+     */
+    public WebauthnAuthenticationClientException(final Throwable cause) {
+        super(cause);
+        
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
new file mode 100644
index 0000000..3547639
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClientFactory.java
@@ -0,0 +1,20 @@
+package net.shibboleth.idp.plugin.authn.webauthn;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Abstract factory for creating singleton {@link WebauthnAuthenticationClient} instances.
+ */
+public interface WebauthnAuthenticationClientFactory {
+    
+    
+    /**
+     * Create an {@link WebauthnAuthenticationClient} instance.
+     * 
+     * @return the client, never {@code nul}.
+     * 
+     * @throws WebauthnAuthenticationClientException if there is an errtor creating the client.
+     */
+    @Nonnull WebauthnAuthenticationClient createInstance() throws WebauthnAuthenticationClientException;
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java
new file mode 100644
index 0000000..c8ff1f2
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/WebAuthnCredentialRegistrationFlowDescriptor.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.admin;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.admin.BasicAdministrativeFlowDescriptor;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * And administrative flow descriptor to register user WebAuthn credentials.
+ */
+public class WebAuthnCredentialRegistrationFlowDescriptor extends BasicAdministrativeFlowDescriptor {
+    
+    /** Profile ID. */
+    @Nonnull @NotEmpty public static final String PROFILE_ID =
+            "http://shibboleth.net/ns/profiles/webauthn/register-credential";
+
+    /**
+     * Constructor.
+     */
+    public WebAuthnCredentialRegistrationFlowDescriptor() {
+        super(PROFILE_ID);
+        setNonBrowserSupported(false);
+    }
+
+}
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
new file mode 100644
index 0000000..1f51f87
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -0,0 +1,200 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.logic.Constraint;
+
+
+
+ at NotThreadSafe
+public final class WebAuthnAuthenticationContext extends BaseContext {
+    
+    @Nullable private byte[] serverChallenge;  
+    
+    /** A registration public key credential.*/   
+    @Nullable 
+    private PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> publicKeyCredential;
+    
+    /** The  credential public key encoded in COSE_Key format.*/
+    @Nullable private byte[] publicKey;
+    
+    /** The credential identifier generated by the authenticator.*/
+    @Nullable private byte[] credentialId;
+    
+    /** 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 PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>  assertionResponse;
+    
+    /**
+     * 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 assertion response as a result of authentication.
+     * 
+     * @return the assertion.
+     */
+    @Nullable public PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs>
+            getAssertionResponse() {
+        return assertionResponse;
+    }
+    
+    @Nonnull public WebAuthnAuthenticationContext setAssertionResponse(@Nullable final
+            PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs> assertion) {
+        assertionResponse = assertion;
+        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 WebAuthnAuthenticationContext setUsername(@Nullable final String name) {
+        username = name;
+        //transformedUsername = name;
+        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.
+     * 
+     * @param key the public key in COSE_Key format.
+     */
+    public WebAuthnAuthenticationContext setPublicKey(@Nonnull final byte[] key) {
+        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.
+     * 
+     * @return the credential Id.
+     */
+    public byte[] getCredentialId() {
+        return credentialId;
+    }
+    
+    /**
+     * Set the credential Id.
+     * 
+     * @param id the credential Id.
+     */
+    public WebAuthnAuthenticationContext setCredentialId(@Nonnull final byte[] id) {
+        credentialId = Constraint.isNotNull(id, "Credential ID can not be null");
+        return this;
+    }
+    
+    @Nullable 
+    public PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> getPublicKeyCredential() {
+        return publicKeyCredential;
+    }
+    
+    public void setPublicKeyCredential(@Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
+        publicKeyCredential = Constraint.isNotNull(credential, "Public Key Credential can not be null or empty");
+    }
+    
+    //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);
+    }
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java
new file mode 100644
index 0000000..628cac8
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnPublicKeyCredentialRecord.java
@@ -0,0 +1,49 @@
+package net.shibboleth.idp.plugin.authn.webauthn.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.shared.logic.Constraint;
+
+ at Immutable
+ at ThreadSafe
+//TODO does this look like a storage record?
+public final class WebAuthnPublicKeyCredentialRecord {
+    
+    /** The  credential public key encoded in COSE_Key format.*/
+    @Nonnull private final byte[] publicKey;
+    
+    /** The credential identifier generated by the authenticator.*/
+    @Nullable private final byte[] credentialId;
+    
+    /** 
+     * Constructor.
+     *
+     * @param key the public key to store.
+     */
+    public WebAuthnPublicKeyCredentialRecord(@Nonnull final byte[] key, @Nonnull final byte[] credId) {
+        publicKey = Constraint.isNotNull(key, "Public key can not be null");
+        credentialId = Constraint.isNotNull(credId, "Credential ID can not be null");
+    }
+    
+    /**
+     * Get the COSE_Key formatted public key.
+     * 
+     * @return the public key in COSE_Key format.
+     */
+    @Nullable public byte[] getPublicKey() {
+        return publicKey;
+    }
+    
+    /**
+     * Get the credential ID.
+     * 
+     * @return the credential ID.
+     */
+    @Nullable public byte[] getCredentialId() {
+        return credentialId;
+    }
+
+}
diff --git a/webauthn-impl/pom.xml b/webauthn-impl/pom.xml
index 7c0a6bc..e9375cd 100644
--- a/webauthn-impl/pom.xml
+++ b/webauthn-impl/pom.xml
@@ -1,4 +1,5 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
@@ -9,21 +10,38 @@
     </parent>
 
     <artifactId>idp-plugin-webauthn-impl</artifactId>
-    <packaging>jar</packaging>    
+    <packaging>jar</packaging>
     <name>Shibboleth IdP :: Plugins :: WebAuthn Login Flow Impl</name>
     <description>IdP WebAuthn plugin implementation.</description>
-    
+
     <properties>
-        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <checkstyle.configLocation>
+            ${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
         <automatic.module.name>net.shibboleth.idp.plugin.authn.webauthn.impl</automatic.module.name>
     </properties>
-    
+
     <dependencies>
-         <!-- Compile time intra-project dependencies -->
+        <!-- Compile time intra-project dependencies -->
         <dependency>
             <groupId>net.shibboleth.idp.plugin.authn</groupId>
             <artifactId>idp-plugin-webauthn-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.yubico</groupId>
+            <artifactId>webauthn-server-core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency> <!-- note when we move to more yubico libs this will need to be runtime -->
+            <groupId>com.upokecenter</groupId>
+            <artifactId>cbor</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <!-- Runtime dependencies -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-cbor</artifactId>
+            <scope>runtime</scope>
+        </dependency>        
         <!-- Service API and Plugin Description dependencies -->
         <dependency>
             <groupId>${idp.groupId}</groupId>
@@ -40,8 +58,32 @@
             <artifactId>shib-profile-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- Provided dependencies -->
-        
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <scope>provided</scope>
+        </dependency>
         <!-- Test dependencies -->
         <dependency>
             <groupId>${idp.groupId}</groupId>
@@ -57,7 +99,7 @@
             <groupId>${shib-attribute.groupId}</groupId>
             <artifactId>shib-attribute-filter-impl</artifactId>
             <scope>test</scope>
-        </dependency>    
+        </dependency>
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-attribute-filter-spring</artifactId>
@@ -67,12 +109,12 @@
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-messaging-impl</artifactId>
             <scope>test</scope>
-        </dependency> 
+        </dependency>
         <dependency>
             <groupId>${opensaml.groupId}</groupId>
             <artifactId>opensaml-core-impl</artifactId>
             <scope>test</scope>
-        </dependency>       
+        </dependency>
         <dependency>
             <groupId>${shib-shared.groupId}</groupId>
             <artifactId>shib-security</artifactId>
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
new file mode 100644
index 0000000..be16e4f
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
+
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.yubico.webauthn.AssertionRequest;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.StartAssertionOptions;
+import com.yubico.webauthn.data.UserVerificationRequirement;
+
+import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Yuibico version of the {@link WebauthnAuthenticationClient}.
+ * 
+ * <p>Thread-safe, only a single instance is required.</p>
+ */
+ at ThreadSafe
+public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticationClient {
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(YubicoWebauthnAuthenticationClient.class);
+    
+    /** Information pertaining to the relying party.*/
+    @Nonnull private final RelyingParty rp;
+    
+    /** The JSON object mapper used to JSONify webauthn objects. */
+    @Nonnull private final ObjectMapper om;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param relyingParty Information pertaining to the relying party.
+     * @param mapper the JSON object mapper.
+     */
+    public YubicoWebauthnAuthenticationClient(@Nonnull final RelyingParty relyingParty,
+            @Nonnull final ObjectMapper mapper) {
+        rp = Constraint.isNotNull(relyingParty, "The reyling party configuration can not be null");
+        om = Constraint.isNotNull(mapper, "Object mapper can not be null");
+    }
+
+    @Override
+    public String createAuthenticationRequest(@Nullable final String username) {
+        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 AssertionRequest assertion =
+                    rp.startAssertion(StartAssertionOptions.builder()
+                            .username(Optional.ofNullable(username))
+                            .userVerification(userVerificationRequirement)
+                            .build());
+            final ObjectWriter writer = om.writerWithDefaultPrettyPrinter();            
+            return writer.writeValueAsString(assertion);
+        }
+        catch (final JsonProcessingException e) {
+            log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
+        }        
+        return null;
+    }
+
+    @Override
+    public boolean validateAuthenticatorAssertionResponse(@Nullable final String jsonAssertionResponse) {
+        
+        if (jsonAssertionResponse == null || jsonAssertionResponse.isEmpty()) {
+            log.warn("JSON Assertion Response is either null or empty, authentication can not be validated");
+            return false;
+        }
+        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
new file mode 100644
index 0000000..a816a1c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.springframework.beans.factory.FactoryBean;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.RelyingParty;
+import com.yubico.webauthn.data.RelyingPartyIdentity;
+
+import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.DummyCredentialRepository;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+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.
+ at ThreadSafe
+public class YubicoWebauthnClientFactory extends AbstractInitializableComponent 
+            implements FactoryBean<WebauthnAuthenticationClient> {
+
+    /** The relying party identifier.*/
+    @GuardedBy("this") @Nullable private String relyingPartyId;
+    
+    /** The relying party name.*/
+    @GuardedBy("this") @Nullable private String relyingPartyName;
+    
+    /** Allow unrestricted origin ports? Default is false.*/
+    @GuardedBy("this") private boolean allowOriginPort;
+    
+    /** Allow subdomains in origins? Default is false.*/
+    @GuardedBy("this") private boolean allowOriginSubdomain;
+    
+    /** The JSON object mapper used to JSONify webauthn objects. */
+    @GuardedBy("this") @Nullable private ObjectMapper om;
+    
+    /** Constructor.*/
+    public YubicoWebauthnClientFactory() {
+        allowOriginPort = false;
+        allowOriginSubdomain = false;
+    }
+    
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (relyingPartyId ==  null) {
+            throw new ComponentInitializationException("relyingPartyId cannot be null");
+        }
+        if (relyingPartyName ==  null) {
+            throw new ComponentInitializationException("relyingPartyName cannot be null");
+        }
+        if (om ==  null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+
+    }
+    
+    
+    @Override
+    public WebauthnAuthenticationClient getObject() throws Exception {
+        final RelyingParty rp = RelyingParty.builder().identity(
+                RelyingPartyIdentity
+                .builder()
+                .id(getRelyingPartyId())
+                .name(getRelyingPartyName())
+                .build()).credentialRepository(new DummyCredentialRepository())
+        .allowOriginPort(isAllowOriginPort())
+        .allowOriginSubdomain(isAllowOriginSubdomain())
+        .build();
+        
+        return new YubicoWebauthnAuthenticationClient(rp, getObjectMapper());
+    }
+
+    @Override
+    public Class<?> getObjectType() {
+        return WebauthnAuthenticationClient.class;
+    }
+    
+    @Override
+    public boolean isSingleton() {
+        return true;
+    }
+    
+    /**
+     * Set the object mapper.
+     * 
+     * @param objectMapper the object mapper.
+     */
+    public synchronized void setObjectMapper(@Nonnull final ObjectMapper objectMapper) {
+        om = Constraint.isNotNull(objectMapper, "Object mapper can not be null");
+    }
+    
+    /**
+     * Get the object mapper.
+     * 
+     * @return the objectMapper;
+     */
+    private synchronized ObjectMapper getObjectMapper() {
+        return om;
+    }
+    /**
+     * Set the relying party identifier.
+     * 
+     * @param relyingPartyIdentifier the relying party identifier.
+     */
+    public synchronized void setRelyingPartyId(@Nonnull final String relyingPartyIdentifier) {
+        
+        relyingPartyId = Constraint.isNotNull(relyingPartyIdentifier,"You must set a relying party ID");
+    }
+    
+    /**
+     * Get the relying party identifier.
+     * <p>
+     * The RP ID must be equal to the origin's effective domain, or a registrable 
+     * domain suffix of the origin's effective domain.
+     * </p>
+     * 
+     * @return the relying party identifier.
+     */
+    @Nullable private synchronized String getRelyingPartyId() {
+        return relyingPartyId;
+    }
+    
+    /**
+     * Set the relying party name.
+     * 
+     * @param relyingPartyIdentifier the relying party name.
+     */
+    public synchronized void setRelyingPartyName(@Nonnull final String name) {
+        
+        relyingPartyName = Constraint.isNotNull("You must set a relying party name", name);
+    }
+    
+    /**
+     * Get the relying party name.
+     * 
+     * @return the relying party name.
+     */
+    @Nullable private synchronized String getRelyingPartyName() {
+        
+        return relyingPartyName;
+    }
+
+    /**
+     * Is origin port allowed?
+     * 
+     * @return allow origin port?
+     */
+    private synchronized boolean isAllowOriginPort() {
+        
+        return allowOriginPort;
+    }
+
+    /**
+     * Allow unrestricted origin ports?
+     * 
+     * @param allow allow origin port.
+     */
+    //TODO Javadoc
+    public synchronized void setAllowOriginPort(final boolean allow) {
+        
+        allowOriginPort = allow;
+    }
+
+    /**
+     * Are subdomains allowed?
+     * 
+     * @return allow subdomains?
+     */
+    private synchronized boolean isAllowOriginSubdomain() {
+        
+        return allowOriginSubdomain;
+    }
+
+    /**
+     * Set if subdomains are allowed.
+     * 
+     * @param allow are subdomains allowed?
+     */
+    public synchronized void setAllowOriginSubdomain(final boolean allow) {
+        
+        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
new file mode 100644
index 0000000..d197a5d
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+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.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;
+
+/**
+ * Create a public key credential request...
+ */
+public class CreatePublicKeyCredentialRequestOptions extends AbstractWebAuthnAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialRequestOptions.class);
+    
+    /** The Webauthn client.*/
+    @NonnullAfterInit private WebauthnAuthenticationClient webauthnClient;
+    
+    /**
+     * Set the WebAuthn client to use.
+     * 
+     * @param client the client.
+     */
+    public void setWebauthnClient(@Nonnull final WebauthnAuthenticationClient client) {
+        checkSetterPreconditions();        
+        webauthnClient = Constraint.isNotNull(client, "WebAuthn Client can not be null");
+    }
+    
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (webauthnClient ==  null) {
+            throw new ComponentInitializationException("Webauthn client cannot be null");
+        }
+
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext, 
+            @Nonnull final WebAuthnAuthenticationContext context) {
+                
+        final String pkCredRequestOptions = webauthnClient.createAuthenticationRequest(context.getUsername());        
+        log.debug("Created PublicKeyCredentialRequestOptions '{}'",pkCredRequestOptions);
+        
+    }
+
+}
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/ExtractPublicKeyAssertionFromFormRequest.java
new file mode 100644
index 0000000..859e2b3
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyAssertionFromFormRequest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that derives the PublicKeyCredential from a form parameter.
+ */
+public class ExtractPublicKeyAssertionFromFormRequest 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);
+    
+    /** Name of header. */
+    @NonnullAfterInit @NotEmpty private String fieldName;
+
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Constructor. */
+    public ExtractPublicKeyAssertionFromFormRequest() {       
+        fieldName = DEFAULT_FIELD_NAME;
+    }
+    
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper ==  null) {
+            throw new ComponentInitializationException("The objectMapper cannot be null");
+        }
+    }    
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+    
+    /**
+     * Set the name of the field to examine.
+     * 
+     * @param field field name
+     */
+    public void setFieldName(@Nonnull @NotEmpty final String field) {
+        checkSetterPreconditions();
+        
+        fieldName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Field name cannot be null or empty");
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        final String pkCredJson = extractPublicKeyCredential(request);   
+        log.trace("Public Key Assertion in JSON is '{}'",pkCredJson);
+        if (pkCredJson == null) {
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+                
+        try {
+            final PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs> pkCred = 
+                    objectMapper.readValue(pkCredJson,
+                            new TypeReference<PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>>() {});
+            log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
+            log.debug("Signature '{}'",pkCred.getResponse().getSignature());
+            context.setAssertionResponse(pkCred);
+        } catch (final JsonProcessingException e) {
+            log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+       
+    }
+
+    @Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
+        return httpRequest.getParameter(fieldName);
+    }
+    
+}
\ No newline at end of file
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/impl/ExtractPublicKeyCredentialFromFormRequest.java
new file mode 100644
index 0000000..d5577b7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+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.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/**
+ * An action that derives the PublicKeyCredential from a form parameter.
+ */
+public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnAuthenticationAction {
+
+    /** Default token code field name. */
+    @Nonnull @NotEmpty public static final String DEFAULT_FIELD_NAME = "publicKeyCredential";
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractPublicKeyCredentialFromFormRequest.class);
+    
+    /** Name of header. */
+    @Nonnull @NotEmpty private String fieldName;
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Constructor. */
+    public ExtractPublicKeyCredentialFromFormRequest() {
+        fieldName = DEFAULT_FIELD_NAME;
+    }
+    
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (objectMapper ==  null) {
+            throw new ComponentInitializationException("The objectMapper cannot be null");
+        }
+    }    
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+    
+    /**
+     * Set the name of the field to examine.
+     * 
+     * @param field field name
+     */
+    public void setFieldName(@Nonnull @NotEmpty final String field) {
+        checkSetterPreconditions();
+        
+        fieldName = Constraint.isNotNull(StringSupport.trimOrNull(field), "Field name cannot be null or empty");
+    }
+    
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+        final String pkCredJson = extractPublicKeyCredential(request);   
+        log.trace("Public Key Credential in JSON is '{}'",pkCredJson);
+        if (pkCredJson == null) {
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+                
+        try {
+            final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> pkCred = 
+                    objectMapper.readValue(pkCredJson,
+                            new TypeReference<PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs>>() {});
+            log.debug("Client Data '{}'",pkCred.getResponse().getClientData());
+            log.debug("Attestation Data '{}'",pkCred.getResponse().getAttestation());
+            context.setPublicKeyCredential(pkCred);
+        } catch (final JsonProcessingException e) {
+            log.debug("{} Can not extract public key credential response", getLogPrefix(),e);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        
+       
+    }
+
+    @Nullable private String extractPublicKeyCredential(@Nonnull final HttpServletRequest httpRequest) {
+        return httpRequest.getParameter(fieldName);
+    }
+    
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..bd874f2
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/GenerateServerChallenge.java
@@ -0,0 +1,82 @@
+/*
+ * 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.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+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.shared.primitive.LoggerFactory;
+
+/**
+ * An action to generate an appropriate server challenge for use.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * @post See above.
+ */
+public class GenerateServerChallenge extends AbstractWebAuthnAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(GenerateServerChallenge.class);
+   
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext 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);
+            
+        } catch (final NoSuchAlgorithmException e) {
+            log.error("Could not generate a challenge",e);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+        }        
+    }
+    
+    /**
+     * Generate a 32 bytes randomized challenge of sufficient entropy. Must be at least 16 bytes long.
+     * 
+     * @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];
+        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/impl/PopulateWebauthnAuthenticationContext.java
new file mode 100644
index 0000000..3c33779
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+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.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+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} 
+ * with the ... FIXME appropriate for this request. 
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * @post See above.
+ */
+public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateWebauthnAuthenticationContext.class);
+    
+    /** Strategy used to locate or create the {@link WebauthnAuthenticationContext} to populate. */
+    @Nonnull 
+    private final Function<ProfileRequestContext,WebAuthnAuthenticationContext> webauthnAuthContextCreationStrategy;
+    
+    /** Lookup strategy for username to match against Duo identity. */
+    @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));
+        
+        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.
+     */
+    public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+        checkSetterPreconditions();
+        usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+    }
+    
+    /**
+     * Set the lookup strategy to use for the username to match against Duo identity.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUsernameLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+
+        usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
+    }
+    
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        
+        final WebAuthnAuthenticationContext context = webauthnAuthContextCreationStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.error("{} Error creating WebauthnAuthenticationContext", 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());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
+
+        log.debug("Created Webauthn authentication context");
+    }
+    
+    
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java
new file mode 100644
index 0000000..417473c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/UsernameFromAuthenticationContextLookupStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
+
+/**
+ * Take the UsernamePrincipal from the authentication context iff there is only one. Return {@code null} otherwise.
+ */
+public class UsernameFromAuthenticationContextLookupStrategy implements Function<ProfileRequestContext, String> {
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String apply(@Nullable final ProfileRequestContext input) {
+
+        if (input != null) {
+            final AuthenticationContext authnContext = input.getSubcontext(AuthenticationContext.class);
+            if (authnContext != null) {
+                final AuthenticationResult result = authnContext.getAuthenticationResult();
+                if (result != null) {
+                    final Subject subject = result.getSubject();
+                    final Set<UsernamePrincipal> usernamePrincipals = subject.getPrincipals(UsernamePrincipal.class);
+                    if (usernamePrincipals.size() == 1) {
+                        return usernamePrincipals.iterator().next().getName();
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
new file mode 100644
index 0000000..7c86f77
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidatePublicKeyCredential.java
@@ -0,0 +1,73 @@
+/*
+ * 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.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validate the public key registration attempt.
+ */
+public class ValidatePublicKeyCredential extends AbstractWebAuthnAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ValidatePublicKeyCredential.class);
+
+
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final WebAuthnAuthenticationContext context) {
+
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkCred =
+                context.getPublicKeyCredential();
+        
+        if (pkCred == null) {
+            log.error("{} public key credential was null", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        // Validate the registration
+        
+        // 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/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
new file mode 100644
index 0000000..3cfb69c
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -0,0 +1,81 @@
+package net.shibboleth.idp.plugin.authn.webauthn.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.security.auth.Subject;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+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.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * See https://w3c.github.io/webauthn/#sctn-verifying-assertion
+ */
+public class ValidateWebAuthnAssertion extends AbstractValidationAction {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ValidateWebAuthnAssertion.class);
+    
+    /** Strategy used to locate or create the {@link WebAuthnAuthenticationContext} to populate. */
+    @Nonnull
+    private final Function<ProfileRequestContext, WebAuthnAuthenticationContext> webauthnContextLookupStrategy;
+
+    /** The webauthn authentication context. */
+    @NonnullBeforeExec private WebAuthnAuthenticationContext context;
+    
+    /** Constructor. */
+    public ValidateWebAuthnAssertion() {
+        webauthnContextLookupStrategy = new ChildContextLookup<>(WebAuthnAuthenticationContext.class)
+                .compose(new ChildContextLookup<>(AuthenticationContext.class));
+
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        context = webauthnContextLookupStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.warn("{} No WebAuthn context returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+
+        }
+        return true;
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        
+        context.setUsername("jblogs");
+        buildAuthenticationResult(profileRequestContext, authenticationContext);
+        
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+    }
+
+    @Override
+    protected Subject populateSubject(@Nonnull final Subject subject) {
+        final String username = context.getUsername();
+        assert username != null;
+        subject.getPrincipals().add(new UsernamePrincipal(username));
+        return subject;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java
new file mode 100644
index 0000000..9b60af7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/DummyCredentialRepository.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.storage.impl;
+
+import java.util.Optional;
+import java.util.Set;
+
+import com.yubico.webauthn.CredentialRepository;
+import com.yubico.webauthn.RegisteredCredential;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
+
+/** A dummy CredentialRepository for testing.*/
+public class DummyCredentialRepository implements CredentialRepository{
+    
+    @Override
+    public Set<RegisteredCredential> lookupAll(final ByteArray credentialId) {
+        return Set.of(RegisteredCredential.builder()
+                .credentialId(new ByteArray("credid".getBytes()))
+                .userHandle(new ByteArray("userhandle".getBytes()))
+                .publicKeyCose(new ByteArray("publickeycose".getBytes()))
+                .signatureCount(0)
+                .build());
+    }
+    
+    @Override
+    public Optional<RegisteredCredential> lookup(final ByteArray credentialId, final ByteArray userHandle) {
+            return Optional.of(RegisteredCredential.builder()
+                    .credentialId(new ByteArray("credid".getBytes()))
+                    .userHandle(new ByteArray("userhandle".getBytes()))
+                    .publicKeyCose(new ByteArray("publickeycose".getBytes()))
+                    .signatureCount(0)
+                    .build());
+    }
+    
+    @Override
+    public Optional<String> getUsernameForUserHandle(final ByteArray userHandle) {
+        return Optional.of("jdoe");
+    }
+    
+    @Override
+    public Optional<ByteArray> getUserHandleForUsername(final String username) {
+        return Optional.of(new ByteArray("credid".getBytes()));
+    }
+    
+    @Override
+    public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(final String username) {
+        return Set.of(
+                PublicKeyCredentialDescriptor.builder()
+                .id(new ByteArray("credid".getBytes()))
+                .build());
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
new file mode 100644
index 0000000..5952ac9
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/StorePublicKeyCredential.java
@@ -0,0 +1,161 @@
+/*
+ * 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/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java
new file mode 100644
index 0000000..38a2541
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/impl/WebauthnPublicKeyCredentialStorageSerializer.java
@@ -0,0 +1,59 @@
+/*
+ * 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.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.opensaml.storage.StorageSerializer;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnPublicKeyCredentialRecord;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Serialize the WebauthnPublicKeyCredentialRecord to a string using Hex encoding.
+ */
+public class WebauthnPublicKeyCredentialStorageSerializer extends AbstractInitializableComponent 
+    implements StorageSerializer<WebAuthnPublicKeyCredentialRecord> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(WebauthnPublicKeyCredentialStorageSerializer.class);
+
+    @Override
+    public String serialize(final WebAuthnPublicKeyCredentialRecord instance) throws IOException {
+        return Hex.encodeHexString(instance.getPublicKey())+"|"+Hex.encodeHexString(instance.getCredentialId());
+    }
+
+    @Override
+    public WebAuthnPublicKeyCredentialRecord deserialize(final long version, final String context, final String key, final String value,
+            final Long expiration) throws IOException {
+        try {
+            //FIXME what about nulls etc.
+            log.debug("Value {}",value);
+            final String[] components = value.split("\\|");
+            log.debug("Split pk cred {}:{}",components[0],components[1]);
+            return new WebAuthnPublicKeyCredentialRecord(Hex.decodeHex(components[0]),Hex.decodeHex(components[1]));
+        } catch (final DecoderException e) {
+           throw new IOException("Error converting public key hex to bytes",e);
+        }
+    }
+
+}
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 dd18f8b..c8209e5 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
@@ -15,6 +15,30 @@
     removed if not needed 
     -->
     
+    <!-- Authentication flow selection flow -->
+    <bean id="authn/WebAuthnFlowChooser" parent="shibboleth.AuthenticationFlow"
+            p:order="1000"
+            p:nonBrowserSupported="false"
+            p:passiveAuthenticationSupported="false"
+            p:forcedAuthenticationSupported="true"
+            p:lifetime="%{idp.authn.webauthn.lifetime:%{idp.authn.defaultLifetime:PT1H}}"
+            p:inactivityTimeout="%{idp.authn.webauthn.inactivityTimeout:%{idp.authn.defaultTimeout:PT30M}}"
+            p:reuseCondition-ref="#{'%{idp.authn.webauthn.reuseCondition:shibboleth.Conditions.TRUE}'.trim()}"
+            p:activationCondition-ref="#{'%{idp.authn.webauthn.activationCondition:shibboleth.Conditions.TRUE}'.trim()}">
+        <property name="supportedPrincipals">
+            <list>
+                <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                    c:classRef="class-ref" />
+                <bean parent="shibboleth.SAML1AuthenticationMethod"
+                    c:method="auth-ref" />
+            </list>
+        </property>
+        <property name="supportedPrincipalsByString">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.authn.webauthn.supportedPrincipals:}'.trim()}" />
+        </property>
+    </bean>
+    
     <bean id="authn/WebAuthn" parent="shibboleth.AuthenticationFlow"
             p:order="%{idp.authn.webauthn.order:1000}"
             p:nonBrowserSupported="%{idp.authn.webauthn.nonBrowserSupported:false}"
@@ -40,6 +64,24 @@
                 c:_0="#{'%{idp.authn.webauthn.supportedPrincipals:}'.trim()}" />
         </property>
     </bean>
+    
+    
+    <!-- Admim flow for WebAuthn credential registration -->
+    <bean parent="shibboleth.AdminFlow"
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnCredentialRegistrationFlowDescriptor"
+            p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
+            p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByIPAddress}"
+            p:nonBrowserSupported="%{idp.authn.webauthn.admin.registration.nonBrowserSupported:false}"
+            p:authenticated="true">
+        <property name="postAuthenticationFlows">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.authn.webauthn.admin.registration.postAuthenticationFlows:}'.trim()}" />
+        </property>
+        <property name="defaultAuthenticationMethodsByString">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.authn.webauthn.admin.registration.defaultAuthenticationMethods:}'.trim()}" />
+         </property>
+    </bean>
 
   
 </beans>
\ No newline at end of file
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
new file mode 100644
index 0000000..303fa5a
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.AdminProfileId" class="java.lang.String"
+        c:_0="http://shibboleth.net/ns/profiles/webauthn/register-credential" />
+
+    <bean id="PopulateWebauthnAuthenticationContext" scope="prototype" 
+        parent="AbstractPopulateWebauthnAuthenticationContext"
+        p:usernameRequiredPredicate="true">
+        <property name="usernameLookupStrategy">
+            <bean id="UsernameFromAuthenticationContextLookupStrategy" 
+            class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy"/>
+        </property>
+    </bean>
+        
+    <bean id="ExtractPublicKeyCredentialFromFormRequest" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+        
+        
+    <bean id="ValidatePublicKeyCredential" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidatePublicKeyCredential" />
+        
+        
+    <bean id="StorePublicKeyCredential" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.StorePublicKeyCredential" 
+        p:storageService-ref="shibboleth.authn.webauthn.StorageService"/>
+
+    <!--  postconfig.xml? -->
+    <bean id="shibboleth.authn.webauthn.StorageService" lazy-init="true"
+        class="org.opensaml.storage.impl.MemoryStorageService"
+        p:cleanupInterval="PT10M" />
+        
+</beans>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
new file mode 100644
index 0000000..0ac016a
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -0,0 +1,78 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="admin.abstract">
+    
+    
+    <!-- Start action. -->
+        
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to determine if authentication is required. -->
+        <transition on="proceed" to="DoAdminPreamble" />
+    </action-state>
+
+    <!-- Resume actual flow processing. -->
+
+    <action-state id="DoProfileWork">
+        <evaluate expression="CheckAccess" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PopulateWebAuthnContext" />
+    </action-state>   
+    
+     <action-state id="PopulateWebAuthnContext">
+        <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+        <evaluate expression="GenerateServerChallenge"/>
+        <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="DisplayWebAuthnView" />    
+    </action-state>     
+    
+    <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-register">
+        <on-render>
+            <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="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" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+        </on-render>
+       <transition on="proceed" to="ExtractPublicKeyCredential" />
+       
+    </view-state>
+    
+   <action-state id="ExtractPublicKeyCredential">
+        <evaluate expression="ExtractPublicKeyCredentialFromFormRequest"/>
+        <evaluate expression="ValidatePublicKeyCredential"/>
+        <evaluate expression="StorePublicKeyCredential"/>
+        
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="DisplayWebAuthnSuccessfulRegistration" />
+    </action-state>
+    
+    <end-state id="DisplayWebAuthnSuccessfulRegistration" view="webauthn/webauthn-registered">
+          <on-entry>
+                <evaluate expression="environment" result="requestScope.environment" />
+                <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+                <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="requestScope.authenticationContext" />
+                <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext))" result="requestScope.webauthnContext" />
+                <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="requestScope.rpUIContext" />
+                <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="requestScope.authenticationErrorContext" />
+                <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="requestScope.authenticationWarningContext" />
+                <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="requestScope.encoder" />
+                <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="requestScope.request" />
+                <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="requestScope.response" />
+            </on-entry>
+     </end-state>
+    
+    <bean-import resource="webauthn-registration-beans.xml" />
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/authn/webauthn/webauthn-abstract-beans.xml" />
+	
+</flow>
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
new file mode 100644
index 0000000..8dce35c
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="AbstractPopulateWebauthnAuthenticationContext" scope="prototype" abstract="true"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext" />
+
+    <bean id="GenerateServerChallenge" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.GenerateServerChallenge" />
+
+    <bean id="CreatePublicKeyCredentialRequestOptions" scope="prototype"
+        class=" net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialRequestOptions"
+        p:webauthnClient-ref="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" />
+        
+     <!-- Singleton clients -->
+    <bean id="shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory" scope="singleton"
+        class="net.shibboleth.idp.plugin.authn.webauthn.client.impl.YubicoWebauthnClientFactory"
+        p:relyingPartyId="localhost" p:relyingPartyName="Shibboleth"
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+        
+     <!-- Create a default object mapper. Setup should not change once injected -->
+    <bean id="shibboleth.authn.WebAuthn.JSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="setSerializationInclusion">
+        <property name="arguments">
+            <util:constant static-field="com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL" />
+        </property>
+    </bean>
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="registerModule">
+        <property name="arguments">
+            <bean class="com.fasterxml.jackson.datatype.jsr310.JavaTimeModule" />
+        </property>
+    </bean>
+
+
+
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="setDateFormat">
+        <property name="arguments">
+            <bean class="java.text.SimpleDateFormat" c:_0="yyyy-MM-dd'T'HH:mm:ss.SSSZZ" />
+        </property>
+    </bean>
+
+</beans>
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 3dd79d0..283c197 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
@@ -1,15 +1,25 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
-       xmlns:context="http://www.springframework.org/schema/context"
-       xmlns:util="http://www.springframework.org/schema/util"
-       xmlns:p="http://www.springframework.org/schema/p"
-       xmlns:c="http://www.springframework.org/schema/c"
-       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
-                           
-       default-init-method="initialize"
-       default-destroy-method="destroy">
-       
+
+    default-init-method="initialize" default-destroy-method="destroy">
+
+     <bean id="PopulateWebauthnAuthenticationContext" scope="prototype" 
+        parent="AbstractPopulateWebauthnAuthenticationContext"
+        p:usernameRequiredPredicate="false">
+    </bean>
+
+    <bean id="ValidateWebAuthnAssertion" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ValidateWebAuthnAssertion" />
+
+    <bean id="ExtractPublicKeyAssertionFromFormRequest" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyAssertionFromFormRequest"
+        p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+        p:objectMapper-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" />
+
 </beans>
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 d8cec3f..5942fec 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
@@ -1,5 +1,41 @@
 <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd">
+	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="authn.abstract, authn/conditions">
 
+    <action-state id="PopulateWebauthnContext">
+        <evaluate expression="PopulateWebauthnAuthenticationContext"/>
+        <evaluate expression="GenerateServerChallenge"/>
+        <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="DisplayWebAuthnView" />    
+    </action-state>
+    
+    
+    <view-state id="DisplayWebAuthnView" view="webauthn/webauthn-authn">
+        <on-render>
+            <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="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" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+        </on-render>
+       <transition on="proceed" to="AuthenticatePublicKeyCredential" />
+       
+    </view-state>
+     
+    <action-state id="AuthenticatePublicKeyCredential">
+        <evaluate expression="ExtractPublicKeyAssertionFromFormRequest"/>
+        <evaluate expression="ValidateWebAuthnAssertion"/>
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="proceed" />
+    </action-state>
+    
+    <bean-import resource="webauthn-beans.xml" />
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/authn/webauthn/webauthn-abstract-beans.xml" />
 	
 </flow>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml
new file mode 100644
index 0000000..7e8dab1
--- /dev/null
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthnFlowChooser/webauthn-flow.xml
@@ -0,0 +1,18 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="authn.abstract, authn/conditions">
+    
+    <!-- end-state transitions are inherited from authn-events-flow.xml -->
+    <view-state id="DisplayWebAuthnChoiceView" view="webauthn/webauthn-selector">
+        <on-render>
+                <evaluate expression="environment" result="viewScope.environment" />
+                <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+                <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)" result="viewScope.encoder" />
+                <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+                <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+            </on-render>
+            <transition on="proceed" to="WebAuthnFlow" />
+            <transition on="password" to="PasswordFlow" />
+    </view-state>
+
+</flow>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
new file mode 100644
index 0000000..00c1974
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
@@ -0,0 +1,163 @@
+
+
+const base64abc = [
+    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
+    "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+    "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
+    "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
+];
+
+
+const base64codes = [
+    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63,
+    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, 255,
+    255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255,
+    255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
+];
+
+function bytesToBase64(bytes) {
+    let result = '', i, l = bytes.length;
+    for (i = 2; i < l; i += 3) {
+        result += base64abc[bytes[i - 2] >> 2];
+        result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+        result += base64abc[((bytes[i - 1] & 0x0F) << 2) | (bytes[i] >> 6)];
+        result += base64abc[bytes[i] & 0x3F];
+    }
+    if (i === l + 1) { // 1 octet yet to write
+        result += base64abc[bytes[i - 2] >> 2];
+        result += base64abc[(bytes[i - 2] & 0x03) << 4];
+        result += "==";
+    }
+    if (i === l) { // 2 octets yet to write
+        result += base64abc[bytes[i - 2] >> 2];
+        result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+        result += base64abc[(bytes[i - 1] & 0x0F) << 2];
+        result += "=";
+    }
+    return result;
+}
+
+function responseToObject(response) {
+    if (response.u2fResponse) {
+        return response;
+    } else {
+        let clientExtensionResults = {};
+
+        try {
+            clientExtensionResults = response.getClientExtensionResults();
+        } catch (e) {
+            console.error('getClientExtensionResults failed', e);
+        }
+
+        if (response.response.attestationObject) {
+            return {
+                type: response.type,
+                id: response.id,
+                response: {
+                    attestationObject: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.attestationObject))),
+                    clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+                },
+                clientExtensionResults,
+            };
+        } else {
+            return {
+                type: response.type,
+                id: response.id,
+                response: {
+                    authenticatorData: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.authenticatorData))),
+                    clientDataJSON: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.clientDataJSON))),
+                    signature: Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.signature))),
+                    userHandle: response.response.userHandle && Base64EncodeUrl(bytesToBase64(ensureUint8Array(response.response.userHandle))),
+                },
+                clientExtensionResults,
+            };
+        }
+    }
+}
+
+if (!window.PublicKeyCredential) {
+    console.log("Bad client, died");
+}
+
+function ensureUint8Array(arg) {
+    if (arg instanceof ArrayBuffer) {
+        return new Uint8Array(arg);
+    } else {
+        return arg;
+    }
+}
+/**
+ * use this to make a Base64 encoded string URL friendly, 
+ * i.e. '+' and '/' are replaced with '-' and '_' also any trailing '=' 
+ * characters are removed
+ *
+ * @param {String} str the encoded string
+ * @returns {String} the URL friendly encoded String
+ */
+function Base64EncodeUrl(str) {
+    return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
+}
+
+const publicKeyCredentialCreationOptions = {
+    challenge: Uint8Array.from(
+        "$webauthnContext.serverChallengeBase64", c => c.charCodeAt(0)),
+    rp: {
+        name: "Shib",
+        id: "localhost",
+    },
+    user: {
+        id: Uint8Array.from(
+            "UZSL85T9AFC", c => c.charCodeAt(0)),
+        name: "phil1001 at jisc.ac.uk",
+        displayName: "Phil",
+    },
+    pubKeyCredParams: [{ alg: -7, type: "public-key" }],
+    authenticatorSelection: {
+        authenticatorAttachment: "cross-platform",
+        userVerification: "preferred"
+    },
+    timeout: 60000,
+    attestation: "direct"
+};
+
+async function authenticate() {
+     try{
+            var challenge = "$webauthnContext.serverChallengeBase64";
+            console.log("Challenge: "+challenge);
+            const assertion = await navigator.credentials.get({
+                publicKey: {
+                    challenge: Uint8Array.from(
+                        challenge, c => c.charCodeAt(0)),
+                    timeout: 60000,
+                }
+            });
+            console.log(assertion);
+            document.getElementById("publicKeyAssertion").value = JSON.stringify(responseToObject(assertion));
+            document.getElementById("authn-submit").click();
+        } catch (err) {
+            console.log(err);
+        }      
+     }
+
+
+async function register() {
+    const credential = await navigator.credentials.create({
+        publicKey: publicKeyCredentialCreationOptions
+    }).catch(console.error);
+
+    console.log("Credential: " + credential);
+    console.log(responseToObject(credential));
+    console.log(JSON.stringify(credential.response));
+    try {
+        document.getElementById("publicKeyCredential").value = JSON.stringify(responseToObject(credential));
+
+    } catch (err) {
+        console.log(err);
+    }
+
+}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js
new file mode 100644
index 0000000..2cf5335
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-yubico.js
@@ -0,0 +1,171 @@
+// Copyright (c) 2018, Yubico AB
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+(function(root, factory) {
+  if (typeof define === 'function' && define.amd) {
+    define(['base64url'], factory);
+  } else if (typeof module === 'object' && module.exports) {
+    module.exports = factory(require('base64url'));
+  } else {
+    root.webauthn = factory(root.base64url);
+  }
+})(this, function(base64url) {
+
+  function extend(obj, more) {
+    return Object.assign({}, obj, more);
+  }
+
+  /**
+   * Create a WebAuthn credential.
+   *
+   * @param request: object - A PublicKeyCredentialCreationOptions object, except
+   *   where binary values are base64url encoded strings instead of byte arrays
+   *
+   * @return a PublicKeyCredentialCreationOptions suitable for passing as the
+   *   `publicKey` parameter to `navigator.credentials.create()`
+   */
+  function decodePublicKeyCredentialCreationOptions(request) {
+    const excludeCredentials = request.excludeCredentials.map(credential => extend(
+      credential, {
+      id: base64url.toByteArray(credential.id),
+    }));
+
+    const publicKeyCredentialCreationOptions = extend(
+      request, {
+      attestation: 'direct',
+      user: extend(
+        request.user, {
+        id: base64url.toByteArray(request.user.id),
+      }),
+      challenge: base64url.toByteArray(request.challenge),
+      excludeCredentials,
+    });
+
+    return publicKeyCredentialCreationOptions;
+  }
+
+  /**
+   * Create a WebAuthn credential.
+   *
+   * @param request: object - A PublicKeyCredentialCreationOptions object, except
+   *   where binary values are base64url encoded strings instead of byte arrays
+   *
+   * @return the Promise returned by `navigator.credentials.create`
+   */
+  function createCredential(request) {
+    return navigator.credentials.create({
+      publicKey: decodePublicKeyCredentialCreationOptions(request),
+    });
+  }
+
+  /**
+   * Perform a WebAuthn assertion.
+   *
+   * @param request: object - A PublicKeyCredentialRequestOptions object,
+   *   except where binary values are base64url encoded strings instead of byte
+   *   arrays
+   *
+   * @return a PublicKeyCredentialRequestOptions suitable for passing as the
+   *   `publicKey` parameter to `navigator.credentials.get()`
+   */
+  function decodePublicKeyCredentialRequestOptions(request) {
+    const allowCredentials = request.allowCredentials && request.allowCredentials.map(credential => extend(
+      credential, {
+      id: base64url.toByteArray(credential.id),
+    }));
+
+    const publicKeyCredentialRequestOptions = extend(
+      request, {
+      allowCredentials,
+      challenge: base64url.toByteArray(request.challenge),
+    });
+
+    return publicKeyCredentialRequestOptions;
+  }
+
+  /**
+   * Perform a WebAuthn assertion.
+   *
+   * @param request: object - A PublicKeyCredentialRequestOptions object,
+   *   except where binary values are base64url encoded strings instead of byte
+   *   arrays
+   *
+   * @return the Promise returned by `navigator.credentials.get`
+   */
+  function getAssertion(request) {
+    console.log('Get assertion', request);
+    return navigator.credentials.get({
+      publicKey: decodePublicKeyCredentialRequestOptions(request),
+    });
+  }
+
+
+  /** Turn a PublicKeyCredential object into a plain object with base64url encoded binary values */
+  function responseToObject(response) {
+    if (response.u2fResponse) {
+      return response;
+    } else {
+      let clientExtensionResults = {};
+
+      try {
+        clientExtensionResults = response.getClientExtensionResults();
+      } catch (e) {
+        console.error('getClientExtensionResults failed', e);
+      }
+
+      if (response.response.attestationObject) {
+        return {
+          type: response.type,
+          id: response.id,
+          response: {
+            attestationObject: bytesToBase64(response.response.attestationObject),
+            clientDataJSON: bytesToBase64(response.response.clientDataJSON),
+          },
+          clientExtensionResults,
+        };
+      } else {
+        return {
+          type: response.type,
+          id: response.id,
+          response: {
+            authenticatorData: base64url.fromByteArray(response.response.authenticatorData),
+            clientDataJSON: base64url.fromByteArray(response.response.clientDataJSON),
+            signature: base64url.fromByteArray(response.response.signature),
+            userHandle: response.response.userHandle && base64url.fromByteArray(response.response.userHandle),
+          },
+          clientExtensionResults,
+        };
+      }
+    }
+  }
+
+  return {
+    decodePublicKeyCredentialCreationOptions,
+    decodePublicKeyCredentialRequestOptions,
+    createCredential,
+    getAssertion,
+    responseToObject,
+  };
+
+});
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
new file mode 100644
index 0000000..1e98647
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -0,0 +1,72 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>#springMessageText("idp.title", "Web Login Service")</title>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+    <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText(" idp.css", "/css/placeholder.css" )">
+    <script src="$request.getContextPath()/js/webauthn-shib.js"></script>
+</head>
+
+<body>
+    <main class="main">
+        <header>
+            <img class="main-logo" src="$request.getContextPath()#springMessageText(" idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+
+            #set ($serviceName = $rpUIContext.serviceName)
+            #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+            <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+            #end
+        </header>
+
+        <section>
+
+            <div class="content">
+                <div class="column one">
+                    <button class="form-element form-button" onclick="authenticate();">Authenticate</button>
+                    <form id="authn-form" action="$flowExecutionUrl" method="post">
+                        #parse("csrf/csrf.vm")
+                        <textarea id="publicKeyAssertion" name="publicKeyAssertion" rows="10" cols="50">
+                        </textarea>
+                        <button id="authn-submit" type="submit" name="_eventId_proceed">Submit Authentication</button>
+                    </form>
+
+
+                </div>
+
+            </div>
+        </section>
+
+        <footer>
+            <div class="container container-footer">
+                <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+            </div>
+        </footer>
+        </div>
+
+</body>
+
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
new file mode 100644
index 0000000..ab1baf0
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -0,0 +1,73 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+
+<head>
+  <title>#springMessageText("idp.title", "Web Login Service")</title>
+  <meta charset="UTF-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+  <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css" )">
+  <script src="$request.getContextPath()/js/webauthn-shib.js"></script>
+</head>
+
+
+
+<body>
+    <main class="main">
+    <header>
+        <img class="main-logo" src="$request.getContextPath()#springMessageText(" idp.logo", "/images/placeholder-logo.png" )" alt="#springMessageText(" idp.logo.alt-text", "logo" )" />
+         #set ($serviceName = $rpUIContext.serviceName)
+        #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+        <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+        #end
+    </header>
+
+      <section>
+    
+        <div class="content">
+          <div class="column one">
+            <button class="form-element form-button" onclick="register();">Register</button>
+    
+            <form action="$flowExecutionUrl" method="post">
+              #parse("csrf/csrf.vm")
+              <textarea id="publicKeyCredential" name="publicKeyCredential" rows="10" cols="50"></textarea>
+              <button id="reg-submit" type="submit" name="_eventId_proceed">Submit Registration</button>
+            </form>
+    
+    
+          </div>
+    
+        </div>
+      </section>
+  </main>
+  <footer>
+    <div class="container container-footer">
+      <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+    </div>
+  </footer>
+  </div>
+
+</body>
+
+</html>
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm
new file mode 100644
index 0000000..2ed681d
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-registered.vm
@@ -0,0 +1,57 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## authenticationErrorContext - context with login error state
+## webauthnContext = web authentication context
+## authenticationWarningContext - context with login warning state
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>#springMessageText("idp.title", "Web Login Service")</title>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+        <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+    </head>
+    
+    
+    
+    <body>
+        <main class="main">
+            <header>
+                <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+                
+                #set ($serviceName = $rpUIContext.serviceName)
+                #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+                    <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+                #end
+            </header>
+            <section>
+                You have registered your authenticator credentials succesfully.
+            </section>
+        </main>
+
+      <footer>
+        <div class="container container-footer">
+          <p class="footer-text">#springMessageText("idp.footer", "Insert your footer text here.")</p>
+        </div>
+      </footer>
+    </div>
+    
+     </body>
+</html>
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
new file mode 100644
index 0000000..cf78851
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-selector.vm
@@ -0,0 +1,66 @@
+##
+## Velocity Template for DisplayWebauthnView view-state
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## encoder - HTMLEncoder class
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
+##
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>#springMessageText("idp.title", "Web Login Service")</title>>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+        <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+    </head>
+    <body>
+        <main class="main">
+            <header>
+                <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+                
+                #set ($serviceName = $rpUIContext.serviceName)
+                #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+                    <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+                #end
+            </header>
+            
+            <section>
+            
+                <form action="$flowExecutionUrl" method="post">
+                    #parse("csrf/csrf.vm")
+                    <div class="grid">
+                            <div class="grid-item">
+                                <button type="submit" name="_eventId_proceed">Use Passkey</button>
+                            </div>
+                    </div>
+                 </form>
+                 <form action="$flowExecutionUrl" method="post">
+                    #parse("csrf/csrf.vm")
+                    <div class="grid">
+                            <div class="grid-item">
+                                <button type="submit" name="_eventId_password">Use Password</button>
+                            </div>
+                    </div>
+                 </form>
+  
+            </section>
+        </main>
+        <footer class="footer">
+            <div class="cc">
+                <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+            </div>
+        </footer>
+     </body>
+    
+     </body>
+</html>
\ No newline at end of file

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


More information about the commits mailing list