Public: anyone can read this; the crew can change it.
A test pins that a passkey belongs to its account: `findPasskey`, `removePasskey` and `notePasskeyUsed` never cross accounts #6801
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6797-a-test-pins-that-a-passkey-belongs-to-it → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +84
−0
test/passkeys-ownership.test.ts added +84
| @@ -0,0 +1,84 @@ | ||
| 1 | /** | |
| 2 | * A passkey belongs to its account: removal never crosses accounts, lookup | |
| 3 | * names the owner, a use touches only its own credential, and an account | |
| 4 | * holds at most PASSKEY_MAX. | |
| 5 | */ | |
| 6 | import { beforeEach, describe, expect, test } from "bun:test"; | |
| 7 | import type { Database } from "bun:sqlite"; | |
| 8 | import type { Credential } from "../src/auth/webauthn.ts"; | |
| 9 | import { | |
| 10 | addPasskey, findPasskey, listPasskeys, notePasskeyUsed, PASSKEY_MAX, | |
| 11 | passkeyIds, passkeyRoom, removePasskey, | |
| 12 | } from "../src/core/passkeys.ts"; | |
| 13 | import { migrations } from "../src/core/schema.ts"; | |
| 14 | import { openDb } from "../src/kernel/db.ts"; | |
| 15 | ||
| 16 | let db: Database; | |
| 17 | ||
| 18 | const cred = (id: string, signCount = 0): Credential => ({ | |
| 19 | id, | |
| 20 | alg: "ES256", | |
| 21 | key: { kty: "EC", crv: "P-256", x: "x", y: "y" }, | |
| 22 | signCount, | |
| 23 | }); | |
| 24 | ||
| 25 | function user(id: string) { | |
| 26 | db.query("INSERT INTO user (id, username, email, password_hash) VALUES (?1, ?1, ?1 || '@x.test', 'x')").run(id); | |
| 27 | } | |
| 28 | ||
| 29 | /** Two accounts with one passkey each; returns the row ids. */ | |
| 30 | function two() { | |
| 31 | user("a"); | |
| 32 | user("b"); | |
| 33 | const a = addPasskey(db, "a", "A phone", cred("cred-a", 1)); | |
| 34 | const b = addPasskey(db, "b", "B key", cred("cred-b", 2)); | |
| 35 | if (!a.ok || !b.ok) throw new Error("setup"); | |
| 36 | return { a: a.value, b: b.value }; | |
| 37 | } | |
| 38 | ||
| 39 | beforeEach(() => { | |
| 40 | db = openDb(":memory:", migrations); | |
| 41 | }); | |
| 42 | ||
| 43 | describe("passkey ownership", () => { | |
| 44 | test("removePasskey only removes the account's own", () => { | |
| 45 | const k = two(); | |
| 46 | expect(removePasskey(db, "a", k.b)).toBe(false); | |
| 47 | expect(listPasskeys(db, "b").map((p) => p.id)).toEqual([k.b]); | |
| 48 | expect(listPasskeys(db, "a")).toHaveLength(1); | |
| 49 | expect(removePasskey(db, "a", k.a)).toBe(true); | |
| 50 | expect(listPasskeys(db, "a")).toEqual([]); | |
| 51 | expect(listPasskeys(db, "b")).toHaveLength(1); | |
| 52 | }); | |
| 53 | ||
| 54 | test("findPasskey returns the owner; an unknown id is undefined", () => { | |
| 55 | const k = two(); | |
| 56 | const found = findPasskey(db, "cred-b"); | |
| 57 | expect(found?.id).toBe(k.b); | |
| 58 | expect(found?.userId).toBe("b"); | |
| 59 | expect(found?.credential).toEqual(cred("cred-b", 2)); | |
| 60 | expect(findPasskey(db, "cred-a")?.userId).toBe("a"); | |
| 61 | expect(findPasskey(db, "nope")).toBeUndefined(); | |
| 62 | expect(passkeyIds(db, "a")).toEqual(["cred-a"]); | |
| 63 | }); | |
| 64 | ||
| 65 | test("notePasskeyUsed touches only that credential", () => { | |
| 66 | const k = two(); | |
| 67 | notePasskeyUsed(db, k.a, 9); | |
| 68 | expect(findPasskey(db, "cred-a")?.credential.signCount).toBe(9); | |
| 69 | expect(findPasskey(db, "cred-b")?.credential.signCount).toBe(2); | |
| 70 | expect(listPasskeys(db, "a")[0]!.lastUsedAt).not.toBeNull(); | |
| 71 | expect(listPasskeys(db, "b")[0]!.lastUsedAt).toBeNull(); | |
| 72 | }); | |
| 73 | ||
| 74 | test("passkeyRoom says no at the most, and for a taken credential", () => { | |
| 75 | const k = two(); | |
| 76 | expect(passkeyRoom(db, "a", "fresh").ok).toBe(true); | |
| 77 | expect(passkeyRoom(db, "a", "cred-b")).toEqual({ ok: false, error: "duplicate" }); | |
| 78 | for (let i = 1; i < PASSKEY_MAX; i++) addPasskey(db, "a", `n${i}`, cred(`more-${i}`)); | |
| 79 | expect(listPasskeys(db, "a")).toHaveLength(PASSKEY_MAX); | |
| 80 | expect(passkeyRoom(db, "a", "fresh")).toEqual({ ok: false, error: "too_many" }); | |
| 81 | expect(passkeyRoom(db, "b", "fresh").ok).toBe(true); | |
| 82 | expect(k.a).toBeTruthy(); | |
| 83 | }); | |
| 84 | }); |
new above
offline
This assertion exists only to use
k; it checks nothing. Calltwo()without binding the result and delete this line.