74 lines
2.7 KiB
JavaScript
74 lines
2.7 KiB
JavaScript
// CCN — passkeys: signing in or joining, and adding one from the account screen. Without it, or without WebAuthn, the demo sign-in remains.
|
|
|
|
const form = document.querySelector("[data-passkey-form]");
|
|
const supported = window.PublicKeyCredential
|
|
&& PublicKeyCredential.parseCreationOptionsFromJSON
|
|
&& PublicKeyCredential.parseRequestOptionsFromJSON;
|
|
|
|
if (form && supported) {
|
|
const button = form.querySelector("[data-passkey]");
|
|
const status = form.querySelector("[data-passkey-status]");
|
|
|
|
const post = async (path, body) => {
|
|
const response = await fetch(path, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const answer = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(answer.error ?? "Something went wrong. Try again.");
|
|
}
|
|
|
|
return answer;
|
|
};
|
|
|
|
button.hidden = false;
|
|
|
|
// The word for a browser without passkeys is on the page by default; this one has them.
|
|
for (const notice of document.querySelectorAll("[data-passkey-unsupported]")) {
|
|
notice.hidden = true;
|
|
}
|
|
|
|
button.addEventListener("click", async () => {
|
|
button.disabled = true;
|
|
status.textContent = "";
|
|
|
|
try {
|
|
// The button adds a passkey on the account screen, replaces them all from a recovery link, and otherwise signs in or joins.
|
|
const start = button.dataset.passkey === "add"
|
|
? { add: true }
|
|
: button.dataset.passkey === "recover"
|
|
? { recover: form.querySelector("[name=recover]").value }
|
|
: { email: form.querySelector("[name=email]").value };
|
|
const { mode, options } = await post("/sign-in/passkey/options", start);
|
|
let credential;
|
|
|
|
if (mode === "create") {
|
|
// A discoverable passkey, so the next sign-in needs no email.
|
|
options.authenticatorSelection = { residentKey: "required", userVerification: "preferred" };
|
|
credential = await navigator.credentials.create({
|
|
publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(options),
|
|
});
|
|
} else {
|
|
credential = await navigator.credentials.get({
|
|
publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(options),
|
|
});
|
|
}
|
|
|
|
const { location } = await post("/sign-in/passkey/finish", {
|
|
credential: credential.toJSON(),
|
|
return: form.querySelector("[name=return]")?.value,
|
|
});
|
|
|
|
window.location.assign(location);
|
|
} catch (error) {
|
|
status.textContent = error.name === "NotAllowedError"
|
|
? "That didn't go through: the passkey prompt was cancelled. Nothing was created or changed — try again."
|
|
: error.message;
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
}
|