Skip to content

Commit

Permalink
adding 'toUTF8String()' and 'fromUTF8String()' utils to library, impr…
Browse files Browse the repository at this point in the history
…oving tests UX to show the User ID in 'success' toast messages for registration and authentication
  • Loading branch information
getify committed Mar 15, 2024
1 parent 87f998c commit 660ad5c
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 29 deletions.
29 changes: 25 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,33 +95,46 @@ const supportsConditionalMediation = (
// ********************************

export {
resetAbortReason,
// feature support tests
supportsWebAuthn,
supportsConditionalMediation,

// main API
regDefaults,
register,
authDefaults,
auth,
verifyAuthResponse,

// helper utils
packPublicKeyJSON,
unpackPublicKeyJSON,
toBase64String,
fromBase64String,
toUTF8String,
fromUTF8String,
resetAbortReason,
};
var publicAPI = {
// feature support tests
supportsWebAuthn,
supportsConditionalMediation,

// main API
regDefaults,
register,
authDefaults,
auth,
verifyAuthResponse,

// helper utils
packPublicKeyJSON,
unpackPublicKeyJSON,
toBase64String,
fromBase64String,
toUTF8String,
fromUTF8String,
resetAbortReason,
};
export default publicAPI;

Expand All @@ -141,7 +154,7 @@ async function register(regOptions = regDefaults()) {
let regResult = await navigator.credentials.create(regOptions);

let regClientDataRaw = new Uint8Array(regResult.response.clientDataJSON);
let regClientData = JSON.parse(sodium.to_string(regClientDataRaw));
let regClientData = JSON.parse(toUTF8String(regClientDataRaw));
if (regClientData.type != "webauthn.create") {
throw new Error("Invalid registration response");
}
Expand Down Expand Up @@ -308,7 +321,7 @@ async function auth(authOptions = authDefaults()) {

let authResult = await navigator.credentials.get(authOptions);
let authClientDataRaw = new Uint8Array(authResult.response.clientDataJSON);
let authClientData = JSON.parse(sodium.to_string(authClientDataRaw));
let authClientData = JSON.parse(toUTF8String(authClientDataRaw));
if (authClientData.type != "webauthn.get") {
throw new Error("Invalid auth response");
}
Expand Down Expand Up @@ -573,7 +586,7 @@ async function computeVerificationData(authDataRaw,clientDataRaw) {

async function checkRPID(rpIDHash,origRPID) {
var originHash = await computeSHA256Hash(
sodium.from_string(origRPID)
fromUTF8String(origRPID)
);
return (
rpIDHash.length > 0 &&
Expand Down Expand Up @@ -677,3 +690,11 @@ function toBase64String(val) {
function fromBase64String(val) {
return sodium.from_base64(val,sodium.base64_variants.ORIGINAL);
}

function toUTF8String(val) {
return sodium.to_string(val);
}

function fromUTF8String(val) {
return sodium.from_string(val);
}
4 changes: 3 additions & 1 deletion test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ <h3><a href="https://github.com/mylofi/webauthn-local-client">Github</a></h3>
<p>
Before running these tests, make sure you're on a device that supports <a href="https://webauthn.io/" target="_blank">Web Authentication</a>, or alternatively use browser DevTools to <a href="https://developer.chrome.com/docs/devtools/webauthn" target="_blank">setup a virtual authenticator in Chrome</a>, or similar in Safari, or <a href="https://addons.mozilla.org/en-US/firefox/addon/webdevauthn/" target="_blank">this Firefox add-on</a>.
</p>
<hr>
<p>
<strong>NOTE:</strong> This test-bed does not store anything (LocalStorage, server, etc), so to prevent confusion, on each page load you should reset the authenticator by removing any credentials created on a previous page load and run of these tests.
<strong>NOTE:</strong> This test-bed <strong>does not persist</strong> (in LocalStorage, on a server, etc) any credentials or other information entered here. To reduce confusion, on each page load you should probably reset your device's authenticator (or virtual authenticator) by removing any credentials created on previous runs of these tests.
</p>
<hr>
<h2>Steps To Run (Re-)Registration [2] and Authentication [4] Tests:</h2>
<ol>
<li>Register a new credential. Enter any username you like. Also put in any text you want for User ID, or click the "Generate Random" button. Make sure to copy the User ID to your clipboard <strong>before</strong> clicking the "Register" button.</li>
Expand Down
62 changes: 38 additions & 24 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {
supportsWebAuthn,
supportsConditionalMediation,
resetAbortReason,

register,
regDefaults,
auth,
authDefaults,
verifyAuthResponse,

packPublicKeyJSON,
unpackPublicKeyJSON,
toUTF8String,
resetAbortReason,
}
// swap "src" for "dist" here to test against the dist/* files
from "webauthn-local-client/src";
Expand Down Expand Up @@ -39,27 +42,27 @@ function ready() {

registerBtn.addEventListener(
"click",
() => promptRegister(/*newRegistration=*/true),
() => promptRegister(/*isNewRegistration=*/true),
false
);
reRegisterBtn.addEventListener(
"click",
() => promptRegister(/*newRegistration=*/false),
() => promptRegister(/*isNewRegistration=*/false),
false
);
authBtn.addEventListener("click",promptAuth,false);
registeredCredentialsEl.addEventListener("click",onAuthCredential,true);
}

async function promptRegister(newRegistration = true) {
async function promptRegister(isNewRegistration = true) {
var registerNameEl;
var registerIDEl;
var generateIDBtn;
var copyBtn;

var result = await Swal.fire({
title: (
newRegistration ? "Register New Credential" : "Re-register Credential"
isNewRegistration ? "Register New Credential" : "Re-register Credential"
),
html: `
<p>
Expand All @@ -74,15 +77,15 @@ async function promptRegister(newRegistration = true) {
<input type="text" id="register-id" class="swal2-input">
</label><br>
${
newRegistration ? `
isNewRegistration ? `
<button type="button" id="generate-id-btn" class="swal2-styled swal2-default-outline modal-btn">Generate Random</button>
` : ""
}
<button type="button" id="copy-user-id-btn" class="swal2-styled swal2-default-outline modal-btn">Copy</button>
</p>
`,
showConfirmButton: true,
confirmButtonText: newRegistration ? "Register" : "Re-register",
confirmButtonText: isNewRegistration ? "Register" : "Re-register",
confirmButtonColor: "darkslateblue",
showCancelButton: true,
cancelButtonColor: "darkslategray",
Expand Down Expand Up @@ -124,7 +127,7 @@ async function promptRegister(newRegistration = true) {
}
if (!registerID) {
Swal.showValidationMessage(
newRegistration ?
isNewRegistration ?
"Please enter a User ID (or generate a new one)" :
"Please enter an existing User ID"
);
Expand All @@ -139,7 +142,7 @@ async function promptRegister(newRegistration = true) {
return registerCredential(
result.value.registerName,
result.value.registerID,
newRegistration
isNewRegistration
);
}

Expand Down Expand Up @@ -178,7 +181,7 @@ async function promptRegister(newRegistration = true) {
}
}

async function registerCredential(name,userIDStr,newRegistration = true) {
async function registerCredential(name,userIDStr,isNewRegistration = true) {
var userID = sodium.from_string(userIDStr);
var regOptions = regDefaults({
user: {
Expand All @@ -190,7 +193,7 @@ async function registerCredential(name,userIDStr,newRegistration = true) {
// only *exclude credentials* on a new registration, not
// on a re-registration
...(
(newRegistration) ? {
(isNewRegistration) ? {
excludeCredentials: Object.entries(credentialsByUserID)
.filter(([userID,entry]) => (userID == userIDStr))
.map(([userID,entry]) => ({
Expand All @@ -206,7 +209,7 @@ async function registerCredential(name,userIDStr,newRegistration = true) {
let regResult = await register(regOptions);
if (regResult.response) {
// on re-register, remove previous credential DOM element (if any)
if (!newRegistration && userIDStr in credentialsByUserID) {
if (!isNewRegistration && userIDStr in credentialsByUserID) {
let liEl = registeredCredentialsEl.querySelector(
`li[data-credential-id='${credentialsByUserID[userIDStr].credentialID}']`
);
Expand All @@ -218,6 +221,7 @@ async function registerCredential(name,userIDStr,newRegistration = true) {
// serialize credential info to DOM element
let li = document.createElement("li");
li.dataset.credentialId = regResult.response.credentialID;
// NOTE: deliberately used her to show using the 'packPublicKeyJSON()' util
li.dataset.publicKey = packPublicKeyJSON(regResult.response.publicKey,/*stringify=*/true);
li.innerHTML = `
<strong>${name}</strong>
Expand All @@ -243,16 +247,16 @@ async function registerCredential(name,userIDStr,newRegistration = true) {

console.log("regResult:",regResult);

showToast(
newRegistration ? "Registration Successful." : "Re-registration Successful."
);
let regType = isNewRegistration ? "Registering" : "Re-registering";
let forUserID = `(${toUTF8String(regResult.request.user.id)})`;
showToast(`${regType} ${forUserID} successful.`);
}
}
catch (err) {
logError(err);

if (
newRegistration &&
isNewRegistration &&
err.cause instanceof Error
) {
let errorString = err.cause.toString();
Expand All @@ -264,11 +268,12 @@ async function registerCredential(name,userIDStr,newRegistration = true) {
}
}

showError(
newRegistration ?
"Registering credential failed. Please try again." :
"Re-registering credential failed. Please try again."
let regType = (
isNewRegistration ?
"Registering" :
"Re-registering"
);
showError(`${regType} credential failed. Please try again.`);
}
}

Expand Down Expand Up @@ -435,7 +440,7 @@ async function promptProvideAuth() {
// show the User ID in the input box, for UX purposes
if (authResult && authResult.response && authResult.response.userID) {
userIDEl.readonly = true;
userIDEl.value = (new TextDecoder()).decode(authResult.response.userID);
userIDEl.value = toUTF8String(authResult.response.userID);
userIDEl.select();

// brief pause to ensure user can see their User ID
Expand Down Expand Up @@ -494,9 +499,12 @@ async function onAuthCredential(evt) {
if (evt.target.matches(".cred-auth-btn")) {
let liEl = evt.target.closest("li[data-credential-id]");
let credentialID = liEl.dataset.credentialId;
// NOTE: deliberately used her to show using the 'unpackPublicKeyJSON()' util
let publicKey = unpackPublicKeyJSON(liEl.dataset.publicKey);
let authResult = await onAuth(credentialID,publicKey);
return checkAuthResponse(authResult,credentialID,publicKey);
if (authResult) {
return checkAuthResponse(authResult,credentialID,publicKey);
}
}
}

Expand All @@ -522,8 +530,14 @@ async function checkAuthResponse(authResult,credentialID,publicKey) {
}
}
return void (
authSuccess ?
showToast("Authentication successful.") :
authSuccess ? (
showToast(`Authentication${(
"userID" in authResult.response ?
` (${toUTF8String(authResult.response.userID)})` :
""
)} successful.`)
) :

showError("Authentication failed.")
);
}
Expand Down

0 comments on commit 660ad5c

Please sign in to comment.