Files
auth-tools/tests/browser/workbench.spec.ts
T

183 lines
6.8 KiB
TypeScript

import { expect, test } from "@playwright/test";
import jsQR from "jsqr";
import { bytesToBase32 } from "../../src/crypto/encoding";
import { parseOtpAuth } from "../../src/otp/profile";
test("displays scannable imported TOTP and HOTP tokens on a dark mobile screen", async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.emulateMedia({ colorScheme: "dark" });
const requests: string[] = [];
page.on("request", (request) => requests.push(request.url()));
await page.goto("/deep/nested/auth/");
await page.getByRole("button", { name: "Import & migration" }).click();
await page
.getByPlaceholder("Paste migration data. It never leaves this tab.")
.fill(
"otpauth://totp/Ex%C3%A4mple:alice%40example.test?secret=JBSWY3DPEHPK3PXP&issuer=Ex%C3%A4mple&algorithm=SHA256&digits=8&period=45\n" +
"otpauth://hotp/Other:bob?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Other&counter=18446744073709551615",
);
await page.getByRole("button", { name: "Inspect import" }).click();
for (const account of ["Exämple: alice@example.test", "Other: bob"]) {
const trigger = page.getByRole("button", {
name: `Show QR code for ${account}`,
exact: true,
});
await trigger.click();
const dialog = page.getByRole("dialog", { name: "OTP token QR code" });
await expect(dialog).toBeVisible();
const svg = dialog.getByRole("img", { name: "OTP provisioning QR code" });
await expect(svg).toBeVisible();
const bounds = await svg.boundingBox();
expect(bounds!.width).toBeGreaterThan(250);
expect(bounds!.height).toBeCloseTo(bounds!.width, 0);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390);
const pixels = await svg.evaluate(async (element) => {
const source = new XMLSerializer().serializeToString(element);
const url = URL.createObjectURL(
new Blob([source], { type: "image/svg+xml" }),
);
try {
const image = new Image();
image.src = url;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = canvas.height = 640;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0, 640, 640);
return Array.from(context.getImageData(0, 0, 640, 640).data);
} finally {
URL.revokeObjectURL(url);
}
});
expect(pixels.slice(0, 4)).toEqual([255, 255, 255, 255]);
const decoded = jsQR(new Uint8ClampedArray(pixels), 640, 640);
expect(decoded).not.toBeNull();
const { profile } = parseOtpAuth(decoded!.data);
if (account.startsWith("Exämple")) {
expect(profile).toMatchObject({
kind: "totp",
issuer: "Exämple",
account: "alice@example.test",
algorithm: "SHA-256",
digits: 8,
period: 45,
});
expect(bytesToBase32(profile.secret)).toBe("JBSWY3DPEHPK3PXP");
} else {
expect(profile).toMatchObject({
kind: "hotp",
issuer: "Other",
account: "bob",
counter: 18446744073709551615n,
});
expect(bytesToBase32(profile.secret)).toBe(
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ",
);
}
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
await expect(trigger).toBeFocused();
}
expect(
requests.every((url) => new URL(url).origin === "http://127.0.0.1:4173"),
).toBe(true);
});
test("runs from a nested path and keeps authentication material local", async ({
page,
}) => {
const requests: string[] = [];
page.on("request", (request) => requests.push(request.url()));
await page.goto("/deep/nested/auth/");
await expect(page.getByText("Sensitive session · memory only")).toBeVisible();
await expect(
page.getByRole("heading", { name: "Credential parameters" }),
).toBeVisible();
await page.getByLabel("Issuer").fill("Example");
await page.getByLabel("Account").fill("alice@example.test");
await expect(page.getByText("TOTP code")).toBeVisible();
await page.getByRole("button", { name: "Show QR code", exact: true }).click();
await expect(page.locator(".qr svg")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toHaveCount(0);
expect(
requests.every((url) => new URL(url).origin === "http://127.0.0.1:4173"),
).toBe(true);
});
test("computes an RFC OCRA vector and decodes client data", async ({
page,
}) => {
await page.goto("/deep/nested/auth/");
await page.getByRole("button", { name: "OCRA challenge" }).click();
await page
.getByLabel("Base32 shared secret")
.fill("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
await page.getByRole("button", { name: "Compute OCRA response" }).click();
await expect(page.getByText("237653")).toBeVisible();
await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click();
const client = btoa(
JSON.stringify({
type: "webauthn.get",
challenge: "YQ",
origin: "https://example.test",
}),
)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replace(/=+$/u, "");
await page.getByLabel("Encoded input").fill(client);
await page.getByRole("button", { name: "Decode locally" }).click();
await expect(page.locator(".diagnostic-output")).toContainText(
"webauthn.get",
);
});
test("localhost development enables live credential creation", async ({
page,
}) => {
await page.goto("/deep/nested/auth/");
await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click();
await page.getByRole("button", { name: "Live ceremony" }).click();
await expect(page.getByText("Live enabled")).toBeVisible();
await expect(
page.getByRole("button", { name: "Create test credential" }),
).toBeEnabled();
});
test("exposes the advanced OTP and WebAuthn laboratories", async ({ page }) => {
await page.goto("/deep/nested/auth/");
await page.getByRole("button", { name: "Resync & rotation" }).click();
await expect(
page.getByRole("heading", { name: "HOTP resynchronization" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Collection comparison" }),
).toBeVisible();
await page.getByRole("button", { name: "Import & migration" }).click();
await expect(
page.getByRole("button", { name: "Scan with camera" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Decrypt PSKC" }),
).toBeDisabled();
await page.getByRole("button", { name: /WebAuthn \/ Passkeys/ }).click();
await page.getByRole("button", { name: "Extensions & traces" }).click();
await expect(
page.getByRole("heading", { name: "Extension configuration" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Ceremony traces" }),
).toBeVisible();
await page.getByRole("button", { name: "Attestation verifier" }).click();
await expect(
page.getByRole("heading", { name: "Trust root and historical policy" }),
).toBeVisible();
});