Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cloud/src/account/org-api-key-revoke.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const session = (accountId: string) => ({
name: null,
avatarUrl: null,
organizationId: ORG,
adminVerified: true,
sealedSession: "sealed",
refreshedSession: null,
});
Expand Down
36 changes: 32 additions & 4 deletions apps/cloud/src/admin/admin-users-api.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const stubMirror = Layer.succeed(

// Only session authentication is served; membership is read from the mirror,
// so any other WorkOS call fails the test.
const stubWorkOS = (userId: string) =>
const stubWorkOS = (userId: string, adminVerified: boolean) =>
Layer.succeed(
WorkOSClient,
new Proxy({} as WorkOSClientService, {
Expand All @@ -144,6 +144,7 @@ const stubWorkOS = (userId: string) =>
return () =>
Effect.succeed({
userId,
adminVerified,
email: `${userId}@placeholder.test`,
organizationId: null,
});
Expand All @@ -153,25 +154,52 @@ const stubWorkOS = (userId: string) =>
}),
);

const authorizeAs = (userId: string) =>
const authorizeAs = (userId: string, adminVerified = true, authorization?: string) =>
authorizeTenant(
new Request("https://admin.invalid", {
headers: { cookie: "wos-session=sealed", [ORG_SELECTOR_HEADER]: ORG },
headers: {
cookie: "wos-session=sealed",
[ORG_SELECTOR_HEADER]: ORG,
...(authorization === undefined ? {} : { authorization }),
},
}),
).pipe(
Effect.provide(
Layer.mergeAll(stubDirectory, stubApiKeys, stubUsers, stubWorkOS(userId), stubMirror),
Layer.mergeAll(
stubDirectory,
stubApiKeys,
stubUsers,
stubWorkOS(userId, adminVerified),
stubMirror,
),
),
);

describe("authorizeTenant · admin session", () => {
it.effect("a bearer header cannot borrow a verified browser's cross-user access", () =>
Effect.gen(function* () {
for (const authorization of ["Bearer org_key", "Bearer user_key", "Bearer", "invalid"]) {
expect(yield* Effect.flip(authorizeAs("user_admin", true, authorization))).toBeInstanceOf(
AdminUsersForbidden,
);
}
}),
);
it.effect("an active admin resolves the selected org as the tenant", () =>
Effect.gen(function* () {
const tenant = yield* authorizeAs("user_admin");
expect(tenant).toBe(ORG);
}),
);

it.effect("an admin without a second factor is forbidden", () =>
Effect.gen(function* () {
expect(yield* Effect.flip(authorizeAs("user_admin", false))).toBeInstanceOf(
AdminUsersForbidden,
);
}),
);

it.effect("an active plain member is forbidden", () =>
Effect.gen(function* () {
const error = yield* Effect.flip(authorizeAs("user_member"));
Expand Down
2 changes: 1 addition & 1 deletion apps/cloud/src/api/protected-api-key-auth.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const stubDirectory = Layer.succeed(MemberDirectory)({
email: null,
name: null,
avatarUrl: null,
role: "member",
role: "admin",
status: "active" as const,
lastActiveAt: null,
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cloud/src/api/protected-jwt-auth.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const stubDirectory = Layer.succeed(MemberDirectory)({
email: null,
name: null,
avatarUrl: null,
role: "member",
role: "admin",
status: "active" as const,
lastActiveAt: null,
}
Expand Down
122 changes: 122 additions & 0 deletions apps/cloud/src/auth/admin-mfa-proof.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect } from "effect";
import { SignJWT } from "jose";
import { readAdminMfaProof, signAdminMfaProof } from "./admin-mfa-proof";

const secret = "a-test-only-cookie-password-of-32-characters";
const identity = { userId: "user_test", sessionId: "session_test" };
const now = 1_800_000_000_000;
const proof = {
mode: "challenge" as const,
factorId: "factor_test",
challengeId: "challenge_test",
exp: now / 1000 + 900,
};
const signed = signAdminMfaProof(secret, identity, "verified", proof, now);

describe("admin verification cookie", () => {
it.effect("accepts a valid proof for the same user and session", () =>
Effect.gen(function* () {
const token = yield* signed;
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toEqual(proof);
}),
);

it.effect("refuses missing, modified, and unsigned cookies", () =>
Effect.gen(function* () {
const token = yield* signed;
const parts = token.split(".");
const unsigned = `${btoa('{"alg":"none"}')}.${parts[1]}.`;
for (const value of [
undefined,
"",
"bad.cookie",
`${token.slice(0, 50)}x${token.slice(51)}`,
unsigned,
]) {
expect(yield* readAdminMfaProof(secret, identity, "verified", value, now)).toBeNull();
}
}),
);

it.effect("refuses another session, another user, and another signing key", () =>
Effect.gen(function* () {
const token = yield* signed;
for (const other of [
{ ...identity, userId: "other" },
{ ...identity, sessionId: "other" },
]) {
expect(yield* readAdminMfaProof(secret, other, "verified", token, now)).toBeNull();
}
expect(
yield* readAdminMfaProof(`${secret}-rotated`, identity, "verified", token, now),
).toBeNull();
}),
);

it.effect("cannot promote an unfinished challenge to verified access", () =>
Effect.gen(function* () {
const token = yield* signAdminMfaProof(
secret,
identity,
"challenge",
{ ...proof, mode: "enroll", exp: now / 1000 + 300 },
now,
);
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
expect(
yield* readAdminMfaProof(secret, identity, "challenge", token, now + 299_000),
).not.toBeNull();
expect(
yield* readAdminMfaProof(secret, identity, "challenge", token, now + 300_000),
).toBeNull();
}),
);

it.effect("expires at fifteen minutes and refuses a future-issued cookie", () =>
Effect.gen(function* () {
const token = yield* signed;
expect(
yield* readAdminMfaProof(secret, identity, "verified", token, now + 899_000),
).not.toBeNull();
expect(
yield* readAdminMfaProof(secret, identity, "verified", token, now + 900_000),
).toBeNull();
expect(
yield* readAdminMfaProof(secret, identity, "verified", token, now - 10_000),
).toBeNull();
}),
);

it.effect("caps token age even when the supplied expiration is longer", () =>
Effect.gen(function* () {
const token = yield* signAdminMfaProof(
secret,
identity,
"verified",
{ ...proof, exp: now / 1000 + 86400 },
now,
);
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
expect(
yield* readAdminMfaProof(secret, identity, "verified", token, now + 901_000),
).toBeNull();
}),
);

it.effect("rejects a signed cookie with missing issued-at or another algorithm", () =>
Effect.gen(function* () {
for (const algorithm of ["HS256", "HS384"]) {
const jwt = new SignJWT({ ...proof })
.setProtectedHeader({ alg: algorithm })
.setIssuer("executor:admin-mfa:verified")
.setSubject(identity.userId)
.setAudience(identity.sessionId);
// HS256 lacks iat; HS384 is otherwise valid but outside the allowlist.
if (algorithm === "HS384") jwt.setIssuedAt(now / 1000);
const token = yield* Effect.promise(() => jwt.sign(new TextEncoder().encode(secret)));
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
}
}),
);
});
3 changes: 3 additions & 0 deletions apps/cloud/src/auth/mirror-feeders.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ describe("login callback", () => {
organizationId: undefined,
accessToken: "access",
refreshToken: "refresh",
adminVerified: true,
sealedSession: "sealed",
}),
listUserMemberships: (id) => {
Expand Down Expand Up @@ -593,6 +594,7 @@ describe("session handlers read membership from the mirror", () => {
...options.workos,
authenticateSealedSession: () =>
Effect.succeed({
adminVerified: true,
userId,
email: `${userId}@placeholder.test`,
organizationId: null,
Expand Down Expand Up @@ -1099,6 +1101,7 @@ describe("account service writes through to the mirror", () => {
name: null,
avatarUrl: null,
organizationId: null,
adminVerified: true,
sealedSession: "sealed",
refreshedSession: null,
});
Expand Down
40 changes: 40 additions & 0 deletions apps/cloud/src/auth/oauth-admin-verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "@effect/vitest";
import { encodeOAuthCallbackState } from "@executor-js/sdk/shared";
import { oauthAdminVerificationResponse } from "./oauth-admin-verification";

describe("OAuth admin verification recovery", () => {
it("keeps provider credentials out of the recovery page and preserves session cookies", async () => {
const state = encodeOAuthCallbackState({ state: "private-state", orgSlug: "example-org" });
const request = new Request(
`https://app.example/api/oauth/callback?code=private-code&state=${state}`,
);
const denied = Response.json(
{ code: "admin_mfa_required" },
{
status: 403,
headers: {
"set-cookie": "wos-session=rotated; Secure; HttpOnly",
"x-executor-error": "org_write_denied",
},
},
);
const response = await oauthAdminVerificationResponse(request, denied);
expect(response.status).toBe(200);
expect(response.headers.get("set-cookie")).toContain("wos-session=rotated");
expect(response.headers.get("cache-control")).toBe("no-store");
expect(response.headers.get("referrer-policy")).toBe("no-referrer");
expect(response.headers.get("content-security-policy")).toContain("default-src 'none'");
const body = await response.text();
expect(body).toContain('href="/example-org/org"');
expect(body).toContain("Continue connection");
expect(body).not.toContain("private-code");
expect(body).not.toContain(state);
expect(body).not.toContain("<script");
});

it("keeps other authorization failures intact", async () => {
const request = new Request("https://app.example/api/oauth/callback?state=invalid");
const denied = Response.json({ code: "no_organization" }, { status: 403 });
expect(await oauthAdminVerificationResponse(request, denied)).toBe(denied);
});
});
1 change: 1 addition & 0 deletions apps/cloud/src/auth/org-selector-auth.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const stubWorkOS = Layer.succeed(
userId: MEMBER,
email: "u@e2e.test",
organizationId: SESSION_ORG,
adminVerified: true,
});
}
// Membership is read from the mirror, never from WorkOS: any WorkOS
Expand Down
49 changes: 47 additions & 2 deletions apps/cloud/src/auth/workos.node.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import type { AddressInfo } from "node:net";

import { signAdminMfaProof } from "./admin-mfa-proof";
import { describe, expect, it } from "@effect/vitest";
import { env } from "cloudflare:workers";
import { Effect, Schema } from "effect";
Expand Down Expand Up @@ -174,7 +175,7 @@ const withWorkOSStub = async <A>(
});
};

const runAuthenticate = (sessionData: string, baseUrl: string) => {
const runAuthenticate = (sessionData: string, baseUrl: string, proof?: string) => {
Object.assign(env, {
WORKOS_API_KEY: API_KEY,
WORKOS_CLIENT_ID: CLIENT_ID,
Expand All @@ -185,12 +186,54 @@ const runAuthenticate = (sessionData: string, baseUrl: string) => {
return Effect.runPromise(
Effect.gen(function* () {
const workos = yield* WorkOSClient;
return yield* workos.authenticateSealedSession(sessionData);
return yield* workos.authenticateSealedSession(sessionData, proof);
}).pipe(Effect.provide(WorkOSClient.Default)),
);
};

describe("authenticateSealedSession", () => {
it("binds admin verification to the authenticated session and refuses expired proofs", async () => {
const keypair = await generateKeypair("k_admin_mfa");
await withWorkOSStub(keypair, async (stub) => {
const session = await sealSession(
await signAccessToken(keypair, { sessionId: "session_admin" }),
);
const now = Date.now();
const makeProof = (sessionId: string, timestamp: number) =>
Effect.runPromise(
signAdminMfaProof(
COOKIE_PASSWORD,
{ userId: USER.id, sessionId },
"verified",
{
factorId: "factor_test",
challengeId: "challenge_test",
mode: "challenge",
exp: Math.floor(timestamp / 1000) + 900,
},
timestamp,
),
);
expect(
(await runAuthenticate(session, stub.baseUrl, await makeProof("session_admin", now)))
?.adminVerified,
).toBe(true);
expect(
(await runAuthenticate(session, stub.baseUrl, await makeProof("session_other", now)))
?.adminVerified,
).toBe(false);
expect(
(
await runAuthenticate(
session,
stub.baseUrl,
await makeProof("session_admin", now - 901_000),
)
)?.adminVerified,
).toBe(false);
});
});

it("validates a sealed session locally with the cached JWKS", async () => {
const keypair = await generateKeypair("k_valid");
await withWorkOSStub(keypair, async (stub) => {
Expand All @@ -211,6 +254,8 @@ describe("authenticateSealedSession", () => {
organizationId: "org_test",
sessionId: "session_valid",
refreshedSession: undefined,
adminVerified: false,
adminVerificationExpiresAt: null,
});
expect(stub.requests()).toEqual([
{ method: "GET", path: `/sso/jwks/${CLIENT_ID}`, body: null },
Expand Down
4 changes: 3 additions & 1 deletion apps/cloud/src/mcp/auth-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ describe("cloud MCP org-authorization classification", () => {
expect(principal?.accountId).toBe(ACCOUNT_ID);
expect(principal?.organizationId).toBe(ORG_ID);
expect(principal?.orgRoleModel).toBe("organization");
expect(principal?.orgRole, "the live membership role reaches the MCP session").toBe("admin");
expect(principal?.orgRole, "machine tokens never grant workspace administration").toBe(
"member",
);
const legacyAccess = principal
? orgWriteAccessForPrincipal(
(({ orgRole: _orgRole, ...legacyMissingRole }) => legacyMissingRole)(principal),
Expand Down
Loading
Loading