// deno-lint-ignore-file const { startRegistration, startAuthentication } = SimpleWebAuthnBrowser; function setStatus(msg, isError = false) { const el = document.getElementById("statusMessage"); if (el) { el.textContent = msg; el.className = isError ? "error" : "success"; } } async function startWebAuthnRegistration(username, inviteCode) { setStatus(""); if (!username || !inviteCode) { setStatus("Username and Invite Code are required.", true); return; } try { // 1. Fetch challenge from API const resp = await fetch("/api/register/challenge", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, inviteCode }), }); let data; try { data = await resp.json(); } catch { const text = await resp.text().catch(() => ""); setStatus(`Challenge request failed (${resp.status}): ${text}`, true); return; } if (!resp.ok) { setStatus(data.error || "Failed to get registration challenge", true); return; } // 2. Pass challenge to authenticator let attResp; try { attResp = await startRegistration({ optionsJSON: data.options }); } catch (error) { if (error.name === "InvalidStateError") { setStatus("Authenticator was probably already registered.", true); } else { setStatus(error.message || "Registration failed on device", true); } throw error; } // Extract PRF client extension result let extensionResults; if (typeof attResp.getClientExtensionResults === "function") { extensionResults = attResp.getClientExtensionResults(); } else { extensionResults = attResp.clientExtensionResults || {}; } // 3. Send response back to verify const verificationResp = await fetch("/api/register/verify", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, inviteCode, response: { ...attResp, clientExtensionResults: extensionResults, }, }), }); let verificationJSON; try { verificationJSON = await verificationResp.json(); } catch { const text = await verificationResp.text().catch(() => ""); setStatus( `Verification failed (${verificationResp.status}): ${text}`, true, ); return; } if (verificationJSON.success) { setStatus("Registration successful! You can now log in."); setTimeout(() => { globalThis.location.href = "/login"; }, 2000); } else { setStatus( verificationJSON.error || "Registration verification failed", true, ); } } catch (err) { console.error(err); } } async function startWebAuthnLogin(username) { setStatus(""); try { // 1. Fetch challenge const resp = await fetch("/api/login/challenge", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username: username || "" }), }); let data; try { data = await resp.json(); } catch { const text = await resp.text().catch(() => ""); setStatus(`Login challenge failed (${resp.status}): ${text}`, true); return; } if (!resp.ok) { setStatus(data.error || "Failed to get login challenge", true); return; } // 2. Pass challenge to authenticator let asseResp; try { asseResp = await startAuthentication({ optionsJSON: data.options }); } catch (error) { setStatus(error.message || "Authentication failed on device", true); throw error; } // Extract PRF extension results let extensionResults; if (typeof asseResp.getClientExtensionResults === "function") { extensionResults = asseResp.getClientExtensionResults(); } else { extensionResults = asseResp.clientExtensionResults || {}; } let kekDerived = false; if (extensionResults?.prf?.results?.first) { try { // Base64Url decode the string into Uint8Array const base64UrlString = extensionResults.prf.results.first; const base64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/"); const binaryString = atob(base64); const prfOutput = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { prfOutput[i] = binaryString.charCodeAt(i); } const salt = new Uint8Array(32); // 32 byte salt for HKDF const info = new TextEncoder().encode("auth-yes:prf:device-share:v1"); const ikm = await crypto.subtle.importKey( "raw", prfOutput, { name: "HKDF" }, false, ["deriveKey"], ); const kek = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: salt, info: info, }, ikm, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"], ); console.log("WebAuthn PRF extension KEK derived successfully"); kekDerived = true; } catch (err) { console.error("Failed to derive KEK from PRF output:", err); } } else { console.log( "WebAuthn PRF extension not supported or no output returned. Proceeding with standard authentication.", ); } // 3. Send response back to verify const verificationResp = await fetch("/api/login/verify", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ response: asseResp, }), }); let verificationJSON; try { verificationJSON = await verificationResp.json(); } catch { const text = await verificationResp.text().catch(() => ""); setStatus( `Login verification failed (${verificationResp.status}): ${text}`, true, ); return; } if (verificationJSON.success) { setStatus("Login successful! Redirecting..."); setTimeout(() => { globalThis.location.href = "/dashboard"; }, 1000); } else { setStatus(verificationJSON.error || "Login verification failed", true); } } catch (err) { console.error(err); } }