From 3f4afb26a4d0a703296f4e71edea8de44b92ec15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 29 Jul 2026 13:39:55 +0200 Subject: [PATCH] test(01-03): pin passkey enrollment reauthentication UX - require password, passkey, TOTP and WebAuthn proof choices - order fresh-factor completion before challenge creation - keep successful enrollment in one cookie-bound ceremony --- .../passkey-enrollment-reauth.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/components/settings/security-section/__tests__/passkey-enrollment-reauth.test.tsx diff --git a/src/components/settings/security-section/__tests__/passkey-enrollment-reauth.test.tsx b/src/components/settings/security-section/__tests__/passkey-enrollment-reauth.test.tsx new file mode 100644 index 000000000..c60a5c0ba --- /dev/null +++ b/src/components/settings/security-section/__tests__/passkey-enrollment-reauth.test.tsx @@ -0,0 +1,59 @@ +/** + * SEC-05 / SEC-08 — Add-passkey reauthentication UX contract. + * + * This repository intentionally keeps component tests SSR/source-oriented + * rather than installing a browser DOM harness. The ordering assertions below + * are security assertions: the existing-factor step must finish before the + * first registration challenge is requested, so cancellation or failure + * cannot leave an enrollable server challenge behind. + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const source = readFileSync( + resolve(__dirname, "../passkey-list-section.tsx"), + "utf8", +); + +function firstIndex(...needles: RegExp[]): number { + const indexes = needles + .map((needle) => source.search(needle)) + .filter((index) => index >= 0); + return indexes.length === 0 ? -1 : Math.min(...indexes); +} + +describe(" enrollment reauthentication", () => { + it("offers every supported existing factor without a bypass", () => { + expect(source).toMatch(/["']password["']/); + expect(source).toMatch(/["']passkey["']/); + expect(source).toMatch(/["']totp["']/); + expect(source).toMatch(/["']webauthn["']/); + + expect(source).not.toMatch(/skipReauth|bypassReauth|allowBearer/i); + expect(source).not.toContain("x-step-up"); + expect(source).not.toContain("Authorization"); + }); + + it("finishes fresh-factor proof before requesting registration options", () => { + const proof = firstIndex(/reauth/i, /existingFactor/, /freshFactor/); + const options = source.indexOf('"/api/auth/passkey/register-options"'); + + expect(proof).toBeGreaterThanOrEqual(0); + expect(options).toBeGreaterThan(proof); + }); + + it("keeps one cookie-bound ceremony from proof through registration verify", () => { + const options = source.indexOf('"/api/auth/passkey/register-options"'); + const browserRegistration = source.indexOf("startRegistration", options); + const verify = source.indexOf( + '"/api/auth/passkey/register-verify"', + browserRegistration, + ); + + expect(options).toBeGreaterThanOrEqual(0); + expect(browserRegistration).toBeGreaterThan(options); + expect(verify).toBeGreaterThan(browserRegistration); + expect(source).toMatch(/challengeId[\s\S]*credential/); + }); +});