Files
ThothII/backend/test/auth-session-store.test.ts
T

1272 lines
56 KiB
TypeScript

import { createHash } from "node:crypto";
import { fork } from "node:child_process";
import {
chmodSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
realpathSync,
renameSync,
rmSync,
symlinkSync,
utimesSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
const fsHooks = vi.hoisted(() => ({
afterRead: undefined as undefined | (() => void),
afterWrite: undefined as undefined | (() => void),
beforeLstat: undefined as undefined | ((path: string) => boolean),
afterLstat: undefined as undefined | ((path: string) => boolean),
beforeReaddir: undefined as undefined | ((path: string) => void),
transformLstat: undefined as undefined | ((path: string, info: import("node:fs").Stats) => import("node:fs").Stats),
}));
vi.mock("node:fs", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:fs")>();
return {
...actual,
readSync: (...args: Parameters<typeof actual.readSync>) => {
const result = actual.readSync(...args);
const callback = fsHooks.afterRead;
fsHooks.afterRead = undefined;
callback?.();
return result;
},
writeSync: (...args: Parameters<typeof actual.writeSync>) => {
const result = actual.writeSync(...args);
const callback = fsHooks.afterWrite;
fsHooks.afterWrite = undefined;
callback?.();
return result;
},
lstatSync: (...args: Parameters<typeof actual.lstatSync>) => {
const before = fsHooks.beforeLstat;
if (before?.(String(args[0]))) fsHooks.beforeLstat = undefined;
const original = actual.lstatSync(...args);
const result = fsHooks.transformLstat?.(String(args[0]), original) ?? original;
const callback = fsHooks.afterLstat;
if (callback?.(String(args[0]))) fsHooks.afterLstat = undefined;
return result;
},
readdirSync: (...args: Parameters<typeof actual.readdirSync>) => {
fsHooks.beforeReaddir?.(String(args[0]));
return actual.readdirSync(...args);
},
};
});
import {
createFileAuthSessionStore,
deriveCsrfToken,
validateAuthSessionRoot,
type AuthSessionStore,
type FileAuthSessionStoreOptions,
type SessionCreateInput,
} from "../src/auth/session-store.js";
import { createWindowsAuthStorageBridge } from "../src/auth/windows-auth-storage.js";
const roots: string[] = [];
const base = new Date("2030-01-02T03:04:05.000Z");
const revision = "a".repeat(64);
const validLocalUser = { enabled: true, authRevision: 7, roles: ["admin"] as const };
function oidcInput(nonce: string, codeVerifier: string) {
return {
nonce,
codeVerifier,
returnTo: "/" as const,
authConfigRevision: revision,
issuer: "https://issuer.example.test",
browserTransactionDigest: "b".repeat(64),
browserTransactionTransport: "https" as const,
};
}
function sessionFilename(index: number): string {
return `${index.toString(16).padStart(64, "0")}.json`;
}
afterEach(() => {
fsHooks.afterRead = undefined;
fsHooks.afterWrite = undefined;
fsHooks.beforeLstat = undefined;
fsHooks.afterLstat = undefined;
fsHooks.beforeReaddir = undefined;
fsHooks.transformLstat = undefined;
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
function root(): string {
const path = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-session-"));
chmodSync(path, 0o700);
roots.push(path);
return path;
}
function digestPath(rootPath: string, directory: "sessions" | "oidc", rawValue: string): string {
return join(rootPath, directory, `${createHash("sha256").update(rawValue).digest("hex")}.json`);
}
function claimPath(rootPath: string, rawState: string): string {
return join(rootPath, "oidc", `${createHash("sha256").update(rawState).digest("hex")}.claim`);
}
function validStore(storageRoot: string, options: FileAuthSessionStoreOptions = {}): AuthSessionStore {
return createFileAuthSessionStore(storageRoot, {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, options);
}
async function create(
store: AuthSessionStore,
overrides: Partial<SessionCreateInput> = {},
now = base,
) {
return store.create({
principal: {
issuer: "local",
subject: "6ba7b810-9dad-4ed1-80b4-00c04fd430c8",
displayName: "Local administrator",
roles: ["admin"],
permissions: ["session.use", "settings.manage"],
isAdmin: true,
},
method: "local",
remembered: true,
userAuthRevision: 7,
authConfigRevision: revision,
idleTtlMs: 10 * 60_000,
absoluteTtlMs: 30 * 60_000,
...overrides,
}, now);
}
async function expectStoreInvalid(operation: Promise<unknown>): Promise<void> {
await expect(operation).rejects.toThrow("auth_session_store_invalid");
}
async function isolatedOidcConsumer(storageRoot: string, state: string): Promise<{
start(): void;
result: Promise<boolean>;
}> {
const child = fork(new URL("./fixtures/oidc-state-consumer.mts", import.meta.url), [], {
cwd: process.cwd(),
execArgv: ["--import", "tsx"],
env: {
...process.env,
THT_TEST_SESSION_ROOT: storageRoot,
THT_TEST_OIDC_STATE: state,
},
silent: true,
});
const ready = new Promise<void>((resolve, reject) => {
child.once("message", (message) => {
if (message === "ready") resolve();
else reject(new Error("OIDC consumer did not become ready"));
});
child.once("error", reject);
child.once("exit", (code) => {
if (code !== null && code !== 0) reject(new Error("OIDC consumer exited before ready"));
});
});
const result = new Promise<boolean>((resolve, reject) => {
child.on("message", (message) => {
if (message && typeof message === "object" && "consumed" in message) {
const outcome = message as { consumed: unknown; failed?: unknown };
if (outcome.failed === true) reject(new Error("OIDC consumer failed"));
else resolve(outcome.consumed === true);
}
});
child.on("error", reject);
child.on("exit", (code) => {
if (code !== 0) reject(new Error("OIDC consumer exited without a result"));
});
});
await ready;
return { start: () => child.send("consume"), result };
}
async function isolatedOidcCreator(storageRoot: string, attempts: number): Promise<{
start(): void;
result: Promise<number>;
}> {
const child = fork(new URL("./fixtures/oidc-state-creator.mts", import.meta.url), [], {
cwd: process.cwd(),
execArgv: ["--import", "tsx"],
env: {
...process.env,
THT_TEST_SESSION_ROOT: storageRoot,
THT_TEST_OIDC_ATTEMPTS: String(attempts),
THT_TEST_OIDC_NOW: base.toISOString(),
THT_TEST_OIDC_CAPACITY: "8",
},
silent: true,
});
const ready = new Promise<void>((resolve, reject) => {
child.once("message", (message) => {
if (message === "ready") resolve();
else reject(new Error("OIDC creator did not become ready"));
});
child.once("error", reject);
child.once("exit", (code) => {
if (code !== null && code !== 0) reject(new Error("OIDC creator exited before ready"));
});
});
const result = new Promise<number>((resolve, reject) => {
let outcome: { created: unknown; failed?: unknown; error?: unknown } | undefined;
child.on("message", (message) => {
if (message && typeof message === "object" && "created" in message) {
outcome = message as { created: unknown; failed?: unknown; error?: unknown };
}
});
child.on("error", reject);
child.on("exit", (code) => {
if (code !== 0 || outcome?.failed === true || typeof outcome?.created !== "number") {
reject(new Error(`OIDC creator failed: ${String(outcome?.error)}`));
} else {
resolve(outcome.created);
}
});
});
await ready;
return { start: () => child.send("create"), result };
}
describe("file-backed auth session store", () => {
test.skipIf(process.platform === "win32")("exports its side-effect-free canonical session-root validator", () => {
const valid = root();
expect(() => validateAuthSessionRoot(valid)).not.toThrow();
const outer = root();
const realRoot = join(outer, "real-auth");
mkdirSync(realRoot, { mode: 0o700 });
chmodSync(realRoot, 0o700);
const linkedRoot = join(outer, "linked-auth");
symlinkSync(realRoot, linkedRoot);
const traversal = `${valid}/../${basename(valid)}`;
const absent = join(outer, "absent-auth");
const fileParent = join(outer, "not-a-directory");
writeFileSync(fileParent, "blocked", { mode: 0o600 });
for (const unsafe of [traversal, linkedRoot, absent, join(fileParent, "auth")]) {
expect(() => validateAuthSessionRoot(unsafe)).toThrow("auth_session_store_invalid");
}
chmodSync(valid, 0o750);
expect(() => validateAuthSessionRoot(valid)).toThrow("auth_session_store_invalid");
});
test.skipIf(process.platform === "win32")("rejects a session root owned by another identity", () => {
const storageRoot = root();
fsHooks.transformLstat = (observed, info) => {
if (observed !== storageRoot) return info;
const foreign = Object.create(info) as import("node:fs").Stats;
Object.defineProperty(foreign, "uid", { value: info.uid + 1 });
return foreign;
};
expect(() => validateAuthSessionRoot(storageRoot)).toThrow("auth_session_store_invalid");
});
test("fails closed and revokes a session when constructed without validity dependencies", async () => {
const storageRoot = root();
const store = createFileAuthSessionStore(storageRoot);
const created = await create(store);
await expect(store.resolve(created.token)).resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false);
});
test("revokes a session when a validity dependency throws", async () => {
const storageRoot = root();
const store = createFileAuthSessionStore(storageRoot, {
currentAuthConfigRevision: () => { throw new Error("dependency unavailable"); },
findLocalUser: async () => validLocalUser,
});
const created = await create(store);
await expectStoreInvalid(store.resolve(created.token));
expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false);
});
test("creates 256-bit opaque tokens, digest-only files, and derived CSRF values", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const first = await create(store);
const second = await create(store);
const path = digestPath(storageRoot, "sessions", first.token);
const contents = readFileSync(path, "utf8");
expect(first.token).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(Buffer.from(first.token, "base64url")).toHaveLength(32);
expect(second.token).not.toBe(first.token);
expect(lstatSync(path).isFile()).toBe(true);
expect(path).toMatch(/[a-f0-9]{64}\.json$/);
expect(contents).not.toContain(first.token);
expect(contents).not.toContain(first.csrfToken);
expect(deriveCsrfToken(first.token)).toBe(first.csrfToken);
expect(first.csrfToken).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(first.csrfToken).not.toBe(first.token);
if (process.platform !== "win32") {
expect(lstatSync(storageRoot).mode & 0o7777).toBe(0o700);
expect(lstatSync(join(storageRoot, "sessions")).mode & 0o7777).toBe(0o700);
expect(lstatSync(join(storageRoot, "oidc")).mode & 0o7777).toBe(0o700);
expect(lstatSync(path).mode & 0o7777).toBe(0o600);
}
});
test("survives a backend restart and respects idle and absolute expiry", async () => {
const storageRoot = root();
const firstStore = validStore(storageRoot);
const created = await create(firstStore);
const restartedStore = validStore(storageRoot);
await expect(restartedStore.resolve(created.token, new Date(base.getTime() + 9 * 60_000)))
.resolves.toMatchObject({ subject: created.record.subject, remembered: true });
await expect(restartedStore.resolve(created.token, new Date(base.getTime() + 10 * 60_000)))
.resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false);
const absolute = await create(restartedStore, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 10 * 60_000 });
await expect(restartedStore.resolve(absolute.token, new Date(base.getTime() + 10 * 60_000)))
.resolves.toBeUndefined();
});
test("touches at most once per five minutes and never extends absolute expiry", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const path = digestPath(storageRoot, "sessions", created.token);
const before = readFileSync(path, "utf8");
await store.touch(created.token, new Date(base.getTime() + 4 * 60_000));
expect(readFileSync(path, "utf8")).toBe(before);
await store.touch(created.token, new Date(base.getTime() + 5 * 60_000));
await expect(store.resolve(created.token, new Date(base.getTime() + 5 * 60_000))).resolves.toMatchObject({
lastSeenAt: "2030-01-02T03:09:05.000Z",
idleExpiresAt: "2030-01-02T03:19:05.000Z",
absoluteExpiresAt: "2030-01-02T03:34:05.000Z",
});
});
test("revokes sessions and prunes expired session and OIDC-state records", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const revoked = await create(store);
const expired = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
const oidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
await store.revoke(revoked.token);
await expect(store.resolve(revoked.token)).resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", revoked.token))).toBe(false);
await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2);
expect(existsSync(digestPath(storageRoot, "sessions", expired.token))).toBe(false);
expect(existsSync(digestPath(storageRoot, "oidc", oidc.state))).toBe(false);
});
test.skipIf(process.platform === "win32")("allows startup maintenance past 512 live records and keeps OIDC available", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const liveSeed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 });
const liveContents = readFileSync(digestPath(storageRoot, "sessions", liveSeed.token));
unlinkSync(digestPath(storageRoot, "sessions", liveSeed.token));
const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
const expiredContents = readFileSync(digestPath(storageRoot, "sessions", expiredSeed.token));
unlinkSync(digestPath(storageRoot, "sessions", expiredSeed.token));
for (let index = 0; index < 512; index += 1) {
writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), liveContents, { mode: 0o600 });
}
const expiredName = sessionFilename(512);
writeFileSync(join(storageRoot, "sessions", expiredName), expiredContents, { mode: 0o600 });
await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(0);
expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(true);
expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true);
await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base))
.resolves.toMatchObject({ record: { nonce: "n".repeat(43) } });
await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(1);
expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(false);
expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true);
});
test.skipIf(process.platform === "win32")("serializes concurrent continuation passes without stranding a later page", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const liveSeed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 });
const liveContents = readFileSync(digestPath(storageRoot, "sessions", liveSeed.token));
unlinkSync(digestPath(storageRoot, "sessions", liveSeed.token));
const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
const expiredContents = readFileSync(digestPath(storageRoot, "sessions", expiredSeed.token));
unlinkSync(digestPath(storageRoot, "sessions", expiredSeed.token));
for (let index = 0; index < 512; index += 1) {
writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), liveContents, { mode: 0o600 });
}
const expiredName = sessionFilename(512);
writeFileSync(join(storageRoot, "sessions", expiredName), expiredContents, { mode: 0o600 });
await expect(Promise.all([
store.prune(new Date(base.getTime() + 2 * 60_000)),
store.prune(new Date(base.getTime() + 2 * 60_000)),
])).resolves.toEqual([0, 1]);
expect(existsSync(join(storageRoot, "sessions", expiredName))).toBe(false);
expect(existsSync(join(storageRoot, "sessions", sessionFilename(0)))).toBe(true);
});
test.skipIf(process.platform === "win32")("fails closed on an unsafe ordinary record beyond the first continuation page", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const seed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 });
const contents = readFileSync(digestPath(storageRoot, "sessions", seed.token));
unlinkSync(digestPath(storageRoot, "sessions", seed.token));
for (let index = 0; index < 512; index += 1) {
writeFileSync(join(storageRoot, "sessions", sessionFilename(index)), contents, { mode: 0o600 });
}
symlinkSync(join(storageRoot, "missing-target"), join(storageRoot, "sessions", sessionFilename(512)));
await expect(store.prune(new Date(base.getTime() + 2 * 60_000)))
.rejects.toThrow("auth_session_store_invalid");
});
test.skipIf(process.platform === "win32")("fails closed if the ordinary-session directory changes during a page scan", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const seed = await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 });
const contents = readFileSync(digestPath(storageRoot, "sessions", seed.token));
unlinkSync(digestPath(storageRoot, "sessions", seed.token));
const filename = sessionFilename(0);
const sessionsDirectory = join(storageRoot, "sessions");
writeFileSync(join(sessionsDirectory, filename), contents, { mode: 0o600 });
fsHooks.beforeLstat = (path) => {
if (path !== join(sessionsDirectory, filename)) return false;
const changed = new Date(base.getTime() + 60 * 60_000);
utimesSync(sessionsDirectory, changed, changed);
return true;
};
await expect(store.prune(new Date(base.getTime() + 2 * 60_000)))
.rejects.toThrow("auth_session_store_invalid");
});
test("persists the OIDC transaction transport as consumed state metadata", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState({
...oidcInput("n".repeat(43), "v".repeat(43)),
browserTransactionTransport: "loopback_http",
});
await expect(store.consumeOidcState(created.state)).resolves.toMatchObject({
browserTransactionTransport: "loopback_http",
});
});
test("creates bounded OIDC state records that expire and are single-use", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const path = digestPath(storageRoot, "oidc", created.state);
expect(created.state).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(readFileSync(path, "utf8")).not.toContain(created.state);
await expect(store.consumeOidcState(created.state, new Date(base.getTime() + 9 * 60_000)))
.resolves.toMatchObject({
nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/",
authConfigRevision: revision, issuer: "https://issuer.example.test",
browserTransactionDigest: "b".repeat(64),
browserTransactionTransport: "https",
});
await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined();
const expired = await store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base);
await expect(store.consumeOidcState(expired.state, new Date(base.getTime() + 10 * 60_000)))
.resolves.toBeUndefined();
});
test("rejects an OIDC-state flood without deleting any valid live state", async () => {
const storageRoot = root();
const store = validStore(storageRoot, { oidcStateCapacity: 8 });
const created = await Promise.all(Array.from({ length: 8 }, (_unused, index) =>
store.createOidcState(oidcInput(`n${String(index).padStart(42, "0")}`, `v${String(index).padStart(42, "0")}`), base)));
await expect(store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base))
.rejects.toThrow("auth_oidc_state_capacity");
await expect(store.consumeOidcState(created[0].state, new Date(base.getTime() + 60_000)))
.resolves.toMatchObject({ nonce: "n" + "0".repeat(42) });
expect(created.slice(1).every(({ state }) => existsSync(digestPath(storageRoot, "oidc", state)))).toBe(true);
});
test("prunes expired OIDC states before admitting a new transaction at capacity", async () => {
const storageRoot = root();
const store = validStore(storageRoot, { oidcStateCapacity: 8 });
const expired = await Promise.all(Array.from({ length: 8 }, (_unused, index) =>
store.createOidcState(oidcInput(`n${String(index).padStart(42, "0")}`, `v${String(index).padStart(42, "0")}`), base)));
const admitted = await store.createOidcState(
oidcInput("x".repeat(43), "y".repeat(43)),
new Date(base.getTime() + 10 * 60_000),
);
expect(expired.every(({ state }) => !existsSync(digestPath(storageRoot, "oidc", state)))).toBe(true);
expect(existsSync(digestPath(storageRoot, "oidc", admitted.state))).toBe(true);
});
test.skipIf(process.platform === "win32")("starts OIDC without scanning more than 256 ordinary session records", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const liveSession = await create(store);
for (let index = 0; index < 300; index += 1) {
writeFileSync(join(storageRoot, "sessions", `${index.toString(16).padStart(64, "0")}.json`), "{}", {
encoding: "utf8",
mode: 0o600,
});
}
const oidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
expect(oidc.state).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(existsSync(digestPath(storageRoot, "sessions", liveSession.token))).toBe(true);
});
test.skipIf(process.platform === "win32")("uses bounded directory reads for ordinary and OIDC pruning", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const expiredSession = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
const expiredOidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
fsHooks.beforeReaddir = (path) => {
if (path === join(storageRoot, "sessions") || path === join(storageRoot, "oidc")) {
throw new Error("unbounded directory read");
}
};
await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2);
expect(existsSync(digestPath(storageRoot, "sessions", expiredSession.token))).toBe(false);
expect(existsSync(digestPath(storageRoot, "oidc", expiredOidc.state))).toBe(false);
});
test.skipIf(process.platform === "win32")("rejects an overfull OIDC directory without deleting a live state", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const live = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
for (let index = 0; index < 193; index += 1) {
writeFileSync(join(storageRoot, "oidc", `${(index + 1_000).toString(16).padStart(64, "0")}.json`), "{}", {
encoding: "utf8",
mode: 0o600,
});
}
await expect(store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base))
.rejects.toThrow("auth_session_store_invalid");
expect(existsSync(digestPath(storageRoot, "oidc", live.state))).toBe(true);
});
test("OIDC-only cleanup removes expired state while preserving a live transaction", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const expired = await store.createOidcState(oidcInput("e".repeat(43), "f".repeat(43)), base);
const live = await store.createOidcState(
oidcInput("l".repeat(43), "m".repeat(43)),
new Date(base.getTime() + 60_000),
);
const admitted = await store.createOidcState(
oidcInput("n".repeat(43), "v".repeat(43)),
new Date(base.getTime() + 10 * 60_000),
);
expect(existsSync(digestPath(storageRoot, "oidc", expired.state))).toBe(false);
expect(existsSync(digestPath(storageRoot, "oidc", admitted.state))).toBe(true);
await expect(store.consumeOidcState(live.state, new Date(base.getTime() + 10 * 60_000)))
.resolves.toMatchObject({ nonce: "l".repeat(43) });
});
test("bounds OIDC state creation across concurrent Node processes", async () => {
const storageRoot = root();
const creators = await Promise.all([
isolatedOidcCreator(storageRoot, 6),
isolatedOidcCreator(storageRoot, 6),
]);
creators.forEach(({ start }) => start());
const winners = await Promise.all(creators.map(({ result }) => result));
expect(winners.reduce((sum, value) => sum + value, 0)).toBe(8);
const names = readdirSync(join(storageRoot, "oidc"));
expect(names.filter((name) => /^[a-f0-9]{64}\.json$/.test(name))).toHaveLength(8);
});
test("fails closed when an OIDC state already has an atomic filesystem claim", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const statePath = digestPath(storageRoot, "oidc", created.state);
linkSync(statePath, claimPath(storageRoot, created.state));
await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined();
expect(existsSync(statePath)).toBe(true);
});
test("treats a competing OIDC claim installed between availability and state checks as unavailable", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const statePath = digestPath(storageRoot, "oidc", created.state);
const stateClaimPath = claimPath(storageRoot, created.state);
fsHooks.beforeLstat = (observed) => {
if (observed !== statePath) return false;
linkSync(statePath, stateClaimPath);
return true;
};
await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined();
expect(existsSync(statePath)).toBe(true);
expect(existsSync(stateClaimPath)).toBe(true);
});
test("prunes an expired OIDC state abandoned after an atomic claim", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const statePath = digestPath(storageRoot, "oidc", created.state);
const stateClaimPath = claimPath(storageRoot, created.state);
linkSync(statePath, stateClaimPath);
await expect(store.prune(new Date(base.getTime() + 10 * 60_000))).resolves.toBe(1);
expect(existsSync(statePath)).toBe(false);
expect(existsSync(stateClaimPath)).toBe(false);
});
test("retains an in-flight orphan claim but removes it after the bounded recovery window", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const statePath = digestPath(storageRoot, "oidc", created.state);
const stateClaimPath = claimPath(storageRoot, created.state);
linkSync(statePath, stateClaimPath);
unlinkSync(statePath);
await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined();
expect(existsSync(stateClaimPath)).toBe(true);
await expect(store.prune(new Date("2031-01-02T03:04:05.000Z"))).resolves.toBe(1);
expect(existsSync(stateClaimPath)).toBe(false);
});
test("allows exactly one separate Node isolate to consume an OIDC state", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
const [first, second] = await Promise.all([
isolatedOidcConsumer(storageRoot, created.state),
isolatedOidcConsumer(storageRoot, created.state),
]);
first.start();
second.start();
const outcomes = await Promise.all([first.result, second.result]);
expect(outcomes.filter(Boolean)).toHaveLength(1);
expect(existsSync(digestPath(storageRoot, "oidc", created.state))).toBe(false);
});
test.skipIf(process.platform === "win32")("refuses symlinked and hard-linked session records", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const symlinked = await create(store);
const symlinkPath = digestPath(storageRoot, "sessions", symlinked.token);
const target = `${symlinkPath}.target`;
renameSync(symlinkPath, target);
symlinkSync(target, symlinkPath);
await expectStoreInvalid(store.resolve(symlinked.token));
const hardLinked = await create(store);
const hardLinkPath = digestPath(storageRoot, "sessions", hardLinked.token);
linkSync(hardLinkPath, `${hardLinkPath}.link`);
await expectStoreInvalid(store.resolve(hardLinked.token));
});
test("refuses malformed and oversized session records without disclosing their contents", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const malformed = await create(store);
const malformedPath = digestPath(storageRoot, "sessions", malformed.token);
writeFileSync(malformedPath, "{}", { encoding: "utf8", mode: 0o600 });
chmodSync(malformedPath, 0o600);
await expectStoreInvalid(store.resolve(malformed.token));
const oversized = await create(store);
const oversizedPath = digestPath(storageRoot, "sessions", oversized.token);
writeFileSync(oversizedPath, "#".repeat(20_000), { encoding: "utf8", mode: 0o600 });
chmodSync(oversizedPath, 0o600);
await expectStoreInvalid(store.resolve(oversized.token));
});
test.skipIf(process.platform === "win32")("refuses unsafe existing roots and storage subdirectories", async () => {
const unsafeRoot = root();
chmodSync(unsafeRoot, 0o755);
await expectStoreInvalid(create(validStore(unsafeRoot)));
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
chmodSync(join(storageRoot, "sessions"), 0o755);
await expectStoreInvalid(store.resolve(created.token));
const outer = root();
const realRoot = join(outer, "real-auth");
mkdirSync(realRoot, { mode: 0o700 });
chmodSync(realRoot, 0o700);
const linkedRoot = join(outer, "linked-auth");
symlinkSync(realRoot, linkedRoot);
await expectStoreInvalid(create(validStore(linkedRoot)));
});
test.skipIf(process.platform === "win32")("refuses storage owned by a different identity", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const sessions = join(storageRoot, "sessions");
fsHooks.transformLstat = (observed, info) => {
if (observed !== sessions) return info;
const foreign = Object.create(info) as import("node:fs").Stats;
Object.defineProperty(foreign, "uid", { value: info.uid + 1 });
return foreign;
};
await expectStoreInvalid(store.resolve(created.token));
});
test.skipIf(process.platform === "win32")("refuses directory replacement during a session read", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const sessions = join(storageRoot, "sessions");
const replacement = join(storageRoot, "sessions-replacement");
fsHooks.afterRead = () => {
renameSync(sessions, replacement);
symlinkSync(replacement, sessions);
};
await expectStoreInvalid(store.resolve(created.token));
});
test("refuses file replacement during a touch", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const path = digestPath(storageRoot, "sessions", created.token);
const replacement = `${path}.replacement`;
fsHooks.afterWrite = () => {
writeFileSync(replacement, "{}", { encoding: "utf8", mode: 0o600 });
chmodSync(replacement, 0o600);
renameSync(replacement, path);
};
await expectStoreInvalid(store.touch(created.token, new Date(base.getTime() + 5 * 60_000)));
});
test.skipIf(process.platform === "win32")("refuses directory replacement during a touch", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const path = digestPath(storageRoot, "sessions", created.token);
const sessions = join(storageRoot, "sessions");
const replacement = join(storageRoot, "sessions-replacement");
fsHooks.afterLstat = (observed) => {
if (observed !== path) return false;
renameSync(sessions, replacement);
symlinkSync(replacement, sessions);
return true;
};
await expectStoreInvalid(store.touch(created.token, new Date(base.getTime() + 5 * 60_000)));
});
test("refuses file replacement during revoke", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const path = digestPath(storageRoot, "sessions", created.token);
const replacement = `${path}.replacement`;
writeFileSync(replacement, "{}", { encoding: "utf8", mode: 0o600 });
chmodSync(replacement, 0o600);
fsHooks.afterLstat = (observed) => {
if (observed !== path) return false;
renameSync(replacement, path);
return true;
};
await expectStoreInvalid(store.revoke(created.token));
expect(readFileSync(path, "utf8")).toBe("{}");
});
test.skipIf(process.platform === "win32")("refuses directory replacement during revoke", async () => {
const storageRoot = root();
const store = validStore(storageRoot);
const created = await create(store);
const path = digestPath(storageRoot, "sessions", created.token);
const sessions = join(storageRoot, "sessions");
const replacement = join(storageRoot, "sessions-replacement");
fsHooks.afterLstat = (observed) => {
if (observed !== path) return false;
renameSync(sessions, replacement);
symlinkSync(replacement, sessions);
return true;
};
await expectStoreInvalid(store.revoke(created.token));
});
test("routes native Windows session creation through an injected storage bridge", async () => {
const storageRoot = root();
const store = validStore(join(storageRoot, "windows-auth-state"));
const createRecord = vi.fn(async () => true);
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const created = await create(
createFileAuthSessionStore(join(storageRoot, "windows-auth-state"), {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: { create: createRecord } } as never),
);
expect(created.token).toMatch(/^[A-Za-z0-9_-]{43}$/);
expect(createRecord).toHaveBeenCalledTimes(1);
expect(existsSync(join(storageRoot, "windows-auth-state"))).toBe(false);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("routes native Windows lifecycle and OIDC operations only through the storage bridge", async () => {
const records = new Map<string, Buffer>();
const calls: string[] = [];
const key = (directory: string, filename: string) => `${directory}/${filename}`;
const bridge = {
create: async (_root: string, directory: string, filename: string, contents: Buffer) => {
calls.push("create");
const entry = key(directory, filename);
if (records.has(entry)) return false;
records.set(entry, Buffer.from(contents));
return true;
},
read: async (_root: string, directory: string, filename: string) => {
calls.push("read");
const value = records.get(key(directory, filename));
return value === undefined ? undefined : Buffer.from(value);
},
replace: async (_root: string, directory: string, filename: string, contents: Buffer) => {
calls.push("replace");
records.set(key(directory, filename), Buffer.from(contents));
},
remove: async (_root: string, directory: string, filename: string) => {
calls.push("remove");
return records.delete(key(directory, filename));
},
list: async (_root: string, directory: string) => {
calls.push("list");
return [...records.keys()]
.filter((entry) => entry.startsWith(`${directory}/`))
.map((entry) => ({ name: entry.slice(directory.length + 1), modifiedUnixMs: base.getTime() }));
},
listPage: async (_root: string, directory: string, after: string | undefined, maximumEntries: number) => {
calls.push("list");
if (directory !== "sessions") throw new Error("only ordinary sessions use continuation pages");
const names = [...records.keys()]
.filter((entry) => entry.startsWith("sessions/"))
.map((entry) => entry.slice("sessions/".length))
.filter((name) => after === undefined || name > after)
.sort();
return {
entries: names.slice(0, maximumEntries).map((name) => ({ name, modifiedUnixMs: base.getTime() })),
more: names.length > maximumEntries,
};
},
claimConsume: async (_root: string, filename: string) => {
calls.push("claim-consume");
const entry = key("oidc", filename);
const value = records.get(entry);
records.delete(entry);
return value === undefined ? undefined : Buffer.from(value);
},
readClaim: async () => undefined,
removeClaim: async () => false,
};
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: bridge } as never);
const session = await create(store);
await expect(store.resolve(session.token)).resolves.toMatchObject({ subject: session.record.subject });
await store.touch(session.token, new Date(base.getTime() + 5 * 60_000));
await store.revoke(session.token);
await expect(store.resolve(session.token)).resolves.toBeUndefined();
const oidc = await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
await expect(store.consumeOidcState(oidc.state, new Date(base.getTime() + 9 * 60_000)))
.resolves.toMatchObject({ nonce: "n".repeat(43) });
await expect(store.consumeOidcState(oidc.state)).resolves.toBeUndefined();
await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
await store.createOidcState(oidcInput("x".repeat(43), "y".repeat(43)), base);
await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2);
expect(calls).toEqual(expect.arrayContaining(["create", "read", "replace", "remove", "claim-consume", "list"]));
expect(records).toHaveLength(0);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("continues Windows ordinary-session maintenance with a strict 512-record page cursor", async () => {
const records = new Map<string, Buffer>();
const pages: Array<{ after: string | undefined; maximumEntries: number }> = [];
const key = (directory: string, filename: string) => `${directory}/${filename}`;
const bridge = {
create: async (_root: string, directory: string, filename: string, contents: Buffer) => {
const entry = key(directory, filename);
if (records.has(entry)) return false;
records.set(entry, Buffer.from(contents));
return true;
},
read: async (_root: string, directory: string, filename: string) => {
const value = records.get(key(directory, filename));
return value === undefined ? undefined : Buffer.from(value);
},
remove: async (_root: string, directory: string, filename: string) => records.delete(key(directory, filename)),
list: async (_root: string, directory: string) => {
if (directory === "sessions") throw new Error("ordinary maintenance must use a continuation page");
return [];
},
listPage: async (_root: string, directory: string, after: string | undefined, maximumEntries: number) => {
if (directory !== "sessions") throw new Error("only ordinary sessions use continuation pages");
pages.push({ after, maximumEntries });
const names = [...records.keys()]
.filter((entry) => entry.startsWith("sessions/"))
.map((entry) => entry.slice("sessions/".length))
.filter((name) => after === undefined || name > after)
.sort();
return {
entries: names.slice(0, maximumEntries).map((name) => ({ name, modifiedUnixMs: base.getTime() })),
more: names.length > maximumEntries,
};
},
};
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: bridge as never });
await create(store, { idleTtlMs: 30 * 60_000, absoluteTtlMs: 30 * 60_000 });
const liveContents = [...records.values()][0];
records.clear();
const expiredSeed = await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
const expiredContents = [...records.values()][0];
records.clear();
if (!liveContents || !expiredContents) throw new Error("seed records missing");
for (let index = 0; index < 512; index += 1) {
records.set(key("sessions", sessionFilename(index)), Buffer.from(liveContents));
}
const expiredName = sessionFilename(512);
records.set(key("sessions", expiredName), Buffer.from(expiredContents));
await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(0);
expect(records.has(key("sessions", expiredName))).toBe(true);
await expect(store.prune(new Date(base.getTime() + 2 * 60_000))).resolves.toBe(1);
expect(records.has(key("sessions", expiredName))).toBe(false);
expect(records.has(key("sessions", sessionFilename(0)))).toBe(true);
expect(pages).toEqual([
{ after: undefined, maximumEntries: 512 },
{ after: sessionFilename(511), maximumEntries: 512 },
]);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("starts OIDC on Windows without listing more than 256 ordinary session records", async () => {
const records = new Map<string, Buffer>();
for (let index = 0; index < 300; index += 1) {
records.set(`sessions/${index.toString(16).padStart(64, "0")}.json`, Buffer.from("{}"));
}
const key = (directory: string, filename: string) => `${directory}/${filename}`;
const listed: string[] = [];
const bridge = {
create: async (_root: string, directory: string, filename: string, contents: Buffer) => {
const entry = key(directory, filename);
if (records.has(entry)) return false;
records.set(entry, Buffer.from(contents));
return true;
},
read: async (_root: string, directory: string, filename: string) => records.get(key(directory, filename)),
remove: async (_root: string, directory: string, filename: string) => records.delete(key(directory, filename)),
list: async (_root: string, directory: string) => {
listed.push(directory);
if (directory === "sessions") throw new Error("ordinary sessions must not be listed during OIDC start");
return [...records.keys()]
.filter((entry) => entry.startsWith("oidc/"))
.map((entry) => ({ name: entry.slice("oidc/".length), modifiedUnixMs: base.getTime() }));
},
};
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", undefined, {
windowsStorageBridge: bridge as never,
});
await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base))
.resolves.toMatchObject({ record: { nonce: "n".repeat(43) } });
expect(listed).toEqual(["oidc", "oidc"]);
expect([...records.keys()].filter((entry) => entry.startsWith("sessions/"))).toHaveLength(300);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("rejects an overfull Windows OIDC directory before reading or deleting live entries", async () => {
const entries = Array.from({ length: 193 }, (_unused, index) => ({
name: `${index.toString(16).padStart(64, "0")}.json`,
modifiedUnixMs: base.getTime(),
}));
const read = vi.fn(async () => undefined);
const remove = vi.fn(async () => false);
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", undefined, {
windowsStorageBridge: {
list: async () => entries,
read,
remove,
} as never,
});
await expect(store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base))
.rejects.toThrow("auth_session_store_invalid");
expect(read).not.toHaveBeenCalled();
expect(remove).not.toHaveBeenCalled();
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("prunes nonempty Windows directories through the Go lower-camel list DTO", async () => {
const records = new Map<string, Buffer>();
const key = (directory: string, entry: string) => `${directory}/${entry}`;
const response = (value: Record<string, unknown>) => ({
code: 0,
stdout: Buffer.from(`${JSON.stringify({ version: 1, ok: true, ...value })}\n`),
stderr: Buffer.alloc(0),
});
const bridge = createWindowsAuthStorageBridge({
thtExecutable: "C:\\tht.exe",
invoke: async ({ input }) => {
const request = JSON.parse(input.toString("utf8")) as {
operation: string;
directory: "sessions" | "oidc";
filename?: string;
contentBase64?: string;
continuation?: boolean;
afterName?: string;
};
const entry = request.filename === undefined ? undefined : key(request.directory, request.filename);
switch (request.operation) {
case "create":
if (entry === undefined || request.contentBase64 === undefined || records.has(entry)) return response({ created: false });
records.set(entry, Buffer.from(request.contentBase64, "base64"));
return response({ created: true });
case "read":
return entry === undefined || !records.has(entry)
? response({})
: response({ found: true, contentBase64: records.get(entry)?.toString("base64") });
case "remove":
return response({ removed: entry !== undefined && records.delete(entry) });
case "list":
if (request.continuation === true) {
const names = [...records.keys()]
.filter((value) => value.startsWith(`${request.directory}/`))
.map((value) => value.slice(request.directory.length + 1))
.filter((name) => request.afterName === undefined || name > request.afterName)
.sort();
return response({
entries: names.slice(0, 512).map((name) => ({ name, modifiedUnixMs: base.getTime() })),
more: names.length > 512,
});
}
return response({
// Raw lower-camel entry objects, exactly as authstorage's Go response emits them.
entries: [...records.keys()]
.filter((value) => value.startsWith(`${request.directory}/`))
.map((value) => ({ name: value.slice(request.directory.length + 1), modifiedUnixMs: base.getTime() })),
});
default:
throw new Error("unexpected bridge operation");
}
},
});
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: bridge });
await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
await store.createOidcState(oidcInput("n".repeat(43), "v".repeat(43)), base);
await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(2);
expect(records).toHaveLength(0);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("prunes empty Windows directories through the required Go entries array", async () => {
const bridge = createWindowsAuthStorageBridge({
thtExecutable: "C:\\tht.exe",
invoke: async ({ input }) => {
const request = JSON.parse(input.toString("utf8")) as { continuation?: boolean };
return {
code: 0,
stdout: Buffer.from(`${JSON.stringify({
version: 1,
ok: true,
entries: [],
...(request.continuation === true ? { more: false } : {}),
})}\n`),
stderr: Buffer.alloc(0),
};
},
});
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: bridge });
await expect(store.prune(base)).resolves.toBe(0);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("prunes a stale Windows OIDC orphan claim through the Go DTO path", async () => {
const claim = `${"d".repeat(64)}.claim`;
let removed = false;
const bridge = createWindowsAuthStorageBridge({
thtExecutable: "C:\\tht.exe",
invoke: async ({ input }) => {
const request = JSON.parse(input.toString("utf8")) as {
operation: string;
directory: string;
filename?: string;
continuation?: boolean;
};
const response = (value: Record<string, unknown>) => ({
code: 0,
stdout: Buffer.from(`${JSON.stringify({ version: 1, ok: true, ...value })}\n`),
stderr: Buffer.alloc(0),
});
if (request.operation === "list") {
return response({
entries: request.directory === "oidc"
? [{ name: claim, modifiedUnixMs: base.getTime() }]
: [],
...(request.continuation === true ? { more: false } : {}),
});
}
if (request.operation === "remove" && request.directory === "oidc" && request.filename === claim) {
removed = true;
return response({ removed: true });
}
throw new Error("unexpected bridge request");
},
});
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
if (!originalPlatform) throw new Error("platform descriptor unavailable");
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
try {
const store = createFileAuthSessionStore("C:\\ProgramData\\ThothII\\auth", {
currentAuthConfigRevision: () => revision,
findLocalUser: async () => validLocalUser,
}, { windowsStorageBridge: bridge });
await expect(store.prune(new Date(base.getTime() + 11 * 60_000))).resolves.toBe(1);
expect(removed).toBe(true);
} finally {
Object.defineProperty(process, "platform", originalPlatform);
}
});
test("revokes on config, local-user, revision, enabled, or role mismatch before returning", async () => {
const storageRoot = root();
let currentRevision = revision;
let localUser: { enabled: boolean; authRevision: number; roles: readonly ("user" | "admin")[] } | undefined = {
enabled: true,
authRevision: 7,
roles: ["admin"],
};
const store = createFileAuthSessionStore(storageRoot, {
currentAuthConfigRevision: () => currentRevision,
findLocalUser: async () => localUser,
});
const configChanged = await create(store);
currentRevision = "b".repeat(64);
await expect(store.resolve(configChanged.token)).resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", configChanged.token))).toBe(false);
currentRevision = revision;
for (const mismatch of [
undefined,
{ enabled: false, authRevision: 7, roles: ["admin"] as const },
{ enabled: true, authRevision: 8, roles: ["admin"] as const },
{ enabled: true, authRevision: 7, roles: ["user"] as const },
]) {
localUser = mismatch;
const session = await create(store);
await expect(store.resolve(session.token)).resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", session.token))).toBe(false);
}
});
test("serializes concurrent resolve and revoke without resurrecting a record", async () => {
const storageRoot = root();
const firstStore = validStore(storageRoot);
const secondStore = validStore(storageRoot);
const created = await create(firstStore);
await Promise.all([
...Array.from({ length: 8 }, () => firstStore.resolve(created.token)),
...Array.from({ length: 8 }, () => secondStore.revoke(created.token)),
]);
await expect(firstStore.resolve(created.token)).resolves.toBeUndefined();
expect(existsSync(digestPath(storageRoot, "sessions", created.token))).toBe(false);
});
});