From b690408660e1edb85b09dffa804e4e3be3193532 Mon Sep 17 00:00:00 2001 From: TruongNM Date: Wed, 26 Jul 2023 22:52:41 +0700 Subject: [PATCH] Implement passkey autofill --- .../controllers/new_session_controller.js | 6 ++- app/javascript/credential.js | 48 ++++++++++++++++--- app/views/sessions/new.html.erb | 2 +- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/app/javascript/controllers/new_session_controller.js b/app/javascript/controllers/new_session_controller.js index 096b65f..503c009 100644 --- a/app/javascript/controllers/new_session_controller.js +++ b/app/javascript/controllers/new_session_controller.js @@ -6,11 +6,15 @@ import { MDCTextField } from '@material/textfield'; export default class extends Controller { static targets = ["usernameField"] + connect() { + Credential.autofill(); + } + create(event) { var [data, status, xhr] = event.detail; console.log(data); var credentialOptions = data; - Credential.get(credentialOptions); + Credential.get(credentialOptions, "optional"); } error(event) { diff --git a/app/javascript/credential.js b/app/javascript/credential.js index 3fc02ae..1245cf1 100644 --- a/app/javascript/credential.js +++ b/app/javascript/credential.js @@ -1,4 +1,5 @@ import * as WebAuthnJSON from "@github/webauthn-json" + import { showMessage } from "messenger"; function getCSRFToken() { @@ -10,8 +11,8 @@ function getCSRFToken() { } } -function callback(url, body) { - fetch(url, { +function _fetch(url, body) { + return fetch(url, { method: "POST", body: JSON.stringify(body), headers: { @@ -20,7 +21,11 @@ function callback(url, body) { "X-CSRF-Token": getCSRFToken() }, credentials: 'same-origin' - }).then(function(response) { + }) +} + +function callback(url, body) { + _fetch(url, body).then(function(response) { if (response.ok) { window.location.replace("/") } else if (response.status < 500) { @@ -31,6 +36,37 @@ function callback(url, body) { }); } +function getCredentialOptions() { + return _fetch("/session/options").then(function(response) { + if (response.ok) { + return response.json(); + } + if (response.status < 500) { + response.text().then(showMessage); + } else { + showMessage("Sorry, something wrong happened."); + } + }); +} + +async function autofill() { + if (WebAuthnJSON.supported && + PublicKeyCredential.isConditionalMediationAvailable) { + try { + const cma = await PublicKeyCredential.isConditionalMediationAvailable(); + if (cma) { + var credentialOptions = await getCredentialOptions(); + get(credentialOptions, "conditional"); + } + } catch (e) { + console.error(e); + if (e.name !== "NotAllowedError") { + alert(e.message); + } + } + } +} + function create(callbackUrl, credentialOptions) { WebAuthnJSON.create({ "publicKey": credentialOptions }).then(function(credential) { callback(callbackUrl, credential); @@ -41,8 +77,8 @@ function create(callbackUrl, credentialOptions) { console.log("Creating new public key credential..."); } -function get(credentialOptions) { - WebAuthnJSON.get({ "publicKey": credentialOptions }).then(function(credential) { +function get(credentialOptions, mediationOption) { + WebAuthnJSON.get({ "publicKey": credentialOptions, "mediation": mediationOption }).then(function(credential) { callback("/session/callback", credential); }).catch(function(error) { showMessage(error); @@ -51,4 +87,4 @@ function get(credentialOptions) { console.log("Getting public key credential..."); } -export { create, get } +export { autofill, create, get } diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index ccfc923..e312e0c 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -8,7 +8,7 @@ <%= form_with scope: :session, url: session_path, id: "new-session", data: { controller: "new-session", action: "ajax:success->new-session#create ajax:error->new-session#error" } do |form| %>
- <%= form.text_field :username, class: "mdc-text-field__input", placeholder: "Username", required: true, autocapitalize: "none", "aria-controls" => "username-helper-text" %> + <%= form.text_field :username, class: "mdc-text-field__input", autocomplete: "username webauthn", placeholder: "Username", required: true, autocapitalize: "none", "aria-controls" => "username-helper-text" %>