85 lines
3.2 KiB
TypeScript
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 });
|
|
}
|
|
});
|