add basic passkey template

This commit is contained in:
2026-08-03 06:04:02 -07:00
parent 1db9afd8cb
commit d34efd29ec
8 changed files with 953 additions and 0 deletions
+306
View File
@@ -0,0 +1,306 @@
/* [@simplewebauthn/browser@10.0.0] */
!function(e, t) {
"object" == typeof exports && "undefined" != typeof module ? t(exports)
: "function" == typeof define && define.amd
? define([ "exports" ], t)
: t((e = "undefined" != typeof globalThis ? globalThis : e || self)
.SimpleWebAuthnBrowser = {})
}(this, (function(e) {
"use strict";
function t(e) {
const t = new Uint8Array(e);
let r = "";
for (const e of t)
r += String.fromCharCode(e);
return btoa(r).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
function r(e) {
const t = e.replace(/-/g, "+").replace(/_/g, "/"),
r = (4 - t.length % 4) % 4, n = t.padEnd(t.length + r, "="),
o = atob(n), i = new ArrayBuffer(o.length), a = new Uint8Array(i);
for (let e = 0; e < o.length; e++)
a[e] = o.charCodeAt(e);
return i
}
function n() {
return void 0 !== window?.PublicKeyCredential &&
"function" == typeof window.PublicKeyCredential
}
function o(e) {
const {id : t} = e;
return { ...e, id: r(t), transports: e.transports }
}
function i(e) {
return "localhost" === e ||
/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(e)
}
class a extends Error {
constructor({message : e, code: t, cause: r, name: n}) {
super(e, {cause : r}), this.name = n ?? r.name, this.code = t
}
}
const s = new class {
createNewAbortSignal() {
if (this.controller) {
const e =
new Error("Cancelling existing WebAuthn API call for new one");
e.name = "AbortError", this.controller.abort(e)
}
const e = new AbortController;
return this.controller = e, e.signal
}
cancelCeremony() {
if (this.controller) {
const e = new Error("Manually cancelling existing WebAuthn API call");
e.name = "AbortError", this.controller.abort(e),
this.controller = void 0
}
}
}
, c = [ "cross-platform", "platform" ];
function l(e) {
if (e && !(c.indexOf(e) < 0))
return e
}
function u(e, t) {
console.warn(
`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${
e}. You should report this error to them.\n`,
t)
}
function d() {
if (!n())
return new Promise((e => e(!1)));
const e = window.PublicKeyCredential;
return void 0 === e.isConditionalMediationAvailable
? new Promise((e => e(!1)))
: e.isConditionalMediationAvailable()
}
e.WebAuthnAbortService = s, e.WebAuthnError = a,
e.base64URLStringToBuffer = r, e.browserSupportsWebAuthn = n,
e.browserSupportsWebAuthnAutofill = d, e.bufferToBase64URLString = t,
e.platformAuthenticatorIsAvailable = function() {
return n() ? PublicKeyCredential
.isUserVerifyingPlatformAuthenticatorAvailable()
: new Promise((e => e(!1)))
}, e.startAuthentication = async function(e, c = !1) {
if (!n())
throw new Error("WebAuthn is not supported in this browser");
let u;
0 !== e.allowCredentials?.length && (u = e.allowCredentials?.map(o));
const h = {...e, challenge : r(e.challenge), allowCredentials : u},
f = {};
if (c) {
if (!await d())
throw Error("Browser does not support WebAuthn autofill");
if (document.querySelectorAll("input[autocomplete$='webauthn']")
.length < 1)
throw Error(
'No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');
f.mediation = "conditional", h.allowCredentials = []
}
let p;
f.publicKey = h, f.signal = s.createNewAbortSignal();
try {
p = await navigator.credentials.get(f)
} catch (e) {
throw function({error : e, options : t}) {
const {publicKey : r} = t;
if (!r)
throw Error("options was missing required publicKey property");
if ("AbortError" === e.name) {
if (t.signal instanceof AbortSignal)
return new a({
message : "Authentication ceremony was sent an abort signal",
code : "ERROR_CEREMONY_ABORTED",
cause : e
})
} else {
if ("NotAllowedError" === e.name)
return new a({
message : e.message,
code : "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
cause : e
});
if ("SecurityError" === e.name) {
const t = window.location.hostname;
if (!i(t))
return new a({
message : `${window.location.hostname} is an invalid domain`,
code : "ERROR_INVALID_DOMAIN",
cause : e
});
if (r.rpId !== t)
return new a({
message : `The RP ID "${r.rpId}" is invalid for this domain`,
code : "ERROR_INVALID_RP_ID",
cause : e
})
} else if ("UnknownError" === e.name)
return new a({
message :
"The authenticator was unable to process the specified options, or could not create a new assertion signature",
code : "ERROR_AUTHENTICATOR_GENERAL_ERROR",
cause : e
})
}
return e
}({error : e, options : f})
}
if (!p)
throw new Error("Authentication was not completed");
const {id : R, rawId : w, response : E, type : g} = p;
let A;
return E.userHandle && (A = t(E.userHandle)), {
id: R, rawId: t(w), response: {
authenticatorData: t(E.authenticatorData),
clientDataJSON: t(E.clientDataJSON),
signature: t(E.signature),
userHandle: A
},
type: g, clientExtensionResults: p.getClientExtensionResults(),
authenticatorAttachment: l(p.authenticatorAttachment)
}
}, e.startRegistration = async function(e) {
if (!n())
throw new Error("WebAuthn is not supported in this browser");
const c = {
publicKey : {
...e,
challenge : r(e.challenge),
user : {...e.user, id : r(e.user.id)},
excludeCredentials : e.excludeCredentials?.map(o)
}
};
let d;
c.signal = s.createNewAbortSignal();
try {
d = await navigator.credentials.create(c)
} catch (e) {
throw function({error : e, options : t}) {
const {publicKey : r} = t;
if (!r)
throw Error("options was missing required publicKey property");
if ("AbortError" === e.name) {
if (t.signal instanceof AbortSignal)
return new a({
message : "Registration ceremony was sent an abort signal",
code : "ERROR_CEREMONY_ABORTED",
cause : e
})
} else if ("ConstraintError" === e.name) {
if (!0 === r.authenticatorSelection?.requireResidentKey)
return new a({
message :
"Discoverable credentials were required but no available authenticator supported it",
code :
"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",
cause : e
});
if ("required" === r.authenticatorSelection?.userVerification)
return new a({
message :
"User verification was required but no available authenticator supported it",
code : "ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",
cause : e
})
} else {
if ("InvalidStateError" === e.name)
return new a({
message : "The authenticator was previously registered",
code : "ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",
cause : e
});
if ("NotAllowedError" === e.name)
return new a({
message : e.message,
code : "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",
cause : e
});
if ("NotSupportedError" === e.name)
return 0 === r.pubKeyCredParams
.filter((e => "public-key" === e.type))
.length
? new a({
message :
'No entry in pubKeyCredParams was of type "public-key"',
code : "ERROR_MALFORMED_PUBKEYCREDPARAMS",
cause : e
})
: new a({
message :
"No available authenticator supported any of the specified pubKeyCredParams algorithms",
code :
"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",
cause : e
});
if ("SecurityError" === e.name) {
const t = window.location.hostname;
if (!i(t))
return new a({
message : `${window.location.hostname} is an invalid domain`,
code : "ERROR_INVALID_DOMAIN",
cause : e
});
if (r.rp.id !== t)
return new a({
message : `The RP ID "${r.rp.id}" is invalid for this domain`,
code : "ERROR_INVALID_RP_ID",
cause : e
})
} else if ("TypeError" === e.name) {
if (r.user.id.byteLength < 1 || r.user.id.byteLength > 64)
return new a({
message : "User ID was not between 1 and 64 characters",
code : "ERROR_INVALID_USER_ID_LENGTH",
cause : e
})
} else if ("UnknownError" === e.name)
return new a({
message :
"The authenticator was unable to process the specified options, or could not create a new credential",
code : "ERROR_AUTHENTICATOR_GENERAL_ERROR",
cause : e
})
}
return e
}({error : e, options : c})
}
if (!d)
throw new Error("Registration was not completed");
const {id : h, rawId : f, response : p, type : R} = d;
let w, E, g, A;
if ("function" == typeof p.getTransports && (w = p.getTransports()),
"function" == typeof p.getPublicKeyAlgorithm)
try {
E = p.getPublicKeyAlgorithm()
} catch (e) {
u("getPublicKeyAlgorithm()", e)
}
if ("function" == typeof p.getPublicKey)
try {
const e = p.getPublicKey();
null !== e && (g = t(e))
} catch (e) {
u("getPublicKey()", e)
}
if ("function" == typeof p.getAuthenticatorData)
try {
A = t(p.getAuthenticatorData())
} catch (e) {
u("getAuthenticatorData()", e)
}
return {
id: h, rawId: t(f), response: {
attestationObject: t(p.attestationObject),
clientDataJSON: t(p.clientDataJSON),
transports: w,
publicKeyAlgorithm: E,
publicKey: g,
authenticatorData: A
},
type: R, clientExtensionResults: d.getClientExtensionResults(),
authenticatorAttachment: l(d.authenticatorAttachment)
}
}, Object.defineProperty(e, "__esModule", {value : !0})
}));
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passkey</title>
<link href="bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container d-flex justify-content-center align-items-center vh-100">
<div class="bg-light p-5 rounded w-50">
<h1 class="mb-4 text-center">🔑 Passkey</h1>
<div class="text-center" id="message"></div>
<div class="mb-3">
<input type="text" class="form-control" id="username" placeholder="username">
</div>
<div class="d-grid gap-2">
<div class="row">
<div class="col">
<button class="btn btn-primary w-100" id="registerButton">Register</button>
</div>
<div class="col">
<button class="btn btn-primary w-100" id="loginButton">Login</button>
</div>
</div>
</div>
</div>
<a href="/private">PRIVATE</a>
</div>
<script src="index.es5.umd.min.js"></script>
<script src="script.js"></script>
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
document.getElementById('registerButton').addEventListener('click', register);
document.getElementById('loginButton').addEventListener('click', login);
function showMessage(message, isError = false) {
const messageElement = document.getElementById('message');
messageElement.textContent = message;
messageElement.style.color = isError ? 'red' : 'green';
}
async function register() {
// Retrieve the username from the input field
const username = document.getElementById('username').value;
try {
// Get registration options from your server. Here, we also receive the challenge.
const response = await fetch('/api/passkey/registerStart', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username})
});
// Check if the registration options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error('User already exists or failed to get registration options from server: ' + msg);
}
// Convert the registration options to JSON.
const options = await response.json();
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new attestation is created. This also means a new public-private-key pair is created.
const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options.publicKey);
// Send attestationResponse back to server for verification and storage.
const verificationResponse = await fetch('/api/passkey/registerFinish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(attestationResponse)
});
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
showMessage(msg, false);
} else {
showMessage(msg, true);
}
} catch
(error) {
showMessage('Error: ' + error.message, true);
}
}
async function login() {
// Retrieve the username from the input field
const username = document.getElementById('username').value;
try {
// Get login options from your server. Here, we also receive the challenge.
const response = await fetch('/api/passkey/loginStart', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: username})
});
// Check if the login options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error('Failed to get login options from server: ' + msg);
}
// Convert the login options to JSON.
const options = await response.json();
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new assertionResponse is created. This also means that the challenge has been signed.
const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options.publicKey);
// Send assertionResponse back to server for verification.
const verificationResponse = await fetch('/api/passkey/loginFinish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(assertionResponse)
});
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
showMessage(msg, false);
} else {
showMessage(msg, true);
}
} catch (error) {
showMessage('Error: ' + error.message, true);
}
}