Files
codex-bot 9fcfbdb694
Verify and deploy Tabularium / verify-and-deploy (push) Successful in 5m28s
Protect Tabularium with persistent authenticator-code login
2026-10-27 21:48:16 +08:00

85 lines
3.2 KiB
TypeScript

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 });
}
});