import { expect, test } from "bun:test"; import { createAuthEngine } from "../src/engine.ts"; import { MemoryAuthStore } from "../src/stores/memory.ts"; import { createAuthHttpHandlers } from "../src/http/index.ts"; import { createContext } from "@wrnexus/core"; import type { AuthDeliveryMessage } from "../src/types.ts"; test("magic-link, OTP, and passkey sign-in cannot bypass new-device verification", async () => { const messages: AuthDeliveryMessage[] = []; let userId = ""; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "all-flows-trust-secret-at-least-32-characters", requireVerifiedEmail: true, requireTrustedDeviceVerification: true, delivery: { async send(message) { messages.push(message); }, }, passkeys: { async registrationOptions() { throw new Error("Not used"); }, async verifyRegistration() { throw new Error("Not used"); }, async authenticationOptions() { return { challenge: "AQID", rpId: "example.test", timeout: 60000, userVerification: "required" as const, }; }, async verifyAuthentication() { return { verified: true, userId }; }, }, }); userId = (await engine.register({ email: "flows@example.test", password: "StrongPassword123!" })) .user!.id; await engine.verifyEmail(messages.find((m) => m.template === "verify-email")!.token!); await engine.requestMagicLink("flows@example.test"); const magic = messages.find((m) => m.template === "magic-link")!.token!; expect((await engine.consumeMagicLink(magic, { fingerprint: "browser" })).code).toBe( "mfa-required", ); const otp = await engine.requestOtpLogin("flows@example.test", "email-otp"); expect( ( await engine.completeOtpLogin( otp.id, messages.filter((m) => m.template === "email-otp").at(-1)!.code!, { fingerprint: "browser" }, ) ).code, ).toBe("mfa-required"); const passkey = await engine.beginPasskeyAuthentication({ identifier: "flows@example.test", rpId: "example.test", origin: "https://example.test", }); expect( ( await engine.finishPasskeyAuthentication({ key: passkey.key, response: {}, session: { fingerprint: "browser" }, }) ).code, ).toBe("mfa-required"); expect(await engine.listSessions(userId)).toHaveLength(0); }); test("new browsers require bound email/SMS verification; signup trust expires without sliding", async () => { let now = 1_780_000_000_000; const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "test-browser-trust-secret-at-least-32-characters", clock: { now: () => now }, requireVerifiedEmail: true, requireTrustedDeviceVerification: true, trustSignupDevice: true, trustedDeviceTtlMs: 30 * 86400_000, sendLoginAlerts: true, delivery: { async send(message) { messages.push(message); }, }, }); const registered = await engine.register({ email: "trust@example.test", phone: "+15551234567", password: "StrongPassword123!", fingerprint: "signup-browser", }); const userId = registered.user!.id; expect( ( await engine.login({ identifier: "trust@example.test", password: "StrongPassword123!", fingerprint: "signup-browser", }) ).code, ).toBe("email-unverified"); expect(await engine.listTrustedDevices(userId)).toHaveLength(0); await engine.verifyEmail(messages.find((m) => m.template === "verify-email")!.token!); await engine.verifyPhone(messages.find((m) => m.template === "verify-phone")!.token!); const login = (fingerprint: string, rememberDevice = false) => engine.login({ identifier: "trust@example.test", password: "StrongPassword123!", fingerprint, rememberDevice, }); expect((await login("signup-browser")).session?.trusted).toBe(true); const firstExpiry = (await engine.listTrustedDevices(userId))[0]!.expiresAt; now += 86400_000; expect((await login("signup-browser", true)).ok).toBe(true); expect((await engine.listTrustedDevices(userId))[0]!.expiresAt).toBe(firstExpiry); const before = (await engine.listSessions(userId)).length; const pending = await login("new-browser", true); expect(pending.code).toBe("mfa-required"); expect(pending.requires?.mfa).toEqual(["email-otp", "sms-otp"]); expect((await engine.listSessions(userId)).length).toBe(before); const challenge = await engine.beginMfaOtp(pending.mfaToken!, "email-otp"); const count = messages.length; expect(await engine.beginMfaOtp(pending.mfaToken!, "email-otp")).toEqual(challenge); expect(messages.length).toBe(count); const code = messages.filter((m) => m.template === "email-otp").at(-1)!.code!; const complete = { mfaToken: pending.mfaToken!, method: "email-otp" as const, challengeId: challenge!.id, code, fingerprint: "new-browser", }; expect((await engine.completeMfa({ ...complete, fingerprint: "other-browser" })).ok).toBe(false); expect((await engine.completeMfa({ ...complete, method: "totp" })).ok).toBe(false); expect((await engine.completeMfa(complete)).ok).toBe(true); expect((await engine.listTrustedDevices(userId)).length).toBe(1); // Explicit opt-in only. expect((await engine.completeMfa(complete)).ok).toBe(false); const again = await login("new-browser"); const sms = await engine.beginMfaOtp(again.mfaToken!, "sms-otp"); const smsCode = messages.filter((m) => m.template === "sms-otp").at(-1)!.code!; expect( ( await engine.completeMfa({ mfaToken: again.mfaToken!, method: "sms-otp", challengeId: sms!.id, code: smsCode, fingerprint: "new-browser", rememberDevice: true, }) ).session?.trusted, ).toBe(true); expect( messages .filter((m) => m.template === "login-alert") .slice(-2) .map((m) => m.channel), ).toEqual(["email", "sms"]); const second = (await engine.listTrustedDevices(userId)).find( (d) => d.expiresAt !== firstExpiry, )!; await engine.revokeTrustedDevice(userId, second.id); expect((await login("new-browser")).code).toBe("mfa-required"); now = firstExpiry + 1; expect((await login("signup-browser")).code).toBe("mfa-required"); }); test("HTTP signup binds trust to an HttpOnly cookie, not a supplied device fingerprint", async () => { const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "http-device-trust-secret-at-least-32-characters", requireVerifiedEmail: true, requireTrustedDeviceVerification: true, trustSignupDevice: true, delivery: { async send(message) { messages.push(message); }, }, }); const url = new URL("https://sso.example.test/api/auth/register"); const ctx = createContext( new Request(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "Trust Test", consent: true, email: "http@example.test", password: "StrongPassword123!", deviceFingerprint: "attacker", }), }), url, ); await createAuthHttpHandlers({ engine }).register(ctx); const cookie = ctx.cookies.headers().find((c) => c.startsWith("wrnexus.auth.device="))!; expect(cookie).toContain("HttpOnly"); expect(cookie).toContain("Secure"); expect(cookie).toContain("Max-Age=2592000"); expect(cookie).not.toContain("attacker"); await engine.verifyEmail(messages.find((m) => m.template === "verify-email")!.token!); expect( ( await engine.login({ identifier: "http@example.test", password: "StrongPassword123!", fingerprint: "attacker", }) ).code, ).toBe("mfa-required"); });