clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

main
lofi / apps / reference / tests / auth_e2e_test.ts
auth_e2e_test.ts 4.6K
1import type { VirtualAuthenticatorHandle } from "@nzip/lofi/testing";23/**4 * A worked example of the `@nzip/lofi/testing` WebAuthn helper: install a CDP5 * virtual authenticator and drive a device-credential enroll -> authenticate6 * round-trip against the running app.7 *8 * This is an opt-in browser gate, not part of `deno task test`:9 * 1. Serve the app on a stable origin — for example `deno task build` followed10 * by `deno task preview`, or `deno task dev`. Enrollment is gated on a11 * stable RP-ID, so add the served hostname to `credentialOrigins` in12 * `src/app.ts` (127.0.0.1 is treated as local-only and refused).13 * 2. Point the test at that URL and run it directly:14 * LOFI_E2E_BASE_URL=https://app.example.com/ deno test -A tests/auth_e2e_test.ts15 *16 * Without LOFI_E2E_BASE_URL (or without Chromium installed) it skips, so the17 * default suite stays fast and never launches a browser.18 *19 * NOTE: CDP virtual authenticators do not model the WebAuthn PRF extension, so20 * this example covers enroll/authenticate only. PRF derivation is feature-21 * detected in the runtime and must be validated on a real device.22 */2324function e2eBaseURL(): string | undefined {25 // The default suite runs without --allow-env; treat a denied read as "skip".26 try {27 return Deno.env.get("LOFI_E2E_BASE_URL") || undefined;28 } catch {29 return undefined;30 }31}3233// Runs inside the browser page, so it may only touch WebAuthn DOM APIs. A minimal34// inline enroll -> authenticate round-trip against the page's own origin (its35// RP-ID); returns the base64url credential id observed on both operations.36const roundTripCredential = async (37 rpId: string,38): Promise<{ enrolled: string; asserted: string }> => {39 const toBase64Url = (buffer: ArrayBuffer): string => {40 let binary = "";41 for (const byte of new Uint8Array(buffer)) binary += String.fromCharCode(byte);42 return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");43 };44 const randomBytes = (length: number) => crypto.getRandomValues(new Uint8Array(length));4546 const created = await navigator.credentials.create({47 publicKey: {48 rp: { id: rpId, name: "lofi auth example" },49 user: { id: randomBytes(16), name: "example", displayName: "example" },50 challenge: randomBytes(32),51 pubKeyCredParams: [{ type: "public-key", alg: -7 }],52 authenticatorSelection: {53 residentKey: "required",54 requireResidentKey: true,55 userVerification: "required",56 },57 attestation: "none",58 timeout: 60_000,59 },60 }) as (PublicKeyCredential | null);61 if (!created) throw new Error("enrollment returned no credential");6263 const asserted = await navigator.credentials.get({64 publicKey: {65 rpId,66 challenge: randomBytes(32),67 userVerification: "required",68 timeout: 60_000,69 },70 }) as (PublicKeyCredential | null);71 if (!asserted) throw new Error("authentication returned no credential");7273 return {74 enrolled: toBase64Url(created.rawId),75 asserted: toBase64Url(asserted.rawId),76 };77};7879Deno.test("a virtual authenticator drives an enroll -> authenticate round-trip", async () => {80 const baseURL = e2eBaseURL();81 if (!baseURL) {82 console.log(83 "skipping auth example; set LOFI_E2E_BASE_URL to a stable-origin deployment to run it",84 );85 return;86 }8788 // Imported lazily so the default suite never loads Playwright.89 const { chromium } = await import("npm:playwright@1.61.1");90 const { BrowserUnavailableError, withVirtualAuthenticator } = await import("@nzip/lofi/testing");9192 let browser;93 try {94 browser = await chromium.launch({ headless: true });95 } catch (error) {96 // Match the fixture's missing-Chromium classification and skip cleanly.97 const message = error instanceof Error ? error.message : String(error);98 if (99 /executable doesn't exist|browser executable|playwright install|failed to launch/i.test(100 message,101 )102 ) {103 console.log(new BrowserUnavailableError({ cause: error }).message);104 return;105 }106 throw error;107 }108109 let authenticator: VirtualAuthenticatorHandle | undefined;110 try {111 const context = await browser.newContext({ baseURL });112 const page = await context.newPage();113 authenticator = await withVirtualAuthenticator(page);114 await page.goto(baseURL, { waitUntil: "domcontentloaded" });115116 const rpId = new URL(baseURL).hostname;117 const result = await page.evaluate(roundTripCredential, rpId);118 if (result.enrolled !== result.asserted) {119 throw new Error("the authenticated credential id must match the enrolled id");120 }121 } finally {122 await authenticator?.dispose();123 await browser.close();124 }125});