fix(auth): confirm passkey operations and preserve safe legacy migration

Fence displayed identity and native prompt generations, classify secret-free outcomes, recover atomic addition receipts without restoring old credentials, and expose truthful own-key verification metadata. Preserve required verifier and enrollment authority boundaries. Include real-store, production browser, native CTAP2 and visible responsive regressions.

Validation: root check/fmt; API 115; passkey security 25; Web unit 712/component 605; auth/entry/activation/hosting browsers; product auth E2E 8 and two explicit browser suites. Server suite: 1262 lib+12 CLI, 3 ignored and 2 environment-specific exclusions. State-name checker retains 15 byte-identical baseline findings; no new exceptions. Physical keys/mobile and hosted CI remain unverified.
This commit is contained in:
2026-10-26 07:18:23 +09:00
parent b284934e34
commit f17b5212c7
66 changed files with 4716 additions and 532 deletions
@@ -34,6 +34,11 @@ const register = async (page: Page) => {
await page.getByRole("button", { name: "Register new account", exact: true })
.click();
};
const additionConfirmed = (page: Page) =>
page.getByRole("status").filter({
hasText:
/^Passkey added to your account; the expected session was confirmed\.$/,
});
const count = (mutations: Mutation[], path: string) =>
mutations.filter((entry) => entry.path === path).length;
const at = async (page: Page, path: string) => {
@@ -151,7 +156,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
readyUrl: `${origin}/health`,
}],
`${root}/tools/web-ux/scenarios/anonymous-entry.json`,
`${root}/target/web-ux/t767-validation/auth-interactions/processes`,
`${root}/target/t783/web-shell-final/processes`,
[],
);
let browser: Browser | undefined;
@@ -229,9 +234,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
name: "Add passkey to alice",
exact: true,
}).click();
await page.getByRole("status").filter({
hasText: /added|registered/i,
}).waitFor();
await additionConfirmed(page).waitFor();
const additions = mutations.filter((entry) =>
entry.path.includes("/addition/")
);
@@ -239,7 +242,12 @@ Deno.test("standalone auth separates login, registration, own account and explic
"/api/auth/passkeys/addition/options",
"/api/auth/passkeys/addition/complete",
]);
assertEquals(additions[0].keys, ["browser_origin"]);
assertEquals(additions[0].keys, [
"browser_origin",
"expected_account_id",
"expected_authentication_reference",
"expected_user_id",
]);
assertEquals(additions[1].keys, ["challenge_id", "credential"]);
assertEquals(
mutations.some((entry) => entry.path.includes("/registration/")),
@@ -357,8 +365,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
name: "Add passkey to alice",
exact: true,
}).click();
await page.getByRole("status").filter({ hasText: /added/i })
.waitFor();
await additionConfirmed(page).waitFor();
assertEquals(
count(mutations, "/api/auth/passkeys/addition/complete"),
1,
@@ -784,9 +791,7 @@ Deno.test("standalone auth separates login, registration, own account and explic
.__authFixtureRelease()
);
if (purpose === "addition") {
await page.getByRole("status").filter({
hasText: /added|registered/i,
}).waitFor();
await additionConfirmed(page).waitFor();
} else await at(page, "/dashboard");
assertEquals(
count(mutations, `/api/auth/passkeys/${purpose}/options`),
@@ -813,8 +818,18 @@ Deno.test("standalone auth separates login, registration, own account and explic
name: "Approve device login",
exact: true,
}).click();
await page.getByRole("alert").filter({ hasText: /invalid|expired/i })
.waitFor();
await page.getByRole("alert").filter({
hasText:
/^Authentication request was rejected \(400\)\. Check your session or restart the operation\.$/,
}).waitFor();
assertEquals(new URL(page.url()).pathname, "/login/device");
assertEquals(
await page.getByText(
"Device login approved. You can return to the CLI.",
{ exact: true },
).count(),
0,
);
assertEquals(count(mutations, "/api/auth/device-login/approve"), 1);
}),
);