[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