[java-idp-plugin-webauthn] branch main updated: Fix up basic registration flow.

Phil Smart philip.smart at jisc.ac.uk
Wed Nov 8 13:58:34 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=8d8ef591e6243a8415ced610a9d71685e22fafb8

The following commit(s) were added to refs/heads/main by this push:
     new 8d8ef59  Fix up basic registration flow.
8d8ef59 is described below

commit 8d8ef591e6243a8415ced610a9d71685e22fafb8
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Nov 8 13:58:20 2023 +0000

    Fix up basic registration flow.
    
    - Including server side PublicKeyCredentialCreationOptions creation
    - And use of Yubico JavaScript to create the required inputs to
        navigator.credentials.create
---
 .../example-webauthn-request-yubico.json           |  63 ++
 resources/javascript/index.html                    | 114 +++
 resources/javascript/placeholder.css               | 802 +++++++++++++++++++++
 resources/javascript/webauthn-json.js              | 192 +++++
 .../js => resources/javascript}/webauthn-shib.js   |  90 +--
 ...ient.java => WebAuthnAuthenticationClient.java} |  20 +-
 .../WebauthnAuthenticationClientFactory.java       |   6 +-
 .../context/WebAuthnAuthenticationContext.java     |  49 +-
 .../impl/YubicoWebauthnAuthenticationClient.java   |  74 +-
 .../client/impl/YubicoWebauthnClientFactory.java   |   8 +-
 .../CreatePublicKeyCredentialCreationOptions.java  |  83 +++
 .../CreatePublicKeyCredentialRequestOptions.java   |  43 +-
 .../ExtractPublicKeyCredentialFromFormRequest.java |   2 +-
 .../PopulateWebauthnAuthenticationContext.java     |  26 +
 .../webauthn/impl/ValidateWebAuthnAssertion.java   |  18 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  53 ++
 .../webauthn-registration-beans.xml                |   3 +
 .../webauthn-registration-flow.xml                 |   2 +-
 .../authn/WebAuthn/webauthn-abstract-beans.xml     |  39 +-
 .../idp/plugin/authn/webauthn/js/webauthn-json.js  | 189 +++++
 .../plugin/authn/webauthn/js/webauthn-json.js.map  |   7 +
 .../authn/webauthn/views/webauthn-register.vm      |  37 +-
 22 files changed, 1773 insertions(+), 147 deletions(-)

diff --git a/resources/javascript/example-webauthn-request-yubico.json b/resources/javascript/example-webauthn-request-yubico.json
new file mode 100644
index 0000000..75b9c40
--- /dev/null
+++ b/resources/javascript/example-webauthn-request-yubico.json
@@ -0,0 +1,63 @@
+{
+    "success": true,
+    "request": {
+        "username": "",
+        "credentialNickname": "",
+        "requestId": "_-Mmw_rKUdlZIYez0Iz5c12zd8xxiexOmaPpx1213c8",
+        "publicKeyCredentialCreationOptions": {
+            "rp": {
+                "name": "Yubico WebAuthn demo",
+                "id": "localhost"
+            },
+            "user": {
+                "name": "",
+                "displayName": "",
+                "id": "2Pp4ZXaN5_9AhXwD4Sh3aHZ01PiePBJ8gz9n8h4LbeM"
+            },
+            "challenge": "ZLxqs6DgjZECnbjeIpKLXEPKEWKVwKr-UaGYAjvwQc0",
+            "pubKeyCredParams": [
+                {
+                    "alg": -7,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -8,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -35,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -36,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -257,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -258,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -259,
+                    "type": "public-key"
+                }
+            ],
+            "excludeCredentials": [],
+            "authenticatorSelection": {
+                "requireResidentKey": true,
+                "residentKey": "required"
+            },
+            "attestation": "direct",
+            "extensions": {
+                "credProps": true
+            }
+        },
+        "sessionToken": "nJEWdhxUNNX3S6lKE0v82U7W_mYJQ0X2VT2Y12R2WfI"
+    },
+    "actions": {
+        "finish": "https://localhost:8443/api/v1/register/finish"
+    }
+}
diff --git a/resources/javascript/index.html b/resources/javascript/index.html
new file mode 100644
index 0000000..686d219
--- /dev/null
+++ b/resources/javascript/index.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>Test Register Page</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="placeholder.css">
+    <script src="webauthn-shib.js"></script>
+    <script src="webauthn-json.js"></script>
+
+    <script>// type="module">
+        if (!window.PublicKeyCredential) {
+            console.log("WebAuthn not supported");
+        }
+
+        async function register() {
+            console.log("Register");
+            var pkCredOptions = {
+            "rp": {
+                "name": "Yubico WebAuthn demo",
+                "id": "localhost"
+            },
+            "user": {
+                "name": "",
+                "displayName": "",
+                "id": "2Pp4ZXaN5_9AhXwD4Sh3aHZ01PiePBJ8gz9n8h4LbeM"
+            },
+            "challenge": "ZLxqs6DgjZECnbjeIpKLXEPKEWKVwKr-UaGYAjvwQc0",
+            "pubKeyCredParams": [
+                {
+                    "alg": -7,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -8,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -35,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -36,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -257,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -258,
+                    "type": "public-key"
+                },
+                {
+                    "alg": -259,
+                    "type": "public-key"
+                }
+            ],
+            "excludeCredentials": [],
+            "authenticatorSelection": {
+                "requireResidentKey": true,
+                "residentKey": "required"
+            },
+            "attestation": "direct",
+            "extensions": {
+                "credProps": true
+            }
+        };
+
+        console.log("Raw request options", pkCredOptions);                 
+        const credential = await create({ publicKey: pkCredOptions }).catch(console.error);    
+        console.log("Credential: ",credential);
+        try {
+            document.getElementById("publicKeyCredential").value = JSON.stringify(credential);    
+        } catch (err) {
+            console.log(err);
+        }
+    }
+    
+        function init() {
+            document.getElementById("registerButton").onclick = register;
+            return false;
+        }
+
+        window.onload = init;
+
+    </script>
+
+</head>
+
+
+
+<body>
+
+    <main class="main">
+        <section>
+            <div class="content">
+                <div class="column one">
+                    <button class="form-element form-button" id="registerButton">Register</button>
+                    <form action="$flowExecutionUrl" method="post">
+                        <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>
+    </div>
+
+</body>
+
+</html>
diff --git a/resources/javascript/placeholder.css b/resources/javascript/placeholder.css
new file mode 100644
index 0000000..c1dbe1c
--- /dev/null
+++ b/resources/javascript/placeholder.css
@@ -0,0 +1,802 @@
+/* Colours pallet
+
+To change the colours, use find and replace with the values below:
+
+ #ECEFF1 - Body background, header / section border, read only / disabled input fields.
+
+ #1534E3 - Links, buttons, list items, selected radio, selected checkbox.
+
+ #1A237E - Hover buttons.
+
+ #32424A - Body text, input fields border.
+ 
+ #7A2D00 - Output message.
+ 
+ #B50024 - Error messages / fields.
+ 
+ #1C7D40 - Success messages / fields.
+ 
+ #999999 - read only / disabled input fields.
+
+*/
+
+html, html * {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font-size: 100%;
+  font: inherit;
+  vertical-align: baseline;
+  box-sizing: border-box;
+  background: none;
+  background-repeat: no-repeat;
+  background-position: left top;
+  border: 0;
+  outline: 0;
+}
+
+html {
+  height: 100%;
+}
+
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, main {
+  display: block;
+}
+
+/* Default document styles - fonts, font sizes, text colours, font weight */
+body {
+  font-family: Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
+  font-size: 16px;
+  font-size: 1rem;
+  line-height: 24px;
+  line-height: 1.5rem;
+  color: #32424a;
+  font-weight: 400;
+  max-width: 3000px;
+  margin: 0 auto;
+  background-color: #eceff1;
+  padding: 24px;
+}
+
+/* Links */
+a[href] {
+  text-decoration: none;
+  color: #1534e3;
+}
+
+/* Link hover states */
+a[href]:hover, a[href]:active, a[href]:focus {
+  text-decoration: underline;
+  color: #1534e3;
+}
+
+/* Heading styles */
+h1 {
+  font-size: 24px;
+  font-size: 1.5rem;
+  line-height: 28px;
+  line-height: 1.75rem;
+  font-weight: 700;
+}
+
+h2 {
+  font-size: 20px;
+  font-size: 1.25rem;
+  line-height: 25px;
+  line-height: 1.5rem;
+  font-weight: 400;
+}
+
+h3 {
+  font-size: 16px;
+  font-size: 1rem;
+  line-height: 22px;
+  line-height: 1.375rem;
+  font-weight: 700;
+}
+
+h4 {
+  font-size: 14px;
+  font-size: 0.875rem;
+  line-height: 18px;
+  line-height: 1.125rem;
+  font-weight: 700;
+  margin-bottom: 0.5em;
+}
+
+h5 {
+  font-size: 14px;
+  font-size: 0.875rem;
+  line-height: 18px;
+  line-height: 1.125rem;
+  font-weight: 400;
+}
+
+h1, h2, h3, h4, h5, h6, p {
+  margin-bottom: 1em;
+}
+
+h1:last-child, h2:last-child, h3:last-child, h4:last-child, h5:last-child, h6:last-child, p:last-child {
+  margin-bottom: 0px;
+}
+
+/* List styles */
+ol, ul {
+  list-style: none;
+  margin: 20px 0;
+}
+
+ol:before, ol:after,
+ul:before, ul:after {
+  content: " ";
+  display: table;
+}
+
+ol:after,
+ul:after {
+  clear: both;
+}
+
+ol:last-child,
+ul:last-child {
+  margin-bottom: 0px;
+}
+
+ol:first-child,
+ul:first-child {
+  margin-top: 0px;
+}
+
+ul li {
+  padding-left: 22px;
+  margin-bottom: 4px;
+  position: relative;
+  list-style: none;
+}
+
+ul li:last-child {
+  margin-bottom: 0px;
+}
+
+ul li:before {
+  content: '';
+  -webkit-border-radius: 2px;
+  -ms-border-radius: 2px;
+  -moz-border-radius: 2px;
+  -o-border-radius: 2px;
+  border-radius: 2px;
+  background-color: #1534e3;
+  height: 7px;
+  width: 7px;
+  display: block;
+  position: absolute;
+  left: 0;
+  top: 7px;
+}
+
+ol {
+  counter-reset: item;
+}
+
+ol li {
+  padding-left: 22px;
+  margin-bottom: 10px;
+  position: relative;
+  list-style: none;
+}
+
+ol li:last-child {
+  margin-bottom: 0px;
+}
+
+ol li:before {
+  color: #1534e3;
+  position: absolute;
+  left: 0;
+  content: counter(item) ". ";
+  counter-increment: item;
+  font-weight: 700;
+  top: 1px;
+}
+
+ol li:nth-child(n+10) {
+  padding-left: 30px;
+}
+
+ol li:nth-child(n+100) {
+  padding-left: 38px;
+}
+
+hr {
+  width: 100%;
+  clear: both;
+  border: 0;
+  outline: 0;
+  background-color: #eceff1;
+  height: 1px;
+  display: block;
+  margin: 30px 0;
+}
+
+b, strong {
+  font-weight: 700;
+}
+
+i, em {
+  font-style: italic;
+}
+
+small {
+  font-size: 0.8em;
+}
+
+big {
+  font-size: 1.2em;
+}
+
+.cc {
+  clear: both;
+  margin: 0 auto;
+  width: 100%;
+  max-width: 649px;
+  padding: 0 0;
+}
+
+.cc:before, .cc:after {
+  content: " ";
+  display: table;
+}
+
+.cc:after {
+  clear: both;
+}
+
+img {
+  max-width: 100%;
+  height: auto;
+}
+
+/* Main content area */
+main {
+  clear: both;
+  margin: 0 auto;
+  width: 100%;
+  max-width: 800px;
+  background-color: #fff;
+  -webkit-border-radius: 8px;
+  -ms-border-radius: 8px;
+  -moz-border-radius: 8px;
+  -o-border-radius: 8px;
+  border-radius: 8px;
+  -webkit-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -ms-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -moz-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -o-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+}
+
+/* Add a border top when 2 sections are together */
+main section + section {
+  border-top: 1px solid #eceff1;
+}
+
+/* Header */
+header {
+  display: block; /* Change to display: none to hide */
+  border-bottom: 1px solid #eceff1;
+  text-align: center;
+  padding: 6% 8%;
+}
+
+header .main-logo {
+  display: block;
+  margin: 0 auto;
+}
+
+.service-logo {
+	display: block;
+	margin: 24px 0;
+}
+
+section {
+  padding: 4% 8% 4% 8%;
+}
+
+/* Output Messages */
+.output-message {
+  font-size: 14px;
+  font-size: 0.875rem;
+  line-height: 18px;
+  line-height: 1.125rem;
+  font-style: italic;
+  -webkit-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+  background-color: #FFD8C2;
+  display: block;
+  padding: 4%;
+  margin-bottom: 20px;
+  color: #7A2D00;
+}
+
+.output-message:last-child {
+  margin-bottom: 0px;
+}
+
+/* Output Message Success */
+.output-message.output--success {
+  background-color: #DCF9E7;
+  color: #1C7D40;
+}
+
+/* Output Message Error */
+.output-message.output--error {
+  background-color: #FFF0F3;
+  color: #B50024;
+}
+
+.boxed {
+  -webkit-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -ms-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -moz-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  -o-box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  box-shadow: 0px 0px 10px 0px rgba(38, 50, 56, 0.11);
+  background-color: #fff;
+  padding: 6%;
+  margin: 30px 0;
+}
+
+.boxed:last-child {
+  margin-bottom: 0px;
+}
+
+.boxed:first-child {
+  margin-top: 0px;
+}
+
+/* Footer style */
+footer {
+  z-index: 1;
+  position: relative;
+  text-align: center;
+  margin-top: 20px;
+  font-size: 12px;
+  font-size: 0.75rem;
+  line-height: 16px;
+  line-height: 1rem;
+}
+
+/* Forms styles */
+fieldset {
+  display: block;
+  margin-bottom: 20px;
+}
+
+fieldset:last-child {
+  margin-bottom: 0px;
+}
+
+fieldset .field-validation {
+  display: block;
+  margin-top: 10px;
+}
+
+fieldset .error {
+  color: #B50024;
+}
+
+fieldset legend span {
+  display: block;
+  text-indent: 100%;
+  white-space: nowrap;
+  overflow: hidden;
+  height: 0;
+}
+
+/* Form labels */
+label {
+  vertical-align: top;
+  font-size: 14px;
+  font-size: 0.875rem;
+  line-height: 18px;
+  line-height: 1.125rem;
+  font-weight: 700;
+  display: block;
+  color: #32424a;
+  margin-bottom: 6px;
+}
+
+label:focus {
+  color: #1534E3;
+}
+
+/* Form input fields */
+input[type="text"], 
+input[type="email"], 
+input[type="password"], 
+input[type="telephone"], 
+input[type="tel"], 
+input[type="url"], 
+textarea, 
+select {
+  background-color: #fff;
+  border: 2px solid #32424a;
+  -webkit-transition: all 0.3s ease-in-out;
+  -moz-transition: all 0.3s ease-in-out;
+  -ms-transition: all 0.3s ease-in-out;
+  -o-transition: all 0.3s ease-in-out;
+  transition: all 0.3s ease-in-out;
+  color: #32424a;
+  padding: 13px 20px;
+  display: block;
+  width: 100%;
+  -webkit-appearance: none;
+  -ms-appearance: none;
+  -moz-appearance: none;
+  -o-appearance: none;
+  appearance: none;
+  -webkit-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+}
+
+/* Form input focus */
+input[type="text"]:focus, 
+input[type="email"]:focus, 
+input[type="password"]:focus, 
+input[type="telephone"]:focus, 
+input[type="tel"]:focus, 
+input[type="url"]:focus, 
+textarea:focus, 
+select:focus,
+input[type="text"]:active, 
+input[type="email"]:active, 
+input[type="password"]:active, 
+input[type="telephone"]:active, 
+input[type="tel"]:active, 
+input[type="url"]:active, 
+textarea:active, 
+select:active {
+  border-color: #1534e3;
+  background-color: #fff;
+}
+
+/* Form input errors */
+input[type="text"].error, 
+input[type="email"].error, 
+input[type="password"].error, 
+input[type="telephone"].error, 
+input[type="tel"].error, 
+input[type="url"].error, 
+textarea.error, 
+select.error {
+  border-color: #B50024;
+}
+
+/* Form input read only / disabled */
+input[type="text"]:read-only, 
+input[type="email"]:read-only, 
+input[type="password"]:read-only, 
+input[type="telephone"]:read-only,
+input[type="tel"]:read-only, 
+input[type="url"]:read-only, 
+textarea:read-only, 
+select:read-only, 
+input[type="text"]:disabled, 
+input[type="email"]:disabled, 
+input[type="password"]:disabled, 
+input[type="telephone"]:disabled, 
+input[type="tel"]:disabled, 
+input[type="url"]:disabled, 
+textarea:disabled, 
+select:disabled {
+  background-color: #ECEFF1;
+  pointer-events: none;
+}
+
+/* Text areas */
+textarea {
+  height: 124px;
+  resize: none;
+}
+
+
+/* Dropdowns */
+select {
+  -webkit-appearance: auto;
+  -ms-appearance: auto;
+  -moz-appearance: auto;
+  -o-appearance: auto;
+  appearance: auto;
+}
+
+select:read-only {
+  background-color: #fff;
+  pointer-events: unset;
+}
+
+select::-ms-expand {
+  display: none;
+}
+
+/* Checkboxes / Radio buttons */
+input[type="checkbox"], input[type="radio"] {
+  position : absolute;
+  opacity: 0;
+  height: 0;
+  width: 0;
+}
+
+input[type="checkbox"] + label, input[type="radio"] + label {
+  display: block;
+  font-weight: 400;
+  font-size: 16px;
+  font-size: 1rem;
+  line-height: 24px;
+  line-height: 1.5rem;
+  cursor: pointer;
+  position: relative;
+  padding-left: 30px;
+  padding-top: 3px;
+  margin-bottom: 4px;
+  margin-right: 12px;
+  display: inline-block;
+}
+
+input[type="checkbox"] + label:before, input[type="radio"] + label:before {
+  content: "";
+  position: absolute;
+  top: 2px;
+  left: 0;
+  height: 20px;
+  width: 20px;
+  background-color: #fff;
+  border: 2px solid #32424a;
+  -webkit-transition: all 0.2s ease-out;
+  -ms-transition: all 0.2s ease-out;
+  -moz-transition: all 0.2s ease-out;
+  -o-transition: all 0.2s ease-out;
+  transition: all 0.2s ease-out;
+}
+
+input[type="checkbox"] + label:after, input[type="radio"] + label:after {
+  content: "";
+  position: absolute;
+  -webkit-transform: rotate(45deg);
+  -ms-transform: rotate(45deg);
+  -moz-transform: rotate(45deg);
+  -o-transform: rotate(45deg);
+  transform: rotate(45deg);
+  display: none;
+}
+
+input[type="checkbox"]:checked + label, input[type="radio"]:checked + label,
+input[type="checkbox"]:focus + label, input[type="radio"]:focus + label {
+  color: #1534e3;
+}
+
+input[type="checkbox"]:checked + label:before, input[type="radio"]:checked + label:before,
+input[type="checkbox"]:focus + label:before, input[type="radio"]:focus + label:before {
+  border-color: #1534e3;
+}
+
+input[type="checkbox"]:checked + label:after, input[type="radio"]:checked + label:after {
+  display: block;
+}
+
+input[type="checkbox"] + label:before {
+  -webkit-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+}
+
+input[type="checkbox"] + label:after {
+  left: 8px;
+  top: 5px;
+  width: 4px;
+  height: 10px;
+  border: solid #1534e3;
+  border-width: 0 4px 4px 0;
+}
+
+input[type="radio"] + label:before {
+  -webkit-border-radius: 100%;
+  -ms-border-radius: 100%;
+  -moz-border-radius: 100%;
+  -o-border-radius: 100%;
+  border-radius: 100%;
+}
+
+input[type="radio"] + label:after {
+  background-color: #1534e3;
+  height: 12px;
+  width: 12px;
+  -webkit-border-radius: 100%;
+  -ms-border-radius: 100%;
+  -moz-border-radius: 100%;
+  -o-border-radius: 100%;
+  border-radius: 100%;
+  left: 6px;
+  top: 8px;
+}
+
+/* Buttons / Submit buttons */
+button, input[type=button], 
+input[type=submit], 
+a.button, 
+.button {
+  display: inline-block;
+  text-align: center;
+  background-color: #1534e3;
+  border: 4px solid #1534e3;
+  font-weight: 700;
+  padding: 11px 74px;
+  cursor: pointer;
+  color: #fff;
+  -webkit-appearance: none;
+  -ms-appearance: none;
+  -moz-appearance: none;
+  -o-appearance: none;
+  appearance: none;
+  -webkit-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+  -webkit-transition: all 0.2s ease-out;
+  -ms-transition: all 0.2s ease-out;
+  -moz-transition: all 0.2s ease-out;
+  -o-transition: all 0.2s ease-out;
+  transition: all 0.2s ease-out;
+}
+
+/* Button hover & focus states */
+button:hover, 
+input[type=button]:hover, 
+input[type=submit]:hover, 
+a.button:hover, 
+.button:hover, 
+button:focus, 
+input[type=button]:focus, 
+input[type=submit]:focus, 
+a.button:focus, 
+.button:focus {
+  background: #1a237e;
+  color: #fff !important;
+  text-decoration: none !important;
+  border-color: #1a237e;
+}
+
+/* Secondary button styles */
+button.button--secondary, 
+input[type=button].button--secondary, 
+input[type=submit].button--secondary, 
+a.button.button--secondary, 
+.button.button--secondary {
+  background-color: transparent;
+  border-color: #1534e3;
+  color: #1534e3;
+}
+
+/* Secondary button hover & focus states */
+button.button--secondary:hover, 
+input[type=button].button--secondary:hover, 
+input[type=submit].button--secondary:hover, 
+a.button.button--secondary:hover, 
+.button.button--secondary:hover,
+button.button--secondary:focus, 
+input[type=button].button--secondary:focus, 
+input[type=submit].button--secondary:focus, 
+a.button.button--secondary:focus, 
+.button.button--secondary:focus {
+  background: #1a237e;
+  border-color: #1a237e;
+}
+
+/* Secondary button disabled states */
+button.button--secondary:disabled, 
+input[type=button].button--secondary:disabled, 
+input[type=submit].button--secondary:disabled, 
+a.button.button--secondary:disabled, 
+.button.button--secondary:disabled {
+  background-color: transparent;
+  color: #999999;
+}
+
+/* Full width buttons */
+button.button--full, 
+input[type=button].button--full, 
+input[type=submit].button--full, 
+a.button.button--full, 
+.button.button--full {
+  width: 100%;
+  padding-left: 30px;
+  padding-right: 30px;
+}
+
+/* Button disabled states */
+button:disabled, 
+input[type=button]:disabled, 
+input[type=submit]:disabled, 
+a.button:disabled, 
+.button:disabled {
+  pointer-events: none;
+  background-color: #999999;
+  border-color: #999999;
+}
+
+/* Placeholder styles */
+::-webkit-input-placeholder {
+  color: #a9b0b4;
+}
+
+:-moz-placeholder {
+  color: #a9b0b4;
+}
+
+::-moz-placeholder {
+  color: #a9b0b4;
+}
+
+:-ms-input-placeholder {
+  color: #a9b0b4;
+}
+
+.grid {
+  margin-top: 12px;
+}
+
+/* Grid (used for 2 columns) */
+.grid:before, .grid:after {
+  content: " ";
+  display: table;
+}
+
+.grid:after {
+  clear: both;
+}
+
+.grid > .grid-item {
+  margin-bottom: 18px;
+  min-height: 1px;
+  width: 100%;
+}
+
+.grid:last-child > .item:last-child {
+  margin-bottom: 0px;
+}
+
+ at media screen and (min-width: 760px) {
+	
+  .grid.md-2 > .grid-item {
+    float: left;
+    width: 48.34436%;
+    margin-right: 3.31126%;
+  }
+  
+  .grid.md-2 > .grid-item:nth-child(n), .grid.md-2 > .grid-item:nth-of-type(n) {
+    margin-right: 3.31126%;
+    clear: none;
+  }
+  
+  .grid.md-2 > .grid-item:nth-child(2n) {
+    margin-right: 0;
+  }
+  
+  .grid.md-2 > .grid-item:nth-child(2n+1) {
+    clear: both;
+  }
+  
+}
diff --git a/resources/javascript/webauthn-json.js b/resources/javascript/webauthn-json.js
new file mode 100644
index 0000000..ec0ed6f
--- /dev/null
+++ b/resources/javascript/webauthn-json.js
@@ -0,0 +1,192 @@
+// src/webauthn-json/base64url.ts
+function base64urlToBuffer(baseurl64String) {
+    const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4);
+    const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding;
+    const str = atob(base64String);
+    const buffer = new ArrayBuffer(str.length);
+    const byteView = new Uint8Array(buffer);
+    for (let i = 0; i < str.length; i++) {
+      byteView[i] = str.charCodeAt(i);
+    }
+    return buffer;
+  }
+  function bufferToBase64url(buffer) {
+    const byteView = new Uint8Array(buffer);
+    let str = "";
+    for (const charCode of byteView) {
+      str += String.fromCharCode(charCode);
+    }
+    const base64String = btoa(str);
+    const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+    return base64urlString;
+  }
+  
+  // src/webauthn-json/convert.ts
+  var copyValue = "copy";
+  var convertValue = "convert";
+  function convert(conversionFn, schema2, input) {
+    if (schema2 === copyValue) {
+      return input;
+    }
+    if (schema2 === convertValue) {
+      return conversionFn(input);
+    }
+    if (schema2 instanceof Array) {
+      return input.map((v) => convert(conversionFn, schema2[0], v));
+    }
+    if (schema2 instanceof Object) {
+      const output = {};
+      for (const [key, schemaField] of Object.entries(schema2)) {
+        if (schemaField.deriveFn) {
+          const v = schemaField.deriveFn(input);
+          if (v !== void 0) {
+            input[key] = v;
+          }
+        }
+        if (!(key in input)) {
+          if (schemaField.required) {
+            throw new Error(`Missing key: ${key}`);
+          }
+          continue;
+        }
+        if (input[key] == null) {
+          output[key] = null;
+          continue;
+        }
+        output[key] = convert(conversionFn, schemaField.schema, input[key]);
+      }
+      return output;
+    }
+  }
+  function derived(schema2, deriveFn) {
+    return {
+      required: true,
+      schema: schema2,
+      deriveFn
+    };
+  }
+  function required(schema2) {
+    return {
+      required: true,
+      schema: schema2
+    };
+  }
+  function optional(schema2) {
+    return {
+      required: false,
+      schema: schema2
+    };
+  }
+  
+  // src/webauthn-json/basic/schema.ts
+  var publicKeyCredentialDescriptorSchema = {
+    type: required(copyValue),
+    id: required(convertValue),
+    transports: optional(copyValue)
+  };
+  var simplifiedExtensionsSchema = {
+    appid: optional(copyValue),
+    appidExclude: optional(copyValue),
+    credProps: optional(copyValue)
+  };
+  var simplifiedClientExtensionResultsSchema = {
+    appid: optional(copyValue),
+    appidExclude: optional(copyValue),
+    credProps: optional(copyValue)
+  };
+  var credentialCreationOptions = {
+    publicKey: required({
+      rp: required(copyValue),
+      user: required({
+        id: required(convertValue),
+        name: required(copyValue),
+        displayName: required(copyValue)
+      }),
+      challenge: required(convertValue),
+      pubKeyCredParams: required(copyValue),
+      timeout: optional(copyValue),
+      excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),
+      authenticatorSelection: optional(copyValue),
+      attestation: optional(copyValue),
+      extensions: optional(simplifiedExtensionsSchema)
+    }),
+    signal: optional(copyValue)
+  };
+  var publicKeyCredentialWithAttestation = {
+    type: required(copyValue),
+    id: required(copyValue),
+    rawId: required(convertValue),
+    response: required({
+      clientDataJSON: required(convertValue),
+      attestationObject: required(convertValue),
+      transports: derived(copyValue, (response) => response.getTransports?.() || [])
+    }),
+    clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
+  };
+  var credentialRequestOptions = {
+    mediation: optional(copyValue),
+    publicKey: required({
+      challenge: required(convertValue),
+      timeout: optional(copyValue),
+      rpId: optional(copyValue),
+      allowCredentials: optional([publicKeyCredentialDescriptorSchema]),
+      userVerification: optional(copyValue),
+      extensions: optional(simplifiedExtensionsSchema)
+    }),
+    signal: optional(copyValue)
+  };
+  var publicKeyCredentialWithAssertion = {
+    type: required(copyValue),
+    id: required(copyValue),
+    rawId: required(convertValue),
+    response: required({
+      clientDataJSON: required(convertValue),
+      authenticatorData: required(convertValue),
+      signature: required(convertValue),
+      userHandle: required(convertValue)
+    }),
+    clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
+  };
+  var schema = {
+    credentialCreationOptions,
+    publicKeyCredentialWithAttestation,
+    credentialRequestOptions,
+    publicKeyCredentialWithAssertion
+  };
+  
+  // src/webauthn-json/basic/api.ts
+  function createRequestFromJSON(requestJSON) {
+    var requestFromJSON = convert(base64urlToBuffer, credentialCreationOptions, requestJSON);
+    console.log("Create request JSON", requestFromJSON);
+  return requestFromJSON;
+  }
+  function createResponseToJSON(credential) {
+    return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential);
+  }
+  async function create(requestJSON) {
+    const credential = await navigator.credentials.create(createRequestFromJSON(requestJSON));
+    return createResponseToJSON(credential);
+  }
+  function getRequestFromJSON(requestJSON) {
+    return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);
+  }
+  function getResponseToJSON(credential) {
+    return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential);
+  }
+  async function get(requestJSON) {
+    const credential = await navigator.credentials.get(getRequestFromJSON(requestJSON));
+    return getResponseToJSON(credential);
+  }
+  
+  // src/webauthn-json/basic/supported.ts
+  function supported() {
+    return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
+  }
+//   export {
+//     create,
+//     get,
+//     schema,
+//     supported
+//   };
+  //# sourceMappingURL=webauthn-json.js.map
+  
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js b/resources/javascript/webauthn-shib.js
similarity index 68%
rename from webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
rename to resources/javascript/webauthn-shib.js
index 00c1974..3f39300 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-shib.js
+++ b/resources/javascript/webauthn-shib.js
@@ -103,61 +103,35 @@ 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
+  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:  Uint8Array.from(request.user.id, c => c.charCodeAt(0)),
+      }),
+      challenge:  Uint8Array.from(request.challenge, c => c.charCodeAt(0)),
+      //excludeCredentials,
+    });
+
+    return publicKeyCredentialCreationOptions;
+  }
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
similarity index 55%
rename from webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
rename to webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java
index de36c42..345243e 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebauthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/WebAuthnAuthenticationClient.java
@@ -4,14 +4,13 @@ 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}.
+ * A client that manages the entire webauthn authentication and registration ceremony.
  * 
  * <p>Implementations must be thread-safe</p>
  */
 //TODO integrate this with the CredentialValidator interface?
 @ThreadSafe
-public interface WebauthnAuthenticationClient {
+public interface WebAuthnAuthenticationClient {
     
     
      /**
@@ -26,6 +25,21 @@ public interface WebauthnAuthenticationClient {
     //TODO how do we guarantee this is JSON, or just return the Yubico object?
      @Nullable String createAuthenticationRequest(@Nullable final String username);
      
+     /**
+      * Create a JSON serialized PublicKeyCredentialCreationOptions.
+      * 
+      * @param username the username of the username that has been pre-identified. Can be {@code null}
+      *                 if no user has been identified, and the IdP is requesting the client discover
+      *                 the credential.  
+      * @param userHandle an opaque user.id used to map public key credentials to user accounts and vice-versa.
+      * @param the challenge used when creating new credentials
+      * 
+      * @return a JSON serialized PublicKeyCredentialCreationOptions object. Can be {@code null} if
+      *         one could not be created.
+      */
+    //TODO how do we guarantee this is JSON, or just return the Yubico object?
+     @Nullable String createRegistrationRequest(@Nullable final String username, final byte[] userHandle, final byte[] challenge);
+     
      
      /**
       * Validate the Authenticator Assertion Response. 
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
index 3547639..4968181 100644
--- 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
@@ -3,18 +3,18 @@ package net.shibboleth.idp.plugin.authn.webauthn;
 import javax.annotation.Nonnull;
 
 /**
- * Abstract factory for creating singleton {@link WebauthnAuthenticationClient} instances.
+ * Abstract factory for creating singleton {@link WebAuthnAuthenticationClient} instances.
  */
 public interface WebauthnAuthenticationClientFactory {
     
     
     /**
-     * Create an {@link WebauthnAuthenticationClient} instance.
+     * 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;
+    @Nonnull WebAuthnAuthenticationClient createInstance() throws WebauthnAuthenticationClientException;
 
 }
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
index 1f51f87..c976eb8 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnAuthenticationContext.java
@@ -11,12 +11,13 @@ import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
 import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
 import com.yubico.webauthn.data.PublicKeyCredential;
 
+import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.codec.EncodingException;
 import net.shibboleth.shared.logic.Constraint;
 
 
-
+//TODO if we use a client which supports more than just Yubico (not too realistic) then we need to generalise these types
 @NotThreadSafe
 public final class WebAuthnAuthenticationContext extends BaseContext {
     
@@ -45,6 +46,13 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     @Nullable 
     private PublicKeyCredential<AuthenticatorAssertionResponse,ClientRegistrationExtensionOutputs>  assertionResponse;
     
+    /** The public key credential creation options for registration.*/ 
+    @Nullable private String publicKeyCredentialCreationOptions;
+    
+    /** The WebAuthn client to use.*/
+    //TODO is the context the correct place for this type of client
+    @Nullable private WebAuthnAuthenticationClient webAuthnClient;
+    
     /**
      * Set the server challenge which the client authenticator needs to sign.
      * 
@@ -187,7 +195,8 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
         return publicKeyCredential;
     }
     
-    public void setPublicKeyCredential(@Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
+    public void setPublicKeyCredential(
+            @Nonnull final PublicKeyCredential<AuthenticatorAttestationResponse,ClientRegistrationExtensionOutputs> credential) {
         publicKeyCredential = Constraint.isNotNull(credential, "Public Key Credential can not be null or empty");
     }
     
@@ -196,5 +205,41 @@ public final class WebAuthnAuthenticationContext extends BaseContext {
     @Nullable public String getServerChallengeBase64() throws EncodingException {
         return Base64Support.encode(serverChallenge, false);
     }
+    
+    /**
+     * Set the WebAuthn client used to handle registration and authentication ceremonies.
+     * 
+     * @param webAuthnClient The webauthnClient to set.
+     */
+    public void setWebAuthnClient(@Nullable final WebAuthnAuthenticationClient client) {
+        webAuthnClient = client;
+    }
+    
+    /**
+     * Get the WebAuthn client used to handle registration and authentication ceremonies.
+     * 
+     * @return the webAuthnClient.
+     */
+    @Nullable public WebAuthnAuthenticationClient getWebAuthnClient() {
+        return webAuthnClient;
+    }
+    
+    /**
+     * Set the options used to create public key credentials. 
+     * 
+     * @param options The publicKeyCredentialCreationOptions to set.
+     */
+    public void setPublicKeyCredentialCreationOptions(@Nullable final String options) {
+        publicKeyCredentialCreationOptions = options;
+    }
+    
+    /**
+     * Get the options used to create public key credentials.
+     * 
+     * @return the publicKeyCredentialCreationOptions.
+     */
+    @Nullable public String getPublicKeyCredentialCreationOptions() {
+        return publicKeyCredentialCreationOptions;
+    }
 
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
index be16e4f..3c41103 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnAuthenticationClient.java
@@ -14,6 +14,9 @@
 
 package net.shibboleth.idp.plugin.authn.webauthn.client.impl;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 
 import javax.annotation.Nonnull;
@@ -21,26 +24,29 @@ 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.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialParameters;
+import com.yubico.webauthn.data.UserIdentity;
 import com.yubico.webauthn.data.UserVerificationRequirement;
 
-import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Yuibico version of the {@link WebauthnAuthenticationClient}.
+ * Yuibico version of the {@link WebAuthnAuthenticationClient}.
  * 
  * <p>Thread-safe, only a single instance is required.</p>
  */
 @ThreadSafe
-public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticationClient {
+public class YubicoWebauthnAuthenticationClient implements WebAuthnAuthenticationClient {
     
     /** Class logger.*/
     @Nonnull private final Logger log = LoggerFactory.getLogger(YubicoWebauthnAuthenticationClient.class);
@@ -51,6 +57,18 @@ public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticatio
     /** The JSON object mapper used to JSONify webauthn objects. */
     @Nonnull private final ObjectMapper om;
     
+    /** List of acceptable public key algorithms.*/
+    private final List<PublicKeyCredentialParameters> preferredPublickeyParams =
+            Collections.unmodifiableList(
+                Arrays.asList(
+                    PublicKeyCredentialParameters.ES256,
+                    PublicKeyCredentialParameters.EdDSA,
+                    PublicKeyCredentialParameters.ES384,
+                    PublicKeyCredentialParameters.ES512,
+                    PublicKeyCredentialParameters.RS256,
+                    PublicKeyCredentialParameters.RS384,
+                    PublicKeyCredentialParameters.RS512));
+    
     /**
      * 
      * Constructor.
@@ -78,8 +96,48 @@ public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticatio
                             .username(Optional.ofNullable(username))
                             .userVerification(userVerificationRequirement)
                             .build());
-            final ObjectWriter writer = om.writerWithDefaultPrettyPrinter();            
-            return writer.writeValueAsString(assertion);
+            return assertion.toJson();
+        }
+        catch (final JsonProcessingException e) {
+            log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
+        }        
+        return null;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public String createRegistrationRequest(final String username, final byte[] userHandle, final byte[] challenge) {
+        try {
+            //set default to preferred.
+            UserVerificationRequirement userVerificationRequirement = UserVerificationRequirement.PREFERRED;
+            if (username == null) {
+                //then require user verification? makes sense, but is that part of the spec?
+                userVerificationRequirement = UserVerificationRequirement.REQUIRED;
+            }
+            final UserIdentity identity = 
+                    UserIdentity.builder().name(username).displayName(username).id(new ByteArray(userHandle)).build();
+            
+            final PublicKeyCredentialCreationOptions options =
+                    PublicKeyCredentialCreationOptions.builder()
+                        .rp(rp.getIdentity())
+                        .user(identity)
+                        .challenge(new ByteArray(challenge))
+                        .pubKeyCredParams(preferredPublickeyParams)
+                        .excludeCredentials(Optional.empty())
+//                        .excludeCredentials(
+//                            credentialRepository.getCredentialIdsForUsername(
+//                                startRegistrationOptions.getUser().getName()))
+//                        .authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection())
+//                        .extensions(
+//                            startRegistrationOptions
+//                                .getExtensions()
+//                                .merge(
+//                                    RegistrationExtensionInputs.builder()
+//                                        .appidExclude(appId)
+//                                        .credProps()
+//                                        .build()))
+                        .timeout(Optional.empty()).build();
+            return om.writerWithDefaultPrettyPrinter().writeValueAsString(options);
         }
         catch (final JsonProcessingException e) {
             log.error("Could not construct FIDO2 authentication request for '{}'",username,e);
@@ -97,4 +155,6 @@ public class YubicoWebauthnAuthenticationClient implements WebauthnAuthenticatio
         return true;
     }
 
+
+
 }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
index a816a1c..b50a7b3 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
@@ -25,7 +25,7 @@ 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.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.DummyCredentialRepository;
 import net.shibboleth.shared.component.AbstractInitializableComponent;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -37,7 +37,7 @@ import net.shibboleth.shared.logic.Constraint;
 //TODO do we need a factory abstract for this type of initialisation e.g. not runtime.
 @ThreadSafe
 public class YubicoWebauthnClientFactory extends AbstractInitializableComponent 
-            implements FactoryBean<WebauthnAuthenticationClient> {
+            implements FactoryBean<WebAuthnAuthenticationClient> {
 
     /** The relying party identifier.*/
     @GuardedBy("this") @Nullable private String relyingPartyId;
@@ -77,7 +77,7 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
     
     
     @Override
-    public WebauthnAuthenticationClient getObject() throws Exception {
+    public WebAuthnAuthenticationClient getObject() throws Exception {
         final RelyingParty rp = RelyingParty.builder().identity(
                 RelyingPartyIdentity
                 .builder()
@@ -93,7 +93,7 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
 
     @Override
     public Class<?> getObjectType() {
-        return WebauthnAuthenticationClient.class;
+        return WebAuthnAuthenticationClient.class;
     }
     
     @Override
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
new file mode 100644
index 0000000..03bffdc
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialCreationOptions.java
@@ -0,0 +1,83 @@
+/*
+ * 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.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.WebAuthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action to create a PublicKeyCredentialCreationOptions from the parameters in the WebAuthn context.
+ */
+public class CreatePublicKeyCredentialCreationOptions extends AbstractWebAuthnAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(CreatePublicKeyCredentialCreationOptions.class);
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext, 
+            @Nonnull final WebAuthnAuthenticationContext context) {
+        
+        final WebAuthnAuthenticationClient client = context.getWebAuthnClient();
+        if (client == null) {
+            log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        
+        try {                
+        final String pkCredCreationOptions = 
+                client.createRegistrationRequest(context.getUsername(), generateUserHandle(), 
+                        context.getServerChallenge());
+                //verify correct JSON response?
+                context.setPublicKeyCredentialCreationOptions(pkCredCreationOptions);
+                log.debug("Created PublicKeyCredentialCreationOptions '{}'",pkCredCreationOptions);
+        } catch (final NoSuchAlgorithmException e) {
+            log.error("{} Unable to generate PublicKeyCredentialCreationOptions",getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }        
+    }
+    
+    /**
+     * Generate a 32 bytes user handle challenge of sufficient entropy. Must be at maximum
+     * 64 bytes long.
+     * 
+     * @return the challenge in bytes
+     * 
+     * @throws NoSuchAlgorithmException if no secure random algorithm is available
+     */
+    @Nonnull private byte[] generateUserHandle() throws NoSuchAlgorithmException {        
+        final byte[] bytes = new byte[32];
+        SecureRandom.getInstanceStrong().nextBytes(bytes);
+        log.trace("Generated '{}' byte challenge",bytes.length);
+        return bytes;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
index d197a5d..749f756 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/CreatePublicKeyCredentialRequestOptions.java
@@ -17,54 +17,41 @@ 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 net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.AbstractWebAuthnAuthenticationAction;
-import net.shibboleth.idp.plugin.authn.webauthn.WebauthnAuthenticationClient;
+import net.shibboleth.idp.plugin.authn.webauthn.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...
+ * Action to create a CreatePublicKeyCredentialRequestOptions from the parameters in the WebAuthn context.
  */
 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 WebAuthnAuthenticationClient client = context.getWebAuthnClient();
+        if (client == null) {
+            log.error("{} WebAuthn client is null, has the context been created correctly?",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
                 
-        final String pkCredRequestOptions = webauthnClient.createAuthenticationRequest(context.getUsername());        
+        final String pkCredRequestOptions = client.createAuthenticationRequest(context.getUsername());
+        //verify correct JSON response?
+        
+        
         log.debug("Created PublicKeyCredentialRequestOptions '{}'",pkCredRequestOptions);
         
     }
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
index d5577b7..f1211f4 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractPublicKeyCredentialFromFormRequest.java
@@ -111,7 +111,7 @@ public class ExtractPublicKeyCredentialFromFormRequest extends AbstractWebAuthnA
         
         final String pkCredJson = extractPublicKeyCredential(request);   
         log.trace("Public Key Credential in JSON is '{}'",pkCredJson);
-        if (pkCredJson == null) {
+        if (StringSupport.trimOrNull(pkCredJson) == null) {
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             return;
         }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
index 3c33779..b7eed7b 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/PopulateWebauthnAuthenticationContext.java
@@ -28,8 +28,11 @@ import org.slf4j.Logger;
 
 import net.shibboleth.idp.authn.AbstractAuthenticationAction;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.WebAuthnAuthenticationClient;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.logic.PredicateSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
@@ -59,6 +62,9 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
     /** Is the username required?*/
     private Predicate<ProfileRequestContext> usernameRequiredPredicate;
     
+    /** The Webauthn client.*/
+    @NonnullAfterInit private WebAuthnAuthenticationClient webauthnClient;
+    
 
     /** Constructor.*/
     public PopulateWebauthnAuthenticationContext() {
@@ -71,6 +77,25 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
         usernameRequiredPredicate = PredicateSupport.alwaysTrue();
     }
     
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (webauthnClient ==  null) {
+            throw new ComponentInitializationException("Webauthn client cannot be null");
+        }
+
+    }
+    
+    /**
+     * Set the WebAuthn client to use.
+     * 
+     * @param client the client.
+     */
+    public void setWebauthnClient(@Nonnull final WebAuthnAuthenticationClient client) {
+        checkSetterPreconditions();        
+        webauthnClient = Constraint.isNotNull(client, "WebAuthn Client can not be null");
+    }
+    
     /**
      * @param flag The usernameRequired to set.
      */
@@ -119,6 +144,7 @@ public class PopulateWebauthnAuthenticationContext extends AbstractAuthenticatio
             return;
         }
         context.setUsername(usernameLookupStrategy.apply(profileRequestContext));
+        context.setWebAuthnClient(webauthnClient);
 
         log.debug("Created Webauthn authentication context");
     }
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
index 3cfb69c..f4985d8 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ValidateWebAuthnAssertion.java
@@ -10,6 +10,10 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 
+import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
 import net.shibboleth.idp.authn.AbstractValidationAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -59,7 +63,19 @@ public class ValidateWebAuthnAssertion extends AbstractValidationAction {
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
         
-        context.setUsername("jblogs");
+        final PublicKeyCredential<AuthenticatorAssertionResponse, ClientRegistrationExtensionOutputs> assertion =
+                context.getAssertionResponse();
+        
+        if (assertion == null) {
+            log.warn("{} No signature was found in the assertion, {} can not authenticate ", 
+                    getLogPrefix(),context.getUsername());
+            handleError(profileRequestContext, authenticationContext, "InvalidResponseType",
+                    AuthnEventIds.INVALID_CREDENTIALS);
+            recordFailure(profileRequestContext);              
+            return; 
+        }
+        
+        
         buildAuthenticationResult(profileRequestContext, authenticationContext);
         
     }
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 c8209e5..3396097 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
@@ -82,6 +82,59 @@
                 c:_0="#{'%{idp.authn.webauthn.admin.registration.defaultAuthenticationMethods:}'.trim()}" />
          </property>
     </bean>
+    
+     <!-- 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.
+        Note, this mapper has very specific features which allows the correct construction of the WebAuthn
+        registration and authentication options for requests.
+     
+      -->
+    <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_ABSENT" />
+        </property>
+    </bean>
+    
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="shibboleth.authn.WebAuthn.JSONObjectMapper" p:targetMethod="configure">
+        <property name="arguments">
+            <list>
+            <util:constant static-field="com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES" />
+            <value type="java.lang.Boolean">true</value>
+            </list>
+        </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="registerModule">
+        <property name="arguments">
+            <bean class="com.fasterxml.jackson.datatype.jdk8.Jdk8Module" />
+        </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>
\ 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
index 303fa5a..ddeb6a4 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -20,6 +20,9 @@
             class="net.shibboleth.idp.plugin.authn.webauthn.impl.UsernameFromAuthenticationContextLookupStrategy"/>
         </property>
     </bean>
+    
+    <bean id="CreatePublicKeyCredentialCreationOptions" scope="prototype"
+        class=" net.shibboleth.idp.plugin.authn.webauthn.impl.CreatePublicKeyCredentialCreationOptions"/>
         
     <bean id="ExtractPublicKeyCredentialFromFormRequest" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractPublicKeyCredentialFromFormRequest"
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
index 0ac016a..6d86fb1 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -26,7 +26,7 @@
      <action-state id="PopulateWebAuthnContext">
         <evaluate expression="PopulateWebauthnAuthenticationContext"/>
         <evaluate expression="GenerateServerChallenge"/>
-        <evaluate expression="CreatePublicKeyCredentialRequestOptions"/>
+        <evaluate expression="CreatePublicKeyCredentialCreationOptions"/>
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="DisplayWebAuthnView" />    
     </action-state>     
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
index 8dce35c..41b6115 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
@@ -10,45 +10,14 @@
     default-init-method="initialize" default-destroy-method="destroy">
 
     <bean id="AbstractPopulateWebauthnAuthenticationContext" scope="prototype" abstract="true"
-        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext" />
+        class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebauthnAuthenticationContext" 
+        p:webauthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebauthnAuthenticationClientFactory')}"/>
 
     <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/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js
new file mode 100644
index 0000000..d6ef38b
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js
@@ -0,0 +1,189 @@
+// src/webauthn-json/base64url.ts
+function base64urlToBuffer(baseurl64String) {
+  const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4);
+  const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding;
+  const str = atob(base64String);
+  const buffer = new ArrayBuffer(str.length);
+  const byteView = new Uint8Array(buffer);
+  for (let i = 0; i < str.length; i++) {
+    byteView[i] = str.charCodeAt(i);
+  }
+  return buffer;
+}
+function bufferToBase64url(buffer) {
+  const byteView = new Uint8Array(buffer);
+  let str = "";
+  for (const charCode of byteView) {
+    str += String.fromCharCode(charCode);
+  }
+  const base64String = btoa(str);
+  const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+  return base64urlString;
+}
+
+// src/webauthn-json/convert.ts
+var copyValue = "copy";
+var convertValue = "convert";
+function convert(conversionFn, schema2, input) {
+  if (schema2 === copyValue) {
+    return input;
+  }
+  if (schema2 === convertValue) {
+    return conversionFn(input);
+  }
+  if (schema2 instanceof Array) {
+    return input.map((v) => convert(conversionFn, schema2[0], v));
+  }
+  if (schema2 instanceof Object) {
+    const output = {};
+    for (const [key, schemaField] of Object.entries(schema2)) {
+      if (schemaField.deriveFn) {
+        const v = schemaField.deriveFn(input);
+        if (v !== void 0) {
+          input[key] = v;
+        }
+      }
+      if (!(key in input)) {
+        if (schemaField.required) {
+          throw new Error(`Missing key: ${key}`);
+        }
+        continue;
+      }
+      if (input[key] == null) {
+        output[key] = null;
+        continue;
+      }
+      output[key] = convert(conversionFn, schemaField.schema, input[key]);
+    }
+    return output;
+  }
+}
+function derived(schema2, deriveFn) {
+  return {
+    required: true,
+    schema: schema2,
+    deriveFn
+  };
+}
+function required(schema2) {
+  return {
+    required: true,
+    schema: schema2
+  };
+}
+function optional(schema2) {
+  return {
+    required: false,
+    schema: schema2
+  };
+}
+
+// src/webauthn-json/basic/schema.ts
+var publicKeyCredentialDescriptorSchema = {
+  type: required(copyValue),
+  id: required(convertValue),
+  transports: optional(copyValue)
+};
+var simplifiedExtensionsSchema = {
+  appid: optional(copyValue),
+  appidExclude: optional(copyValue),
+  credProps: optional(copyValue)
+};
+var simplifiedClientExtensionResultsSchema = {
+  appid: optional(copyValue),
+  appidExclude: optional(copyValue),
+  credProps: optional(copyValue)
+};
+var credentialCreationOptions = {
+  publicKey: required({
+    rp: required(copyValue),
+    user: required({
+      id: required(convertValue),
+      name: required(copyValue),
+      displayName: required(copyValue)
+    }),
+    challenge: required(convertValue),
+    pubKeyCredParams: required(copyValue),
+    timeout: optional(copyValue),
+    excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),
+    authenticatorSelection: optional(copyValue),
+    attestation: optional(copyValue),
+    extensions: optional(simplifiedExtensionsSchema)
+  }),
+  signal: optional(copyValue)
+};
+var publicKeyCredentialWithAttestation = {
+  type: required(copyValue),
+  id: required(copyValue),
+  rawId: required(convertValue),
+  response: required({
+    clientDataJSON: required(convertValue),
+    attestationObject: required(convertValue),
+    transports: derived(copyValue, (response) => response.getTransports?.() || [])
+  }),
+  clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
+};
+var credentialRequestOptions = {
+  mediation: optional(copyValue),
+  publicKey: required({
+    challenge: required(convertValue),
+    timeout: optional(copyValue),
+    rpId: optional(copyValue),
+    allowCredentials: optional([publicKeyCredentialDescriptorSchema]),
+    userVerification: optional(copyValue),
+    extensions: optional(simplifiedExtensionsSchema)
+  }),
+  signal: optional(copyValue)
+};
+var publicKeyCredentialWithAssertion = {
+  type: required(copyValue),
+  id: required(copyValue),
+  rawId: required(convertValue),
+  response: required({
+    clientDataJSON: required(convertValue),
+    authenticatorData: required(convertValue),
+    signature: required(convertValue),
+    userHandle: required(convertValue)
+  }),
+  clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
+};
+var schema = {
+  credentialCreationOptions,
+  publicKeyCredentialWithAttestation,
+  credentialRequestOptions,
+  publicKeyCredentialWithAssertion
+};
+
+// src/webauthn-json/basic/api.ts
+function createRequestFromJSON(requestJSON) {
+  return convert(base64urlToBuffer, credentialCreationOptions, requestJSON);
+}
+function createResponseToJSON(credential) {
+  return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential);
+}
+async function create(requestJSON) {
+  const credential = await navigator.credentials.create(createRequestFromJSON(requestJSON));
+  return createResponseToJSON(credential);
+}
+function getRequestFromJSON(requestJSON) {
+  return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);
+}
+function getResponseToJSON(credential) {
+  return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential);
+}
+async function get(requestJSON) {
+  const credential = await navigator.credentials.get(getRequestFromJSON(requestJSON));
+  return getResponseToJSON(credential);
+}
+
+// src/webauthn-json/basic/supported.ts
+function supported() {
+  return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
+}
+export {
+  create,
+  get,
+  schema,
+  supported
+};
+//# sourceMappingURL=webauthn-json.js.map
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js.map b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js.map
new file mode 100644
index 0000000..9d6aa22
--- /dev/null
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/js/webauthn-json.js.map
@@ -0,0 +1,7 @@
+{
+  "version": 3,
+  "sources": ["../../src/webauthn-json/base64url.ts", "../../src/webauthn-json/convert.ts", "../../src/webauthn-json/basic/schema.ts", "../../src/webauthn-json/basic/api.ts", "../../src/webauthn-json/basic/supported.ts"],
+  "sourcesContent": ["export type Base64urlString = string;\n\nexport function base64urlToBuffer(\n  baseurl64String: Base64urlString,\n): ArrayBuffer {\n  // Base64url to Base64\n  const padding = \"==\".slice(0, (4 - (baseurl64String.length % 4)) % 4);\n  const base64String =\n    baseurl64String.replace(/-/g, \"+\").replace(/_/g, \"/\") + padding;\n\n  // Base64 to binary string\n  const str = atob(base64String);\n\n  // Binary string to buffer\n  const buffer = new ArrayBuffer(str.le [...]
+  "mappings": ";AAEO,2BACL,iBACa;AAEb,QAAM,UAAU,KAAK,MAAM,GAAI,KAAK,gBAAgB,SAAS,KAAM;AACnE,QAAM,eACJ,gBAAgB,QAAQ,MAAM,KAAK,QAAQ,MAAM,OAAO;AAG1D,QAAM,MAAM,KAAK;AAGjB,QAAM,SAAS,IAAI,YAAY,IAAI;AACnC,QAAM,WAAW,IAAI,WAAW;AAChC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,aAAS,KAAK,IAAI,WAAW;AAAA;AAE/B,SAAO;AAAA;AAGF,2BAA2B,QAAsC;AAEtE,QAAM,WAAW,IAAI,WAAW;AAChC,MAAI,MAAM;AACV,aAAW,YAAY,UAAU;AAC/B,WAAO,OAAO,aAAa;AAAA;AAI7B,QAAM,eAAe,KAAK;AAI1B,QAAM,kBAAkB,aACrB,QAAQ,OAAO,KACf,QAAQ,OAAO,KACf,QAAQ,MA [...]
+  "names": []
+}
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index ab1baf0..7703ce9 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -28,12 +28,38 @@
   <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>
+
+  <script type="module">
+    import * as webauthnJson from "$request.getContextPath()/js/webauthn-json.js";
+  
+    async function register() {
+        console.log("Register");
+        var pkCredOptions = $webauthnContext.publicKeyCredentialCreationOptions;
+        console.log("Raw request options", pkCredOptions);                 
+        const credentialJson = await webauthnJson.create({ publicKey: pkCredOptions }).catch(console.error);    
+        console.log("Credential: ", JSON.stringify(credentialJson));
+        try {
+            document.getElementById("publicKeyCredential").value = JSON.stringify(credentialJson);    
+        } catch (err) {
+            console.log(err);
+        }
+     };
+
+     function init() {
+          document.getElementById("registerButton").onclick = register;        
+          return false;
+     }
+     
+     window.onload = init;
+
+</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" )" />
@@ -46,9 +72,12 @@
       <section>
     
         <div class="content">
-          <div class="column one">
-            <button class="form-element form-button" onclick="register();">Register</button>
-    
+          <div class="column one">           
+            
+            <button class="form-element form-button" id="registerButton">Register</button>
+            <textarea id="publicKeyCredentialCreation" name="publicKeyCredentialCreation" rows="20" cols="50">
+            $webauthnContext.publicKeyCredentialCreationOptions</textarea>
+            
             <form action="$flowExecutionUrl" method="post">
               #parse("csrf/csrf.vm")
               <textarea id="publicKeyCredential" name="publicKeyCredential" rows="10" cols="50"></textarea>

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


More information about the commits mailing list