fix(auth): add Windows session storage bridge
This commit is contained in:
@@ -488,20 +488,97 @@ describe("file-backed auth session store", () => {
|
||||
await expectStoreInvalid(store.revoke(created.token));
|
||||
});
|
||||
|
||||
test("rejects native Windows storage before any state write", async () => {
|
||||
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 {
|
||||
await expectStoreInvalid(create(store));
|
||||
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() }));
|
||||
},
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, 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({ nonce: "x".repeat(43), codeVerifier: "y".repeat(43), returnTo: "/" }, 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("revokes on config, local-user, revision, enabled, or role mismatch before returning", async () => {
|
||||
const storageRoot = root();
|
||||
let currentRevision = revision;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { createWindowsAuthStorageBridge } from "../src/auth/windows-auth-storage.js";
|
||||
|
||||
const root = "C:\\ProgramData\\ThothII\\auth";
|
||||
const filename = "a".repeat(64) + ".json";
|
||||
|
||||
describe("Windows auth-storage bridge", () => {
|
||||
test("uses hidden tht argv and sends record bytes only over bounded stdin", async () => {
|
||||
const calls: Array<{ executable: string; args: readonly string[]; input: Buffer; timeoutMs: number }> = [];
|
||||
const bridge = createWindowsAuthStorageBridge({
|
||||
thtExecutable: "C:\\Program Files\\ThothII\\tht.exe",
|
||||
invoke: async (call) => {
|
||||
calls.push(call);
|
||||
return { code: 0, stdout: Buffer.from('{"version":1,"ok":true,"created":true}\n'), stderr: Buffer.alloc(0) };
|
||||
},
|
||||
});
|
||||
|
||||
await expect(bridge.create(root, "sessions", filename, Buffer.from('{"subject":"record-data"}'))).resolves.toBe(true);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]).toMatchObject({
|
||||
executable: "C:\\Program Files\\ThothII\\tht.exe",
|
||||
args: ["_auth-storage"],
|
||||
});
|
||||
expect(JSON.stringify(calls[0].args)).not.toContain("record-data");
|
||||
expect(JSON.parse(calls[0].input.toString("utf8"))).toMatchObject({
|
||||
version: 1,
|
||||
operation: "create",
|
||||
root,
|
||||
directory: "sessions",
|
||||
filename,
|
||||
contentBase64: Buffer.from('{"subject":"record-data"}').toString("base64"),
|
||||
});
|
||||
expect(calls[0].timeoutMs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ label: "nonzero", result: { code: 1, stdout: Buffer.from('{"version":1,"ok":true}\n'), stderr: Buffer.from("secret") } },
|
||||
{ label: "malformed stdout", result: { code: 0, stdout: Buffer.from("not-json"), stderr: Buffer.alloc(0) } },
|
||||
{ label: "unexpected stdout", result: { code: 0, stdout: Buffer.from('{"version":1,"ok":true}\nextra'), stderr: Buffer.alloc(0) } },
|
||||
{ label: "unexpected JSON field", result: { code: 0, stdout: Buffer.from('{"version":1,"ok":true,"created":true,"detail":"secret"}\n'), stderr: Buffer.alloc(0) } },
|
||||
])("fails closed on $label bridge output", async ({ result }) => {
|
||||
const bridge = createWindowsAuthStorageBridge({
|
||||
thtExecutable: "C:\\tht.exe",
|
||||
invoke: async () => result,
|
||||
});
|
||||
|
||||
await expect(bridge.create(root, "sessions", filename, Buffer.from("record")))
|
||||
.rejects.toThrow("auth_session_store_invalid");
|
||||
});
|
||||
|
||||
test("fails closed on a bridge timeout without disclosing request content", async () => {
|
||||
const bridge = createWindowsAuthStorageBridge({
|
||||
thtExecutable: "C:\\tht.exe",
|
||||
invoke: async () => { throw new Error("timeout secret-record"); },
|
||||
});
|
||||
|
||||
await expect(bridge.create(root, "sessions", filename, Buffer.from("secret-record")))
|
||||
.rejects.toThrow("auth_session_store_invalid");
|
||||
});
|
||||
|
||||
test("rejects a claimed-read response without bounded record bytes", async () => {
|
||||
const bridge = createWindowsAuthStorageBridge({
|
||||
thtExecutable: "C:\\tht.exe",
|
||||
invoke: async () => ({ code: 0, stdout: Buffer.from('{"version":1,"ok":true,"found":true}\n'), stderr: Buffer.alloc(0) }),
|
||||
});
|
||||
|
||||
await expect(bridge.readClaim(root, filename)).rejects.toThrow("auth_session_store_invalid");
|
||||
});
|
||||
|
||||
test("rejects an executable value that would require shell parsing", () => {
|
||||
expect(() => createWindowsAuthStorageBridge({ thtExecutable: "tht.exe && unexpected" }))
|
||||
.toThrow("auth_session_store_invalid");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user