import { describe, expect, test } from "bun:test"; import { createAuthEngine } from "../src/engine.ts"; import { MemoryAuthStore } from "../src/stores/memory.ts"; import { generateTotp } from "../src/totp/index.ts"; import type { AuthDeliveryMessage, PasskeyProvider } from "../src/types.ts"; function fixture() { let time = 1_720_000_000_000; let seed = 11; const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "test-auth-secret-that-is-longer-than-thirty-two-characters", issuer: "WRNexus Test", clock: { now: () => time }, random: { bytes(length) { const bytes = new Uint8Array(length); for (let index = 0; index < length; index += 1) { seed = (seed * 1664525 + 1013904223) >>> 0; bytes[index] = seed & 255; } return bytes; }, }, delivery: { async send(message) { messages.push(message); }, }, }); return { engine, messages, advance(ms: number) { time += ms; }, now: () => time, }; } describe("authentication engine", () => { test("rejects unsafe numeric authentication configuration", () => { expect(() => createAuthEngine({ store: new MemoryAuthStore(), secret: "invalid-config-secret-that-is-longer-than-thirty-two-characters", sessionTtlMs: Number.NaN, }), ).toThrow("sessionTtlMs"); expect(() => createAuthEngine({ store: new MemoryAuthStore(), secret: "invalid-config-secret-that-is-longer-than-thirty-two-characters", passwordMinLength: 4, }), ).toThrow("passwordMinLength"); }); test("registers, verifies email, signs in, and validates the session", async () => { const { engine, messages } = fixture(); const registered = await engine.register({ email: "Owner@Example.com", username: "owner", displayName: "Owner", password: "StrongPassword123", }); expect(registered.ok).toBe(true); expect(messages[0]?.template).toBe("verify-email"); const verified = await engine.verifyEmail(messages[0]!.token!); expect(verified).toMatchObject({ ok: true, user: { emailVerified: true } }); const login = await engine.login({ identifier: "owner@example.com", password: "StrongPassword123", }); expect(login.ok).toBe(true); expect(login.session).toBeDefined(); expect(await engine.validateSession(login.session!.id)).toMatchObject({ userId: registered.user!.id, }); }); test("delivery provider failures do not corrupt completed authentication state", async () => { const store = new MemoryAuthStore(); const engine = createAuthEngine({ store, secret: "delivery-failure-secret-that-is-longer-than-thirty-two-characters", sendLoginAlerts: true, delivery: { async send() { throw new Error("provider unavailable"); }, }, }); const registered = await engine.register({ email: "delivery-failure@example.com", password: "StrongPassword123", }); expect(registered.ok).toBe(true); const user = await store.findUserById(registered.user!.id); user!.status = "active"; user!.emailVerified = true; await store.updateUser(user!); const login = await engine.login({ identifier: "delivery-failure@example.com", password: "StrongPassword123", }); expect(login.ok).toBe(true); expect( (await store.listSecurityEvents(registered.user!.id)).some( (event) => event.type === "delivery.failed", ), ).toBe(true); }); test("engine-level registration rejects malformed identities before persistence", async () => { const { engine } = fixture(); expect( await engine.register({ email: "not-an-email", password: "StrongPassword123" }), ).toMatchObject({ ok: false, code: "registration-failed" }); expect(await engine.register({ phone: "+12", password: "StrongPassword123" })).toMatchObject({ ok: false, code: "registration-failed", }); expect(await engine.store.listUsers()).toHaveLength(0); }); test("normalizes identities and rejects duplicates", async () => { const { engine } = fixture(); expect( (await engine.register({ email: "One@Example.com", password: "StrongPassword123" })).ok, ).toBe(true); const duplicate = await engine.register({ email: "one@example.com", password: "AnotherStrong123", }); expect(duplicate).toMatchObject({ ok: false, code: "registration-failed" }); }); test("issues and consumes password reset links once", async () => { const { engine, messages } = fixture(); await engine.register({ email: "reset@example.com", password: "StrongPassword123" }); await engine.requestPasswordReset("reset@example.com", "https://example.test"); const token = messages.find((message) => message.template === "password-reset")!.token!; expect((await engine.resetPassword(token, "NewStrongPassword456")).ok).toBe(true); expect((await engine.resetPassword(token, "OtherStrongPassword789")).ok).toBe(false); expect( (await engine.login({ identifier: "reset@example.com", password: "NewStrongPassword456" })) .ok, ).toBe(true); }); test("supports email OTP verification with attempt and expiry protection", async () => { const { engine, messages, advance } = fixture(); const registered = await engine.register({ email: "otp@example.com", password: "StrongPassword123", }); const challenge = await engine.issueOtp(registered.user!.id, "email-otp"); const code = messages.find((message) => message.template === "email-otp")!.code!; expect((await engine.verifyOtp(challenge.id, "000000")).ok).toBe(false); expect((await engine.verifyOtp(challenge.id, `${code.slice(0, 3)}-${code.slice(3)}`)).ok).toBe( false, ); expect((await engine.verifyOtp(challenge.id, code)).ok).toBe(true); expect((await engine.verifyOtp(challenge.id, code)).ok).toBe(false); const expired = await engine.issueOtp(registered.user!.id, "email-otp"); advance(11 * 60_000); expect((await engine.verifyOtp(expired.id, messages.at(-1)!.code!)).ok).toBe(false); }); test("binds OTPs to linked destinations and to their intended flow", async () => { const { engine, messages } = fixture(); const registered = await engine.register({ email: "bound@example.com", password: "StrongPassword123", }); let rejected = false; try { await engine.issueOtp(registered.user!.id, "email-otp", "attacker@example.com"); } catch { rejected = true; } expect(rejected).toBe(true); const loginChallenge = await engine.requestOtpLogin("bound@example.com", "email-otp"); const code = messages.find((message) => message.template === "email-otp")!.code!; expect((await engine.verifyOtp(loginChallenge!.id, code)).ok).toBe(false); expect((await engine.completeOtpLogin(loginChallenge!.id, code)).ok).toBe(true); }); test("returns an opaque OTP challenge for an unknown account", async () => { const { engine, messages } = fixture(); const challenge = await engine.requestOtpLogin("missing@example.com", "email-otp"); expect(challenge?.id.startsWith("otp_")).toBe(true); expect(messages).toHaveLength(0); expect((await engine.completeOtpLogin(challenge!.id, "123456")).ok).toBe(false); }); test("enables replay-safe TOTP and one-use recovery codes", async () => { const { engine, now } = fixture(); const registered = await engine.register({ email: "mfa@example.com", password: "StrongPassword123", }); const setup = await engine.beginTotp(registered.user!.id, "Primary authenticator"); const token = await generateTotp(setup.secret, { timestamp: now() }); expect(await engine.confirmTotp(registered.user!.id, setup.credentialId, token)).toBe(true); const next = await generateTotp(setup.secret, { timestamp: now() + 30_000 }); expect(await engine.verifyTotp(registered.user!.id, next)).toBe(true); expect(await engine.verifyTotp(registered.user!.id, next)).toBe(false); const codes = await engine.generateRecoveryCodes(registered.user!.id, 3); expect(codes).toHaveLength(3); expect(await engine.consumeRecoveryCode(registered.user!.id, codes[0]!)).toBe(true); expect(await engine.consumeRecoveryCode(registered.user!.id, codes[0]!)).toBe(false); expect(await engine.listRecoveryCodeStatus(registered.user!.id)).toEqual({ total: 3, remaining: 2, }); const replacement = await engine.generateRecoveryCodes(registered.user!.id, 2); expect(await engine.consumeRecoveryCode(registered.user!.id, codes[1]!)).toBe(false); expect(await engine.consumeRecoveryCode(registered.user!.id, replacement[0]!)).toBe(true); }); test("creates a trusted session when remember-device is requested", async () => { const { engine } = fixture(); await engine.register({ email: "remember@example.com", password: "StrongPassword123" }); const result = await engine.login({ identifier: "remember@example.com", password: "StrongPassword123", fingerprint: "browser-fingerprint", deviceName: "Work browser", rememberDevice: true, }); expect(result).toMatchObject({ ok: true, session: { trusted: true } }); }); test("tracks and revokes sessions and trusted devices", async () => { const { engine } = fixture(); const registered = await engine.register({ email: "devices@example.com", password: "StrongPassword123", }); const device = await engine.trustDevice(registered.user!.id, { fingerprint: "browser-device", name: "Work laptop", }); expect(device.name).toBe("Work laptop"); const session = await engine.createSession(registered.user!.id, { fingerprint: "browser-device", }); expect(session.trusted).toBe(true); expect(await engine.revokeSession(registered.user!.id, session.id)).toBe(true); expect(await engine.validateSession(session.id)).toBeUndefined(); }); test("lists only active sessions and removes expired records", async () => { const { engine, advance } = fixture(); const registered = await engine.register({ email: "active-sessions@example.com", password: "StrongPassword123", }); const expired = await engine.createSession(registered.user!.id); advance(25 * 60 * 60_000); const active = await engine.createSession(registered.user!.id); expect(await engine.listSessions(registered.user!.id)).toEqual([active]); expect(await engine.store.findSession(expired.id)).toBeUndefined(); }); test("does not reveal account status before password verification", async () => { const { engine } = fixture(); const registered = await engine.register({ email: "disabled-login@example.com", password: "StrongPassword123", }); await engine.setAccountStatus(registered.user!.id, "disabled"); const wrongPassword = await engine.login({ identifier: "disabled-login@example.com", password: "WrongPassword999", }); const unknownAccount = await engine.login({ identifier: "missing-login@example.com", password: "WrongPassword999", }); expect(wrongPassword).toMatchObject({ ok: false, code: "invalid-credentials", message: "The email, phone, username, or password you entered is incorrect.", }); expect(unknownAccount).toMatchObject({ ok: false, code: "invalid-credentials", message: wrongPassword.message, }); expect( await engine.login({ identifier: "disabled-login@example.com", password: "StrongPassword123", }), ).toMatchObject({ ok: false, code: "account-disabled", message: "This account is disabled. Contact support for help.", }); }); test("raises CAPTCHA and blocks repeated suspicious sign-in attempts", async () => { const { engine } = fixture(); await engine.register({ email: "risk@example.com", password: "StrongPassword123" }); for (let index = 0; index < 4; index += 1) { await engine.login({ identifier: "risk@example.com", password: "WrongPassword999", captchaVerified: true, }); } const captcha = await engine.login({ identifier: "risk@example.com", password: "WrongPassword999", }); expect(captcha).toMatchObject({ ok: false, code: "captcha-required", requires: { captcha: true }, }); await engine.login({ identifier: "risk@example.com", password: "WrongPassword999", captchaVerified: true, }); const blocked = await engine.login({ identifier: "risk@example.com", password: "WrongPassword999", captchaVerified: true, }); expect(blocked.ok).toBe(false); expect(blocked.risk?.block).toBe(true); }); test("links OAuth accounts and creates users for new provider identities", async () => { const { engine } = fixture(); const result = await engine.loginWithOAuth(" GitHub ", { id: "github-123", email: "oauth@example.com", name: "OAuth User", raw: { email_verified: true }, }); expect(result.ok).toBe(true); expect(result.user?.emailVerified).toBe(true); const accounts = await engine.store.listOAuthAccounts(result.user!.id); expect(accounts).toHaveLength(1); expect(accounts[0]?.provider).toBe("github"); }); test("creates and accepts a single-use invitation", async () => { const { engine, messages } = fixture(); const invitation = await engine.createInvitation({ email: "invited@example.com", displayName: "Invited User", roles: ["member"], invitedBy: "admin-user", baseUrl: "https://example.test", }); expect(messages.at(-1)?.template).toBe("invitation"); const accepted = await engine.acceptInvitation(invitation.token, { password: "StrongPassword123", }); expect(accepted).toMatchObject({ ok: true, user: { emailVerified: true, status: "active" } }); expect(accepted.user?.roles).toContain("member"); expect( (await engine.acceptInvitation(invitation.token, { password: "StrongPassword123" })).ok, ).toBe(false); }); test("customizes delivered token links without application-owned auth APIs", async () => { const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "token-url-test-secret-that-is-longer-than-thirty-two-characters", delivery: { async send(message) { messages.push(message); }, }, tokenUrl({ purpose, token, baseUrl }) { if (!baseUrl) return undefined; const path = purpose === "password-reset" ? `/recover/reset?token=${encodeURIComponent(token)}` : purpose === "invite" ? `/invitation?token=${encodeURIComponent(token)}` : `/${purpose}?token=${encodeURIComponent(token)}`; return new URL(path, baseUrl).toString(); }, }); await engine.register({ email: "links@example.com", password: "StrongPassword123", }); messages.length = 0; await engine.requestPasswordReset("links@example.com", "https://example.test"); expect(messages.at(-1)?.url?.startsWith("https://example.test/recover/reset?token=")).toBe( true, ); await engine.createInvitation({ email: "invited-links@example.com", baseUrl: "https://example.test", }); expect(messages.at(-1)?.url?.startsWith("https://example.test/invitation?token=")).toBe(true); }); test("supports passwordless OTP login and sends a login alert", async () => { const { engine, messages } = fixture(); await engine.register({ email: "passwordless@example.com", password: "StrongPassword123" }); const challenge = await engine.requestOtpLogin("passwordless@example.com", "email-otp"); const code = messages.find((message) => message.template === "email-otp")!.code!; const result = await engine.completeOtpLogin(challenge!.id, code); expect(result).toMatchObject({ ok: true, user: { emailVerified: true } }); expect(result.session).toBeDefined(); }); test("protects TOTP and OAuth secrets before persistence", async () => { const store = new MemoryAuthStore(); const protectedValues: string[] = []; const engine = createAuthEngine({ store, secret: "protector-test-secret-that-is-longer-than-thirty-two-characters", secretProtector: { async protect(value, purpose) { const output = `sealed:${purpose}:${value}`; protectedValues.push(output); return output; }, async reveal(value) { return value.split(":").slice(2).join(":"); }, }, }); const registered = await engine.register({ email: "protected@example.com", password: "StrongPassword123", }); const setup = await engine.beginTotp(registered.user!.id); expect((await store.listTotp(registered.user!.id))[0]?.secret.startsWith("sealed:totp:")).toBe( true, ); await engine.linkOAuth( registered.user!.id, "example", { id: "provider-id", raw: {} }, { access_token: "access", refresh_token: "refresh", token_type: "Bearer", }, ); expect(protectedValues.some((value) => value.startsWith("sealed:oauth-access:"))).toBe(true); expect(setup.secret.startsWith("sealed:")).toBe(false); }); test("requires an explicit policy and audits impersonation sessions", async () => { const store = new MemoryAuthStore(); const engine = createAuthEngine({ store, secret: "impersonation-secret-that-is-longer-than-thirty-two-characters", authorizeImpersonation: ({ actor }) => actor.roles.includes("admin"), }); const actorResult = await engine.register({ email: "admin@example.com", password: "StrongPassword123", }); const targetResult = await engine.register({ email: "target@example.com", password: "StrongPassword123", }); const actor = await store.findUserById(actorResult.user!.id); const target = await store.findUserById(targetResult.user!.id); actor!.roles = ["admin"]; actor!.status = "active"; target!.status = "active"; await store.updateUser(actor!); await store.updateUser(target!); const actorSession = await engine.createSession(actor!.id); const started = await engine.startImpersonation(actor!.id, target!.id, { sessionId: actorSession.id, reason: "Support case", }); expect(started).toMatchObject({ ok: true, user: { id: target!.id } }); expect(started.session?.metadata).toMatchObject({ impersonated: true, actorUserId: actor!.id }); const stopped = await engine.stopImpersonation(started.session!.id); expect(stopped).toMatchObject({ ok: true, user: { id: actor!.id }, session: { id: actorSession.id }, }); }); test("does not auto-link an unverified OAuth email to an existing account", async () => { const { engine } = fixture(); await engine.register({ email: "existing@example.com", password: "StrongPassword123" }); const result = await engine.loginWithOAuth("unknown", { id: "provider-unverified", email: "existing@example.com", raw: { email_verified: false }, }); expect(result).toMatchObject({ ok: false, code: "oauth-link-required" }); }); test("uses a configured passkey provider for registration and authentication", async () => { let expectedOrigin = ""; let expectedRpId = ""; const provider: PasskeyProvider = { async registrationOptions() { return { challenge: "AQID", rp: { id: "example.test", name: "Test" }, user: { id: "AQID", name: "user", displayName: "User" }, timeout: 1, attestation: "none", }; }, async verifyRegistration() { return { verified: true, credential: { credentialId: "cred-1", publicKey: "public", counter: 0, transports: ["internal"], name: "Device passkey", }, }; }, async authenticationOptions() { return { challenge: "BAUG", rpId: "example.test", timeout: Number.POSITIVE_INFINITY, userVerification: "preferred", }; }, async verifyAuthentication(input) { expectedOrigin = input.expectedOrigin; expectedRpId = input.expectedRpId; return { verified: true, credentialId: "cred-1", newCounter: 1 }; }, }; let time = 1_720_000_000_000; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "passkey-test-secret-that-is-longer-than-thirty-two-characters", passkeys: provider, clock: { now: () => time }, }); const registered = await engine.register({ email: "passkey@example.com", password: "StrongPassword123", }); const start = await engine.beginPasskeyRegistration(registered.user!.id, { rpId: "example.test", rpName: "Test", origin: "https://example.test", }); expect(start.options.timeout).toBe(30_000); expect( await engine.finishPasskeyRegistration(registered.user!.id, { key: start.key, response: {}, rpId: "example.test", origin: "https://example.test", }), ).toBe(true); const auth = await engine.beginPasskeyAuthentication({ identifier: "passkey@example.com", rpId: "example.test", origin: "https://example.test", }); expect(auth.options.timeout).toBe(5 * 60_000); time += 1; expect( ( await engine.finishPasskeyAuthentication({ key: auth.key, response: { id: "cred-1" }, rpId: "example.test", origin: "https://example.test", }) ).ok, ).toBe(true); expect(expectedOrigin).toBe("https://example.test"); expect(expectedRpId).toBe("example.test"); const repeatedCounter = await engine.beginPasskeyAuthentication({ identifier: "passkey@example.com", rpId: "example.test", origin: "https://example.test", }); expect( await engine.finishPasskeyAuthentication({ key: repeatedCounter.key, response: { id: "cred-1" }, }), ).toMatchObject({ ok: false, code: "passkey-counter-regression" }); await engine.setAccountStatus(registered.user!.id, "disabled"); const disabledAuth = await engine.beginPasskeyAuthentication({ identifier: "passkey@example.com", rpId: "example.test", origin: "https://example.test", }); expect( await engine.finishPasskeyAuthentication({ key: disabledAuth.key, response: { id: "cred-1" }, }), ).toMatchObject({ ok: false, code: "account-disabled" }); }); test("rejects a passkey result that switches away from the identified user", async () => { const store = new MemoryAuthStore(); const provider: PasskeyProvider = { async registrationOptions() { throw new Error("not used"); }, async verifyRegistration() { return { verified: false }; }, async authenticationOptions() { return { challenge: "BAUG", rpId: "example.test", timeout: 60_000, userVerification: "required", }; }, async verifyAuthentication() { return { verified: true, userId: "different-user" }; }, }; const engine = createAuthEngine({ store, secret: "passkey-switch-secret-that-is-longer-than-thirty-two-characters", passkeys: provider, }); const registered = await engine.register({ email: "identified@example.com", password: "StrongPassword123", }); const auth = await engine.beginPasskeyAuthentication({ identifier: "identified@example.com", rpId: "example.test", origin: "https://example.test", }); const result = await engine.finishPasskeyAuthentication({ key: auth.key, response: {} }); expect(result).toMatchObject({ ok: false, code: "passkey-user-mismatch" }); expect(registered.user?.id).toBeDefined(); }); test("passwordless flows respect account status and enrolled MFA", async () => { const { engine, messages, now } = fixture(); const registered = await engine.register({ email: "passwordless-mfa@example.com", password: "StrongPassword123", }); const setup = await engine.beginTotp(registered.user!.id); const token = await generateTotp(setup.secret, { timestamp: now() }); expect(await engine.confirmTotp(registered.user!.id, setup.credentialId, token)).toBe(true); await engine.requestMagicLink("passwordless-mfa@example.com", "https://example.test"); const magicToken = messages.find((message) => message.template === "magic-link")!.token!; expect(await engine.consumeMagicLink(magicToken)).toMatchObject({ ok: false, code: "mfa-required", requires: { mfa: expect.any(Array) }, }); const otp = await engine.requestOtpLogin("passwordless-mfa@example.com", "email-otp"); const otpCode = messages.filter((message) => message.template === "email-otp").at(-1)!.code!; expect(await engine.completeOtpLogin(otp.id, otpCode)).toMatchObject({ ok: false, code: "mfa-required", }); await engine.setAccountStatus(registered.user!.id, "disabled"); const second = await engine.requestOtpLogin("passwordless-mfa@example.com", "email-otp"); expect(await engine.completeOtpLogin(second.id, "000000")).toMatchObject({ ok: false, code: "invalid-otp", }); }); test("temporary login locks do not activate a pending account when they expire", async () => { let time = 1_720_000_000_000; const store = new MemoryAuthStore(); const engine = createAuthEngine({ store, secret: "pending-lock-secret-that-is-longer-than-thirty-two-characters", requireVerifiedEmail: true, maxFailedLogins: 1, lockDurationMs: 60_000, clock: { now: () => time }, }); const registered = await engine.register({ email: "pending-lock@example.com", password: "StrongPassword123", }); await engine.login({ identifier: "PENDING-LOCK@example.com", password: "WrongPassword999", captchaVerified: true, }); expect((await store.findUserById(registered.user!.id))?.status).toBe("locked"); time += 61_000; const login = await engine.login({ identifier: "pending-lock@example.com", password: "StrongPassword123", captchaVerified: true, }); expect(login).toMatchObject({ ok: false, code: "email-unverified" }); expect((await store.findUserById(registered.user!.id))?.status).toBe("pending"); }); test("OAuth refreshes preserve stored tokens when a provider omits them", async () => { const { engine } = fixture(); const registered = await engine.register({ email: "oauth-refresh@example.com", password: "StrongPassword123", }); const profile = { id: "oauth-refresh-id", email: "oauth-refresh@example.com", raw: {} }; const first = await engine.linkOAuth(registered.user!.id, "example", profile, { access_token: "access-token", refresh_token: "refresh-token", token_type: "Bearer", scope: "openid profile", }); const second = await engine.linkOAuth(registered.user!.id, "example", profile); expect(second.accessToken).toBe(first.accessToken); expect(second.refreshToken).toBe(first.refreshToken); expect(second.scope).toBe(first.scope); }); test("disabled accounts cannot use previously issued recovery or verification tokens", async () => { const { engine, messages } = fixture(); const registered = await engine.register({ email: "disabled-recovery@example.com", password: "StrongPassword123", }); const verificationToken = messages.find( (message) => message.template === "verify-email", )!.token!; await engine.requestPasswordReset("disabled-recovery@example.com", "https://example.test"); const resetToken = messages.find((message) => message.template === "password-reset")!.token!; await engine.setAccountStatus(registered.user!.id, "disabled"); expect(await engine.verifyEmail(verificationToken)).toMatchObject({ ok: false, code: "account-disabled", }); expect(await engine.resetPassword(resetToken, "ReplacementPassword456")).toMatchObject({ ok: false, code: "account-disabled", }); const before = messages.length; await engine.requestPasswordReset("disabled-recovery@example.com", "https://example.test"); expect(messages).toHaveLength(before); // Administrative re-enablement leaves the original one-time credentials // available because the disabled-account checks did not consume them. await engine.setAccountStatus(registered.user!.id, "active"); expect((await engine.verifyEmail(verificationToken)).ok).toBe(true); expect((await engine.resetPassword(resetToken, "ReplacementPassword456")).ok).toBe(true); }); test("a valid password reset clears a failed-login lock without activating pending users", async () => { const store = new MemoryAuthStore(); const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store, secret: "password-reset-lock-secret-that-is-longer-than-thirty-two-characters", maxFailedLogins: 1, delivery: { async send(message) { messages.push(message); }, }, }); const registered = await engine.register({ email: "locked-reset@example.com", password: "StrongPassword123", }); await engine.login({ identifier: "locked-reset@example.com", password: "WrongPassword123", captchaVerified: true, }); expect((await store.findUserById(registered.user!.id))?.status).toBe("locked"); await engine.requestPasswordReset("locked-reset@example.com", "https://example.test"); const token = messages .filter((message) => message.template === "password-reset") .at(-1)!.token!; expect((await engine.resetPassword(token, "ReplacementPassword456")).ok).toBe(true); expect((await store.findUserById(registered.user!.id))?.status).toBe("pending"); expect( ( await engine.login({ identifier: "locked-reset@example.com", password: "ReplacementPassword456", captchaVerified: true, }) ).ok, ).toBe(true); }); test("MFA email and SMS challenges require verified linked identities", async () => { const { engine, messages, now } = fixture(); const registered = await engine.register({ email: "unverified-mfa@example.com", password: "StrongPassword123", }); const verificationToken = messages.find( (message) => message.template === "verify-email", )!.token!; const setup = await engine.beginTotp(registered.user!.id); const token = await generateTotp(setup.secret, { timestamp: now() }); expect(await engine.confirmTotp(registered.user!.id, setup.credentialId, token)).toBe(true); const firstLogin = await engine.login({ identifier: "unverified-mfa@example.com", password: "StrongPassword123", }); expect(firstLogin).toMatchObject({ ok: false, code: "mfa-required", requires: { mfa: ["totp"] }, }); expect(await engine.beginMfaOtp(firstLogin.mfaToken!, "email-otp")).toBeUndefined(); expect((await engine.verifyEmail(verificationToken)).ok).toBe(true); const secondLogin = await engine.login({ identifier: "unverified-mfa@example.com", password: "StrongPassword123", }); expect(secondLogin.requires?.mfa).toContain("email-otp"); expect(secondLogin.requires?.mfa).toContain("totp"); expect(await engine.beginMfaOtp(secondLogin.mfaToken!, "email-otp")).toBeDefined(); }); });