import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { authConfig, base32, matchingStep, newAuthConfig, safeReturn, sessionCookie, signSession, totp, validSession, } from "../src/lib/auth"; import { consumeCode } from "../src/lib/auth-store"; test("RFC6238 SHA1 vectors, format and narrow time window", () => { const secret = base32(Buffer.from("12345678901234567890")); for (const [time, expected] of [ [59, "94287082"], [1111111109, "07081804"], [2000000000, "69279037"], ] as const) { assert.equal(totp(secret, Math.floor(time / 30), 8), expected); } assert.equal(matchingStep(secret, "287082", 59000), 1); assert.equal(matchingStep(secret, "123", 59000), null); assert.equal(matchingStep(secret, totp(secret, 10), 1000000), null); }); test("signed sessions reject tampering, expiration, future issuance and excessive lifetime", () => { const config = newAuthConfig(); const token = signSession(config, 100); assert.equal(validSession(token, config, 101), true); assert.equal(validSession(token + "x", config, 101), false); assert.equal(validSession(token, newAuthConfig(), 101), false); assert.equal(validSession(signSession(config, 100, 1), config, 101), false); assert.equal(validSession(signSession(config, 200), config, 100), false); assert.equal( validSession(signSession(config, 100, 604801), config, 101), false, ); assert.equal(validSession("a.b.c", config), false); assert.equal(safeReturn("//untrusted.example"), "/"); assert.equal(safeReturn("/\\untrusted.example"), "/"); assert.equal(safeReturn("/docs/example"), "/docs/example"); assert.match(sessionCookie("test"), /HttpOnly; SameSite=Lax/); assert.match(sessionCookie("", true), /Max-Age=0/); }); test("durable replay protection and rate limit survive reopened SQLite connections", () => { const directory = mkdtempSync(join(tmpdir(), "tabularium-auth-state-")); const previous = process.env.AUTH_CONFIG_FILE; try { const config = newAuthConfig(); process.env.AUTH_CONFIG_FILE = join(directory, "config.json"); writeFileSync(process.env.AUTH_CONFIG_FILE, JSON.stringify(config), { mode: 0o600, }); assert.deepEqual(authConfig(), config); const now = 2000000000000; const code = totp(config.secret, Math.floor(now / 30000)); assert.equal(consumeCode(config, code, now), "ok"); assert.equal(consumeCode(config, code, now), "invalid"); for (let i = 0; i < 18; i++) assert.equal(consumeCode(config, "invalid", now), "invalid"); assert.equal(consumeCode(config, code, now), "limited"); const later = now + 300001; assert.equal( consumeCode( config, totp(config.secret, Math.floor(later / 30000)), later, ), "ok", ); writeFileSync(process.env.AUTH_CONFIG_FILE, "{}"); assert.throws(() => authConfig()); } finally { if (previous === undefined) delete process.env.AUTH_CONFIG_FILE; else process.env.AUTH_CONFIG_FILE = previous; rmSync(directory, { recursive: true, force: true }); } });