fix(auth): pin native auth storage operations

This commit is contained in:
2026-08-17 14:52:21 +02:00
parent 6d438f4c7e
commit cb0e873ed7
23 changed files with 3016 additions and 1385 deletions
+4 -1
View File
@@ -1,4 +1,5 @@
import { createFileAuthSessionStore } from "../../src/auth/session-store.js";
import { createFixturePosixAuthStorageBridge } from "./posix-auth-storage-bridge.mjs";
const root = process.env.THT_TEST_SESSION_ROOT;
const state = process.env.THT_TEST_OIDC_STATE;
@@ -9,7 +10,9 @@ process.send("ready");
process.once("message", async (message) => {
if (message !== "consume") process.exit(3);
try {
const record = await createFileAuthSessionStore(root).consumeOidcState(state);
const record = await createFileAuthSessionStore(root, undefined, {
posixStorageBridge: createFixturePosixAuthStorageBridge(),
}).consumeOidcState(state);
process.send?.({ consumed: record !== undefined });
process.exit(0);
} catch {
+5 -1
View File
@@ -1,4 +1,5 @@
import { createFileAuthSessionStore } from "../../src/auth/session-store.js";
import { createFixturePosixAuthStorageBridge } from "./posix-auth-storage-bridge.mjs";
const root = process.env.THT_TEST_SESSION_ROOT;
const attempts = Number(process.env.THT_TEST_OIDC_ATTEMPTS);
@@ -7,7 +8,10 @@ const capacity = Number(process.env.THT_TEST_OIDC_CAPACITY);
if (!root || !Number.isSafeInteger(attempts) || attempts < 1 || Number.isNaN(now.getTime())
|| !Number.isSafeInteger(capacity) || capacity < 1) process.exit(2);
const store = createFileAuthSessionStore(root, undefined, { oidcStateCapacity: capacity });
const store = createFileAuthSessionStore(root, undefined, {
oidcStateCapacity: capacity,
posixStorageBridge: createFixturePosixAuthStorageBridge(),
});
process.send?.("ready");
process.once("message", async (message) => {
if (message !== "create") process.exit(3);
+150
View File
@@ -0,0 +1,150 @@
import {
chmodSync,
existsSync,
linkSync,
lstatSync,
mkdirSync,
opendirSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import type {
WindowsAuthStorageBridge,
WindowsAuthStorageDirectory,
} from "../../src/auth/windows-auth-storage.js";
// Test-process fixture only. The production POSIX implementation is the Go auth-storage bridge;
// the Go package covers retained-descriptor adversarial races separately.
export function createFixturePosixAuthStorageBridge(): WindowsAuthStorageBridge {
const ensure = async (root: string): Promise<void> => {
if (!existsSync(root)) {
mkdirSync(root, { mode: 0o700 });
chmodSync(root, 0o700);
}
for (const name of ["sessions", "oidc"]) {
const path = join(root, name);
if (!existsSync(path)) {
mkdirSync(path, { mode: 0o700 });
chmodSync(path, 0o700);
}
}
};
const path = async (root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<string> => {
await ensure(root);
return join(root, directory, filename);
};
const names = async (root: string, directory: WindowsAuthStorageDirectory): Promise<string[]> => {
await ensure(root);
const handle = opendirSync(join(root, directory));
const result: string[] = [];
try {
for (;;) {
const entry = handle.readSync();
if (entry === null) break;
result.push(entry.name);
}
} finally {
handle.closeSync();
}
return result.sort();
};
const missing = (error: unknown): boolean => (error as { code?: unknown })?.code === "ENOENT";
return {
validateRoot: ensure,
ensureLayout: ensure,
readAuthConfig: (value) => readFileSync(value),
readLocalUsers: async (value) => readFileSync(value),
create: async (root, directory, filename, contents) => {
try {
const value = await path(root, directory, filename);
writeFileSync(value, contents, { flag: "wx", mode: 0o600 });
chmodSync(value, 0o600);
return true;
} catch (error) {
if (missing(error) || (error as { code?: unknown })?.code === "EEXIST") return false;
throw error;
}
},
read: async (root, directory, filename) => {
try { return readFileSync(await path(root, directory, filename)); } catch (error) {
if (missing(error)) return undefined;
throw error;
}
},
replace: async (root, directory, filename, contents) => {
const value = await path(root, directory, filename);
const temporary = `${value}.fixture-replacement`;
writeFileSync(temporary, contents, { flag: "wx", mode: 0o600 });
renameSync(temporary, value);
},
remove: async (root, directory, filename) => {
try {
unlinkSync(await path(root, directory, filename));
return true;
} catch (error) {
if (missing(error)) return false;
throw error;
}
},
list: async (root, directory, maximumEntries = 256) => {
const result = await names(root, directory);
if (result.length > maximumEntries) throw new Error("fixture list overflow");
return result.map((name) => ({ name, modifiedUnixMs: lstatSync(join(root, directory, name)).mtimeMs }));
},
listPage: async (root, directory, afterName, maximumEntries) => {
if (directory !== "sessions") throw new Error("fixture directory invalid");
const selected = (await names(root, directory)).filter((name) => afterName === undefined || name > afterName);
return {
entries: selected.slice(0, maximumEntries).map((name) => ({
name, modifiedUnixMs: lstatSync(join(root, directory, name)).mtimeMs,
})),
more: selected.length > maximumEntries,
};
},
claimConsume: async (root, filename) => {
const source = await path(root, "oidc", filename);
const claim = source.replace(/\.json$/, ".claim");
try {
linkSync(source, claim);
} catch (error) {
if (missing(error) || (error as { code?: unknown })?.code === "EEXIST") return undefined;
throw error;
}
const contents = readFileSync(source);
unlinkSync(source);
unlinkSync(claim);
return contents;
},
readClaim: async (root, filename) => {
const source = await path(root, "oidc", filename);
const claim = source.replace(/\.json$/, ".claim");
try {
const left = lstatSync(source);
const right = lstatSync(claim);
if (left.ino !== right.ino || left.dev !== right.dev || left.nlink !== 2 || right.nlink !== 2) return undefined;
return readFileSync(source);
} catch (error) {
if (missing(error)) return undefined;
throw error;
}
},
removeClaim: async (root, filename) => {
const source = await path(root, "oidc", filename);
const claim = source.replace(/\.json$/, ".claim");
try {
const left = lstatSync(source);
const right = lstatSync(claim);
if (left.ino !== right.ino || left.dev !== right.dev || left.nlink !== 2 || right.nlink !== 2) return false;
unlinkSync(source);
unlinkSync(claim);
return true;
} catch (error) {
if (missing(error)) return false;
throw error;
}
},
};
}
+1 -1
View File
@@ -6,7 +6,7 @@ writeFileSync(marker, `started:${process.pid}\n`);
process.on("exit", () => appendFileSync(marker, "exited\n"));
process.on("SIGTERM", () => {
appendFileSync(marker, "terminated\n");
process.exit(0);
if (mode !== "timeout") process.exit(0);
});
if (mode === "stdin") {