diff --git a/edb/server/protocol/auth_ext/_static/utils.js b/edb/server/protocol/auth_ext/_static/utils.js index a1e0cdd2ef8..6e04ac97ca6 100644 --- a/edb/server/protocol/auth_ext/_static/utils.js +++ b/edb/server/protocol/auth_ext/_static/utils.js @@ -21,3 +21,35 @@ export function encodeBase64Url(bytes) { .replace(/\//g, "_") .replace(/=/g, ""); } + +/** + * Parse an HTTP Response object. Allows passing in custom handlers for + * different status codes and error.type values + * + * @param {Response} response + * @param {Function[]=} handlers + */ +export async function parseResponseAsJSON(response, handlers = []) { + const bodyText = await response.text(); + + if (!response.ok) { + let error; + try { + error = JSON.parse(bodyText); + } catch (e) { + throw new Error( + `Failed to parse body as JSON. Status: ${response.status} ${response.statusText}. Body: ${bodyText}` + ); + } + + for (const handler of handlers) { + handler(response, error); + } + + throw new Error( + `Response was not OK. Status: ${response.status} ${response.statusText}. Body: ${bodyText}` + ); + } + + return response.json(); +} diff --git a/edb/server/protocol/auth_ext/_static/webauthn-authenticate.js b/edb/server/protocol/auth_ext/_static/webauthn-authenticate.js index a46da28ddbf..8db77603b33 100644 --- a/edb/server/protocol/auth_ext/_static/webauthn-authenticate.js +++ b/edb/server/protocol/auth_ext/_static/webauthn-authenticate.js @@ -1,19 +1,26 @@ -import { decodeBase64Url, encodeBase64Url } from "./utils.js"; +import { + decodeBase64Url, + encodeBase64Url, + parseResponseAsJSON, +} from "./utils.js"; document.addEventListener("DOMContentLoaded", () => { - const registerForm = document.getElementById("email-factor"); + /** @type {HTMLFormElement | null} */ + const authenticateForm = document.getElementById("email-factor"); - if (registerForm === null) { + if (authenticateForm === null) { return; } - registerForm.addEventListener("submit", async (event) => { + authenticateForm.addEventListener("submit", async (event) => { if (event.submitter?.id !== "webauthn-signin") { return; } event.preventDefault(); - const formData = new FormData(/** @type {HTMLFormElement} */ registerForm); + const formData = new FormData( + /** @type {HTMLFormElement} */ authenticateForm + ); const email = formData.get("email"); const provider = "builtin::local_webauthn"; const challenge = formData.get("challenge"); @@ -151,9 +158,9 @@ async function authenticateAssertion(props) { signature: encodeBase64Url( new Uint8Array(props.assertion.response.signature) ), - userHandle: props.assertion.response.userHandle ? encodeBase64Url( - new Uint8Array(props.assertion.response.userHandle) - ) : null, + userHandle: props.assertion.response.userHandle + ? encodeBase64Url(new Uint8Array(props.assertion.response.userHandle)) + : null, }, }; @@ -170,19 +177,26 @@ async function authenticateAssertion(props) { }), }); - if (!authenticateResponse.ok) { - console.error( - "Failed to authenticate WebAuthn credentials:", - authenticateResponse.statusText - ); - console.error(await authenticateResponse.text()); - throw new Error("Failed to authenticate WebAuthn credentials"); - } - - try { - return await authenticateResponse.json(); - } catch (e) { - console.error("Failed to parse WebAuthn registration result:", e); - throw new Error("Failed to parse WebAuthn registration result"); - } + return await parseResponseAsJSON(authenticateResponse, [ + (response, error) => { + if (response.status === 401 && error?.type === "VerificationRequired") { + console.error( + "User's email is not verified", + response.statusText, + error + ); + throw new Error( + "Please verify your email before attempting to sign in." + ); + } + }, + (response, error) => { + console.error( + "Failed to authenticate WebAuthn credentials:", + response.statusText, + error + ); + throw new Error("Failed to authenticate WebAuthn credentials"); + }, + ]); }