feat(auth): add generic OIDC login with mandatory groups
This commit is contained in:
@@ -69,6 +69,10 @@ 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" };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
fsHooks.afterRead = undefined;
|
||||
fsHooks.afterWrite = undefined;
|
||||
@@ -258,7 +262,7 @@ describe("file-backed auth session store", () => {
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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();
|
||||
@@ -272,20 +276,19 @@ describe("file-backed auth session store", () => {
|
||||
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({
|
||||
nonce: "n".repeat(43),
|
||||
codeVerifier: "v".repeat(43),
|
||||
returnTo: "/",
|
||||
}, base);
|
||||
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: "/" });
|
||||
.resolves.toMatchObject({
|
||||
nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/",
|
||||
authConfigRevision: revision, issuer: "https://issuer.example.test",
|
||||
});
|
||||
await expect(store.consumeOidcState(created.state)).resolves.toBeUndefined();
|
||||
|
||||
const expired = await store.createOidcState({ nonce: "x".repeat(43), codeVerifier: "y".repeat(43), returnTo: "/" }, base);
|
||||
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();
|
||||
});
|
||||
@@ -293,7 +296,7 @@ describe("file-backed auth session store", () => {
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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));
|
||||
|
||||
@@ -304,7 +307,7 @@ describe("file-backed auth session store", () => {
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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) => {
|
||||
@@ -321,7 +324,7 @@ describe("file-backed auth session store", () => {
|
||||
test("prunes an expired OIDC state abandoned after an atomic claim", async () => {
|
||||
const storageRoot = root();
|
||||
const store = validStore(storageRoot);
|
||||
const created = await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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);
|
||||
@@ -334,7 +337,7 @@ describe("file-backed auth session store", () => {
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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);
|
||||
@@ -349,7 +352,7 @@ describe("file-backed auth session store", () => {
|
||||
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({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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),
|
||||
@@ -587,12 +590,12 @@ describe("file-backed auth session store", () => {
|
||||
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);
|
||||
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({ nonce: "x".repeat(43), codeVerifier: "y".repeat(43), returnTo: "/" }, base);
|
||||
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);
|
||||
@@ -651,7 +654,7 @@ describe("file-backed auth session store", () => {
|
||||
findLocalUser: async () => validLocalUser,
|
||||
}, { windowsStorageBridge: bridge });
|
||||
await create(store, { idleTtlMs: 60_000, absoluteTtlMs: 60_000 });
|
||||
await store.createOidcState({ nonce: "n".repeat(43), codeVerifier: "v".repeat(43), returnTo: "/" }, base);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user