Ctrl K
ComunidadGuías…

tests/contract/webauthn.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import assert from "node:assert/strict";
import { newOperatorAccount } from "../../src/core/accounts";
import { createApp } from "../../src/core/app";
import { organizationStatement } from "../../src/core/auth";
import type { Db } from "../../src/core/db";
import { sha256Hex } from "../../src/core/ids";
import { migrate } from "../../src/core/migrations";
import type { WebAuthnCapability } from "../../src/core/webauthn";
import { clientVia } from "./harness";

function fakeWebAuthn(): WebAuthnCapability {
  let sequence = 0;
  return {
    configuration: {
      state: "enabled",
      rpId: "memory.example.com",
      origin: "https://memory.example.com",
      rpName: "Titen",
    },
    async registrationOptions({ username, credentials }) {
      sequence += 1;
      return {
        challenge: `register-${sequence}`,
        rp: { id: "memory.example.com", name: "Titen" },
        user: { id: `user-${sequence}`, name: username, displayName: username },
        excludeCredentials: credentials.map((credential) => ({ id: credential.id })),
      };
    },
    async verifyRegistration({ response, challengeHash }) {
      const input = response as { id?: string; challenge?: string };
      if (!input.id || !input.challenge || await sha256Hex(input.challenge) !== challengeHash)
        return { verified: false };
      return {
        verified: true,
        credential: {
          id: input.id,
          publicKey: new Uint8Array([1, 2, 3, sequence]),
          counter: 0,
          transports: ["internal"],
          deviceType: "multiDevice",
          backedUp: true,
        },
      };
    },
    async authenticationOptions({ credentials }) {
      sequence += 1;
      return {
        challenge: `authenticate-${sequence}`,
        rpId: "memory.example.com",
        allowCredentials: credentials.map((credential) => ({ id: credential.id })),
      };
    },
    async verifyAuthentication({ response, challengeHash, credential }) {
      const input = response as { id?: string; challenge?: string; counter?: number };
      if (
        input.id !== credential.id || !input.challenge
        || await sha256Hex(input.challenge) !== challengeHash
        || (input.counter ?? 0) <= credential.counter
      ) return { verified: false };
      return { verified: true, newCounter: input.counter! };
    },
  };
}

export async function assertWebAuthnContract(db: Db, runtime: string) {
  await migrate(db);
  let now = new Date("2026-08-30T02:00:00.000Z");
  const orgId = `org_webauthn_${runtime}`;
  const account = await newOperatorAccount({
    orgId,
    createdBy: `owner_webauthn_${runtime}`,
    username: `passkey-${runtime}`,
    role: "owner",
    scopes: ["views:compile", "memberships:read"],
    maxTrust: "verified",
    now,
  });
  await db.batch([
    organizationStatement(orgId, `WebAuthn ${runtime}`, now),
    ...account.statements,
  ]);
  const app = createApp({
    db,
    runtime: `webauthn-${runtime}`,
    now: () => now,
    webauthn: fakeWebAuthn(),
  });
  const client = clientVia(app, "https://memory.example.com");
  const call = client.call;

  const temporary = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password: account.temporaryPassword },
  });
  assert.equal(temporary.status, 201);
  assert.equal(temporary.body.data.auth_stage, "password_change");
  const password = `correct horse battery ${runtime}`;
  const changed = await call("PATCH", "/v1/operator-accounts/current/password", {
    key: temporary.body.data.api_key,
    body: { password },
  });
  assert.equal(changed.status, 200);

  const established = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  assert.equal(established.status, 201);
  assert.equal(established.body.data.auth_stage, "full");
  const supersededSession = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  assert.equal(supersededSession.status, 201);

  const options = await call("POST", "/v1/operator-accounts/current/passkeys/options", {
    key: established.body.data.api_key,
    body: {},
  });
  assert.equal(options.status, 201);
  assert.match(options.body.data.challenge_id, /^wch_/);
  assert.equal(options.body.data.options.challenge, "register-1");
  const extraField = await call("POST", "/v1/operator-accounts/current/passkeys", {
    key: established.body.data.api_key,
    body: {
      challenge_id: options.body.data.challenge_id,
      label: "Primary device",
      response: { id: "credential-primary", challenge: "register-1" },
      ignored: true,
    },
  });
  assert.equal(extraField.status, 400);
  assert.equal(extraField.body.error.code, "VALIDATION_ERROR");
  const registration = await call("POST", "/v1/operator-accounts/current/passkeys", {
    key: established.body.data.api_key,
    body: {
      challenge_id: options.body.data.challenge_id,
      label: "Primary device",
      response: { id: "credential-primary", challenge: "register-1" },
    },
  });
  assert.equal(registration.status, 201);
  assert.match(registration.body.data.credential.id, /^wcr_/);
  assert.equal(registration.body.data.credential.label, "Primary device");
  assert.equal(registration.body.data.recovery_codes.length, 8);
  const recoveryCode = registration.body.data.recovery_codes[0] as string;
  assert.match(recoveryCode, /^[A-Za-z0-9_-]{22}$/);
  assert.equal((await call("GET", "/v1/principal", {
    key: supersededSession.body.data.api_key,
  })).status, 401, "first passkey enrollment must revoke older dashboard sessions");
  assert.equal((await call("GET", "/v1/principal", {
    key: established.body.data.api_key,
  })).status, 200, "first passkey enrollment must preserve its current dashboard session");

  const stored = await db.all<{
    credentials: number;
    recovery_codes: number;
    raw_recovery_matches: number;
    raw_challenge_matches: number;
  }>(
    `SELECT
       (SELECT COUNT(*) FROM webauthn_credentials WHERE account_id = ?) AS credentials,
       (SELECT COUNT(*) FROM operator_recovery_codes WHERE account_id = ?) AS recovery_codes,
       (SELECT COUNT(*) FROM operator_recovery_codes WHERE code_hash = ?) AS raw_recovery_matches,
       (SELECT COUNT(*) FROM webauthn_challenges WHERE challenge_hash = ?) AS raw_challenge_matches`,
    [account.accountId, account.accountId, recoveryCode, "register-1"],
  );
  assert.deepEqual(stored[0], {
    credentials: 1,
    recovery_codes: 8,
    raw_recovery_matches: 0,
    raw_challenge_matches: 0,
  });

  const staged = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  assert.equal(staged.status, 201);
  assert.equal(staged.body.data.auth_stage, "second_factor");
  assert.equal(staged.body.data.second_factor_required, true);
  assert.deepEqual(staged.body.data.scopes, []);
  const forbidden = await call("GET", "/v1/memories", { key: staged.body.data.api_key });
  assert.equal(forbidden.status, 403);
  assert.equal(forbidden.body.error.code, "STAGED_SESSION");

  const authenticationOptions = await call(
    "POST",
    "/v1/dashboard-sessions/current/passkey-options",
    { key: staged.body.data.api_key, body: {} },
  );
  assert.equal(authenticationOptions.status, 201);
  assert.equal(authenticationOptions.body.data.options.challenge, "authenticate-2");
  const completed = await call("POST", "/v1/dashboard-sessions/current/passkey", {
    key: staged.body.data.api_key,
    body: {
      challenge_id: authenticationOptions.body.data.challenge_id,
      response: { id: "credential-primary", challenge: "authenticate-2", counter: 1 },
    },
  });
  assert.equal(completed.status, 201);
  assert.equal(completed.body.data.auth_stage, "full");
  assert.match(completed.body.data.api_key, /^titen_sk_/);
  assert.equal((await call("GET", "/v1/memories", { key: completed.body.data.api_key })).status, 200);
  assert.equal((await call("GET", "/v1/principal", { key: staged.body.data.api_key })).status, 401);

  const [credential] = await db.all<{ counter: number }>(
    "SELECT counter FROM webauthn_credentials WHERE credential_id = ?",
    ["credential-primary"],
  );
  assert.equal(credential!.counter, 1);
  const [usedChallenge] = await db.all<{ used_at_ms: number | null }>(
    "SELECT used_at_ms FROM webauthn_challenges WHERE id = ?",
    [authenticationOptions.body.data.challenge_id],
  );
  assert.equal(usedChallenge!.used_at_ms, now.getTime());

  const recoveryStage = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  const recovered = await call("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: recoveryStage.body.data.api_key,
    body: { recovery_code: recoveryCode },
  });
  assert.equal(recovered.status, 201);
  assert.equal(recovered.body.data.auth_stage, "full");
  const [consumed] = await db.all<{ count: number }>(
    "SELECT COUNT(*) AS count FROM operator_recovery_codes WHERE account_id = ? AND used_at_ms IS NOT NULL",
    [account.accountId],
  );
  assert.equal(Number(consumed!.count), 1);

  const secondRecoveryStage = await call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  const replay = await call("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: secondRecoveryStage.body.data.api_key,
    body: { recovery_code: recoveryCode },
  });
  assert.equal(replay.status, 401);
  assert.equal(replay.body.error.code, "SECOND_FACTOR_INVALID");

  const concurrentCode = registration.body.data.recovery_codes[1] as string;
  const concurrentStages = await Promise.all([0, 1].map(() => call("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  })));
  const concurrent = await Promise.all(concurrentStages.map((session) => call(
    "POST",
    "/v1/dashboard-sessions/current/recovery-code",
    { key: session.body.data.api_key, body: { recovery_code: concurrentCode } },
  )));
  assert.deepEqual(concurrent.map((response) => response.status).sort(), [201, 401]);

  const accountKey = await sha256Hex(`dashboard-login:${account.username}`);
  await db.batch([{ sql: "INSERT OR REPLACE INTO login_throttles VALUES (?, 50, ?, ?)", params: [accountKey, now.getTime() + 60_000, now.getTime()] }]);
  const blockedPassword = await call("POST", "/v1/dashboard-sessions", { body: { username: account.username, password } });
  assert.equal(blockedPassword.status, 401);
  const startRecovery = async (username = account.username) => call("POST", "/v1/dashboard-sessions/recovery", { body: { username } });
  const rescue = await startRecovery();
  assert.equal(rescue.status, 201);
  assert.deepEqual(Object.keys(rescue.body.data).sort(), ["api_key", "auth_stage", "expires_at"]);
  const unknownRescue = await startRecovery("unknown-rescue-user");
  assert.equal(unknownRescue.status, 201);
  assert.deepEqual(Object.keys(unknownRescue.body.data).sort(), Object.keys(rescue.body.data).sort());
  assert.equal((await call("GET", "/v1/memories", { key: rescue.body.data.api_key })).status, 403);
  const rescueOptions = await call("POST", "/v1/dashboard-sessions/current/passkey-options", { key: rescue.body.data.api_key, body: {} });
  const rescued = await call("POST", "/v1/dashboard-sessions/current/passkey", {
    key: rescue.body.data.api_key,
    body: { challenge_id: rescueOptions.body.data.challenge_id, response: { id: "credential-primary", challenge: rescueOptions.body.data.options.challenge, counter: 2 } },
  });
  assert.equal(rescued.status, 201);
  assert.equal(rescued.body.data.failed_attempts, 50);
  await db.batch([{ sql: "INSERT OR REPLACE INTO login_throttles VALUES (?, 50, ?, ?)", params: [accountKey, now.getTime() + 60_000, now.getTime()] }]);
  const rescueCodeSession = await startRecovery();
  assert.equal((await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: rescueCodeSession.body.data.api_key, body: { recovery_code: "invalid-recovery-code" } })).status, 401);
  const rescueCode = registration.body.data.recovery_codes[3] as string;
  const codeRescued = await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: rescueCodeSession.body.data.api_key, body: { recovery_code: rescueCode } });
  assert.equal(codeRescued.status, 201);
  assert.equal(codeRescued.body.data.auth_stage, "full");
  const replayRescue = await startRecovery();
  assert.equal((await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: replayRescue.body.data.api_key, body: { recovery_code: rescueCode } })).status, 401);
  const expiredRescue = await startRecovery();
  now = new Date(now.getTime() + 16 * 60_000);
  assert.equal((await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: expiredRescue.body.data.api_key, body: { recovery_code: registration.body.data.recovery_codes[4] } })).status, 401);
  const disabledRescue = await startRecovery();
  await db.batch([{ sql: "UPDATE operator_accounts SET disabled_at = ? WHERE id = ?", params: [now.toISOString(), account.accountId] }]);
  assert.equal((await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: disabledRescue.body.data.api_key, body: { recovery_code: registration.body.data.recovery_codes[4] } })).status, 401);
  await db.batch([{ sql: "UPDATE operator_accounts SET disabled_at = NULL, must_change_password = 1 WHERE id = ?", params: [account.accountId] }]);
  const rotateRescue = await startRecovery();
  const rotateSession = await call("POST", "/v1/dashboard-sessions/current/recovery-code", { key: rotateRescue.body.data.api_key, body: { recovery_code: registration.body.data.recovery_codes[4] } });
  assert.equal(rotateSession.status, 201);
  assert.equal(rotateSession.body.data.auth_stage, "password_change");
  assert.deepEqual(rotateSession.body.data.scopes, []);
  await db.batch([{ sql: "UPDATE operator_accounts SET must_change_password = 0 WHERE id = ?", params: [account.accountId] }]);

  const racedRecoveryStage = await startRecovery();
  let resetDuringRecovery = false;
  const racedRecoveryDb: Db = {
    ...db,
    async all<Row>(sql: string, params?: import("../../src/core/db").Param[]) {
      const rows = await db.all<Row>(sql, params);
      if (!resetDuringRecovery && sql.includes("a.must_change_password, m.role")) {
        resetDuringRecovery = true;
        await db.batch([
          { sql: "UPDATE operator_accounts SET must_change_password = 1 WHERE id = ?", params: [account.accountId] },
          { sql: "UPDATE api_keys SET revoked_at = ? WHERE principal_id = ? AND label = 'Dashboard session'", params: [now.toISOString(), account.principalId] },
        ]);
      }
      return rows;
    },
  };
  const raceCall = clientVia(createApp({ db: racedRecoveryDb, runtime, now: () => now }), "https://memory.example.com").call;
  assert.equal((await raceCall("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: racedRecoveryStage.body.data.api_key, body: { recovery_code: registration.body.data.recovery_codes[5] },
  })).status, 401, "reset must prevent stale recovery proof from issuing a session");
  assert.equal(resetDuringRecovery, true);
  await db.batch([{ sql: "UPDATE operator_accounts SET must_change_password = 0 WHERE id = ?", params: [account.accountId] }]);

  const disabledCall = clientVia(createApp({
    db,
    runtime: `webauthn-disabled-${runtime}`,
    now: () => now,
  }), "https://memory.example.com").call;
  const disabledStage = await disabledCall("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  assert.equal(disabledStage.body.data.auth_stage, "second_factor");
  const disabledRecovery = await disabledCall("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: disabledStage.body.data.api_key,
    body: { recovery_code: registration.body.data.recovery_codes[2] },
  });
  assert.equal(disabledRecovery.status, 201, "recovery must survive disabled WebAuthn configuration");

  const fullKey = disabledRecovery.body.data.api_key as string;
  const passkeys = await call("GET", "/v1/operator-accounts/current/passkeys", { key: fullKey });
  assert.equal(passkeys.status, 200);
  assert.equal(passkeys.body.data.credentials.length, 1);
  const [listedPasskey] = passkeys.body.data.credentials;
  assert.deepEqual(Object.keys(listedPasskey).sort(), ["backed_up", "created_at", "device_type", "id", "label", "last_used_at"]);
  assert.equal(listedPasskey.id, registration.body.data.credential.id);
  assert.equal(listedPasskey.label, "Primary device");
  assert.equal(typeof listedPasskey.backed_up, "boolean");
  assert.notEqual(listedPasskey.last_used_at, null, "a used passkey reports when it was last used");
  assert.equal(JSON.stringify(passkeys.body).includes("credential-primary"), false, "the raw credential id stays server-side");

  // Regeneration must invalidate every code of the previous generation.
  const previousCodes = registration.body.data.recovery_codes as string[];
  const regenerated = await call("POST", "/v1/operator-accounts/current/recovery-codes", { key: fullKey, body: {} });
  assert.equal(regenerated.status, 201);
  const freshCodes = regenerated.body.data.recovery_codes as string[];
  assert.equal(freshCodes.length, 8);
  assert.equal(new Set(freshCodes).size, 8);
  assert.equal(freshCodes.some((code) => previousCodes.includes(code)), false);
  const [regeneratedState] = await db.all<{ unused: number; generations: number; outside_current: number }>(
    `SELECT
       (SELECT COUNT(*) FROM operator_recovery_codes WHERE account_id = ? AND used_at_ms IS NULL) AS unused,
       (SELECT COUNT(*) FROM operator_recovery_generations WHERE account_id = ?) AS generations,
       (SELECT COUNT(*) FROM operator_recovery_codes c WHERE c.account_id = ? AND c.generation_id NOT IN (
          SELECT generation_id FROM operator_recovery_generations WHERE account_id = ?)) AS outside_current`,
    [account.accountId, account.accountId, account.accountId, account.accountId],
  );
  assert.deepEqual(
    { unused: Number(regeneratedState!.unused), generations: Number(regeneratedState!.generations), outside_current: Number(regeneratedState!.outside_current) },
    { unused: 8, generations: 1, outside_current: 0 },
  );
  const [regenerateAudit] = await db.all<{ count: number }>(
    "SELECT COUNT(*) AS count FROM audit_log WHERE action = 'operator_account.recovery_regenerate' AND resource_id = ?",
    [account.accountId],
  );
  assert.equal(Number(regenerateAudit!.count), 1);
  const oldCodeStage = await call("POST", "/v1/dashboard-sessions", { body: { username: account.username, password } });
  const oldCode = await call("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: oldCodeStage.body.data.api_key,
    body: { recovery_code: previousCodes[7] },
  });
  assert.equal(oldCode.status, 401, "an unused code from the previous generation is rejected");
  assert.equal(oldCode.body.error.code, "SECOND_FACTOR_INVALID");
  const newCodeStage = await call("POST", "/v1/dashboard-sessions", { body: { username: account.username, password } });
  const newCode = await call("POST", "/v1/dashboard-sessions/current/recovery-code", {
    key: newCodeStage.body.data.api_key,
    body: { recovery_code: freshCodes[0] },
  });
  assert.equal(newCode.status, 201);
  assert.equal(newCode.body.data.auth_stage, "full");

  const lastCredentialId = registration.body.data.credential.id as string;
  const noConfirmation = await call("DELETE", `/v1/operator-accounts/current/passkeys/${lastCredentialId}`, {
    key: disabledRecovery.body.data.api_key,
    body: {},
  });
  assert.equal(noConfirmation.status, 401);
  const removed = await disabledCall("DELETE", `/v1/operator-accounts/current/passkeys/${lastCredentialId}`, {
    key: disabledRecovery.body.data.api_key,
    body: { password },
  });
  assert.equal(removed.status, 200);
  const noSecondFactor = await disabledCall("POST", "/v1/dashboard-sessions", {
    body: { username: account.username, password },
  });
  assert.equal(noSecondFactor.status, 201);
  assert.equal(noSecondFactor.body.data.auth_stage, "full");
  const emptyPasskeys = await call("GET", "/v1/operator-accounts/current/passkeys", { key: noSecondFactor.body.data.api_key });
  assert.equal(emptyPasskeys.status, 200);
  assert.deepEqual(emptyPasskeys.body.data.credentials, []);
  const withoutPasskey = await call("POST", "/v1/operator-accounts/current/recovery-codes", {
    key: noSecondFactor.body.data.api_key,
    body: {},
  });
  assert.equal(withoutPasskey.status, 409, "recovery codes require a registered passkey");
}