Skip to content

Commit fec546e

Browse files
authored
Test cloud admin verification and ordinary access (#2110)
1 parent d0ca1b6 commit fec546e

31 files changed

Lines changed: 1073 additions & 159 deletions

‎apps/cloud/src/account/org-api-key-revoke.node.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const session = (accountId: string) => ({
5959
name: null,
6060
avatarUrl: null,
6161
organizationId: ORG,
62+
adminVerified: true,
6263
sealedSession: "sealed",
6364
refreshedSession: null,
6465
});

‎apps/cloud/src/admin/admin-users-api.node.test.ts‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const stubMirror = Layer.succeed(
135135

136136
// Only session authentication is served; membership is read from the mirror,
137137
// so any other WorkOS call fails the test.
138-
const stubWorkOS = (userId: string) =>
138+
const stubWorkOS = (userId: string, adminVerified: boolean) =>
139139
Layer.succeed(
140140
WorkOSClient,
141141
new Proxy({} as WorkOSClientService, {
@@ -144,6 +144,7 @@ const stubWorkOS = (userId: string) =>
144144
return () =>
145145
Effect.succeed({
146146
userId,
147+
adminVerified,
147148
email: `${userId}@placeholder.test`,
148149
organizationId: null,
149150
});
@@ -153,25 +154,52 @@ const stubWorkOS = (userId: string) =>
153154
}),
154155
);
155156

156-
const authorizeAs = (userId: string) =>
157+
const authorizeAs = (userId: string, adminVerified = true, authorization?: string) =>
157158
authorizeTenant(
158159
new Request("https://admin.invalid", {
159-
headers: { cookie: "wos-session=sealed", [ORG_SELECTOR_HEADER]: ORG },
160+
headers: {
161+
cookie: "wos-session=sealed",
162+
[ORG_SELECTOR_HEADER]: ORG,
163+
...(authorization === undefined ? {} : { authorization }),
164+
},
160165
}),
161166
).pipe(
162167
Effect.provide(
163-
Layer.mergeAll(stubDirectory, stubApiKeys, stubUsers, stubWorkOS(userId), stubMirror),
168+
Layer.mergeAll(
169+
stubDirectory,
170+
stubApiKeys,
171+
stubUsers,
172+
stubWorkOS(userId, adminVerified),
173+
stubMirror,
174+
),
164175
),
165176
);
166177

167178
describe("authorizeTenant · admin session", () => {
179+
it.effect("a bearer header cannot borrow a verified browser's cross-user access", () =>
180+
Effect.gen(function* () {
181+
for (const authorization of ["Bearer org_key", "Bearer user_key", "Bearer", "invalid"]) {
182+
expect(yield* Effect.flip(authorizeAs("user_admin", true, authorization))).toBeInstanceOf(
183+
AdminUsersForbidden,
184+
);
185+
}
186+
}),
187+
);
168188
it.effect("an active admin resolves the selected org as the tenant", () =>
169189
Effect.gen(function* () {
170190
const tenant = yield* authorizeAs("user_admin");
171191
expect(tenant).toBe(ORG);
172192
}),
173193
);
174194

195+
it.effect("an admin without a second factor is forbidden", () =>
196+
Effect.gen(function* () {
197+
expect(yield* Effect.flip(authorizeAs("user_admin", false))).toBeInstanceOf(
198+
AdminUsersForbidden,
199+
);
200+
}),
201+
);
202+
175203
it.effect("an active plain member is forbidden", () =>
176204
Effect.gen(function* () {
177205
const error = yield* Effect.flip(authorizeAs("user_member"));

‎apps/cloud/src/api/protected-api-key-auth.node.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const stubDirectory = Layer.succeed(MemberDirectory)({
6767
email: null,
6868
name: null,
6969
avatarUrl: null,
70-
role: "member",
70+
role: "admin",
7171
status: "active" as const,
7272
lastActiveAt: null,
7373
}

‎apps/cloud/src/api/protected-jwt-auth.node.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const stubDirectory = Layer.succeed(MemberDirectory)({
8484
email: null,
8585
name: null,
8686
avatarUrl: null,
87-
role: "member",
87+
role: "admin",
8888
status: "active" as const,
8989
lastActiveAt: null,
9090
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { describe, expect, it } from "@effect/vitest";
2+
import { Effect } from "effect";
3+
import { SignJWT } from "jose";
4+
import { readAdminMfaProof, signAdminMfaProof } from "./admin-mfa-proof";
5+
6+
const secret = "a-test-only-cookie-password-of-32-characters";
7+
const identity = { userId: "user_test", sessionId: "session_test" };
8+
const now = 1_800_000_000_000;
9+
const proof = {
10+
mode: "challenge" as const,
11+
factorId: "factor_test",
12+
challengeId: "challenge_test",
13+
exp: now / 1000 + 900,
14+
};
15+
const signed = signAdminMfaProof(secret, identity, "verified", proof, now);
16+
17+
describe("admin verification cookie", () => {
18+
it.effect("accepts a valid proof for the same user and session", () =>
19+
Effect.gen(function* () {
20+
const token = yield* signed;
21+
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toEqual(proof);
22+
}),
23+
);
24+
25+
it.effect("refuses missing, modified, and unsigned cookies", () =>
26+
Effect.gen(function* () {
27+
const token = yield* signed;
28+
const parts = token.split(".");
29+
const unsigned = `${btoa('{"alg":"none"}')}.${parts[1]}.`;
30+
for (const value of [
31+
undefined,
32+
"",
33+
"bad.cookie",
34+
`${token.slice(0, 50)}x${token.slice(51)}`,
35+
unsigned,
36+
]) {
37+
expect(yield* readAdminMfaProof(secret, identity, "verified", value, now)).toBeNull();
38+
}
39+
}),
40+
);
41+
42+
it.effect("refuses another session, another user, and another signing key", () =>
43+
Effect.gen(function* () {
44+
const token = yield* signed;
45+
for (const other of [
46+
{ ...identity, userId: "other" },
47+
{ ...identity, sessionId: "other" },
48+
]) {
49+
expect(yield* readAdminMfaProof(secret, other, "verified", token, now)).toBeNull();
50+
}
51+
expect(
52+
yield* readAdminMfaProof(`${secret}-rotated`, identity, "verified", token, now),
53+
).toBeNull();
54+
}),
55+
);
56+
57+
it.effect("cannot promote an unfinished challenge to verified access", () =>
58+
Effect.gen(function* () {
59+
const token = yield* signAdminMfaProof(
60+
secret,
61+
identity,
62+
"challenge",
63+
{ ...proof, mode: "enroll", exp: now / 1000 + 300 },
64+
now,
65+
);
66+
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
67+
expect(
68+
yield* readAdminMfaProof(secret, identity, "challenge", token, now + 299_000),
69+
).not.toBeNull();
70+
expect(
71+
yield* readAdminMfaProof(secret, identity, "challenge", token, now + 300_000),
72+
).toBeNull();
73+
}),
74+
);
75+
76+
it.effect("expires at fifteen minutes and refuses a future-issued cookie", () =>
77+
Effect.gen(function* () {
78+
const token = yield* signed;
79+
expect(
80+
yield* readAdminMfaProof(secret, identity, "verified", token, now + 899_000),
81+
).not.toBeNull();
82+
expect(
83+
yield* readAdminMfaProof(secret, identity, "verified", token, now + 900_000),
84+
).toBeNull();
85+
expect(
86+
yield* readAdminMfaProof(secret, identity, "verified", token, now - 10_000),
87+
).toBeNull();
88+
}),
89+
);
90+
91+
it.effect("caps token age even when the supplied expiration is longer", () =>
92+
Effect.gen(function* () {
93+
const token = yield* signAdminMfaProof(
94+
secret,
95+
identity,
96+
"verified",
97+
{ ...proof, exp: now / 1000 + 86400 },
98+
now,
99+
);
100+
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
101+
expect(
102+
yield* readAdminMfaProof(secret, identity, "verified", token, now + 901_000),
103+
).toBeNull();
104+
}),
105+
);
106+
107+
it.effect("rejects a signed cookie with missing issued-at or another algorithm", () =>
108+
Effect.gen(function* () {
109+
for (const algorithm of ["HS256", "HS384"]) {
110+
const jwt = new SignJWT({ ...proof })
111+
.setProtectedHeader({ alg: algorithm })
112+
.setIssuer("executor:admin-mfa:verified")
113+
.setSubject(identity.userId)
114+
.setAudience(identity.sessionId);
115+
// HS256 lacks iat; HS384 is otherwise valid but outside the allowlist.
116+
if (algorithm === "HS384") jwt.setIssuedAt(now / 1000);
117+
const token = yield* Effect.promise(() => jwt.sign(new TextEncoder().encode(secret)));
118+
expect(yield* readAdminMfaProof(secret, identity, "verified", token, now)).toBeNull();
119+
}
120+
}),
121+
);
122+
});

‎apps/cloud/src/auth/mirror-feeders.node.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ describe("login callback", () => {
228228
organizationId: undefined,
229229
accessToken: "access",
230230
refreshToken: "refresh",
231+
adminVerified: true,
231232
sealedSession: "sealed",
232233
}),
233234
listUserMemberships: (id) => {
@@ -593,6 +594,7 @@ describe("session handlers read membership from the mirror", () => {
593594
...options.workos,
594595
authenticateSealedSession: () =>
595596
Effect.succeed({
597+
adminVerified: true,
596598
userId,
597599
email: `${userId}@placeholder.test`,
598600
organizationId: null,
@@ -1099,6 +1101,7 @@ describe("account service writes through to the mirror", () => {
10991101
name: null,
11001102
avatarUrl: null,
11011103
organizationId: null,
1104+
adminVerified: true,
11021105
sealedSession: "sealed",
11031106
refreshedSession: null,
11041107
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from "@effect/vitest";
2+
import { encodeOAuthCallbackState } from "@executor-js/sdk/shared";
3+
import { oauthAdminVerificationResponse } from "./oauth-admin-verification";
4+
5+
describe("OAuth admin verification recovery", () => {
6+
it("keeps provider credentials out of the recovery page and preserves session cookies", async () => {
7+
const state = encodeOAuthCallbackState({ state: "private-state", orgSlug: "example-org" });
8+
const request = new Request(
9+
`https://app.example/api/oauth/callback?code=private-code&state=${state}`,
10+
);
11+
const denied = Response.json(
12+
{ code: "admin_mfa_required" },
13+
{
14+
status: 403,
15+
headers: {
16+
"set-cookie": "wos-session=rotated; Secure; HttpOnly",
17+
"x-executor-error": "org_write_denied",
18+
},
19+
},
20+
);
21+
const response = await oauthAdminVerificationResponse(request, denied);
22+
expect(response.status).toBe(200);
23+
expect(response.headers.get("set-cookie")).toContain("wos-session=rotated");
24+
expect(response.headers.get("cache-control")).toBe("no-store");
25+
expect(response.headers.get("referrer-policy")).toBe("no-referrer");
26+
expect(response.headers.get("content-security-policy")).toContain("default-src 'none'");
27+
const body = await response.text();
28+
expect(body).toContain('href="/example-org/org"');
29+
expect(body).toContain("Continue connection");
30+
expect(body).not.toContain("private-code");
31+
expect(body).not.toContain(state);
32+
expect(body).not.toContain("<script");
33+
});
34+
35+
it("keeps other authorization failures intact", async () => {
36+
const request = new Request("https://app.example/api/oauth/callback?state=invalid");
37+
const denied = Response.json({ code: "no_organization" }, { status: 403 });
38+
expect(await oauthAdminVerificationResponse(request, denied)).toBe(denied);
39+
});
40+
});

‎apps/cloud/src/auth/org-selector-auth.node.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ const stubWorkOS = Layer.succeed(
9999
userId: MEMBER,
100100
email: "u@e2e.test",
101101
organizationId: SESSION_ORG,
102+
adminVerified: true,
102103
});
103104
}
104105
// Membership is read from the mirror, never from WorkOS: any WorkOS

‎apps/cloud/src/auth/workos.node.test.ts‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
22
import type { AddressInfo } from "node:net";
33

4+
import { signAdminMfaProof } from "./admin-mfa-proof";
45
import { describe, expect, it } from "@effect/vitest";
56
import { env } from "cloudflare:workers";
67
import { Effect, Schema } from "effect";
@@ -174,7 +175,7 @@ const withWorkOSStub = async <A>(
174175
});
175176
};
176177

177-
const runAuthenticate = (sessionData: string, baseUrl: string) => {
178+
const runAuthenticate = (sessionData: string, baseUrl: string, proof?: string) => {
178179
Object.assign(env, {
179180
WORKOS_API_KEY: API_KEY,
180181
WORKOS_CLIENT_ID: CLIENT_ID,
@@ -185,12 +186,54 @@ const runAuthenticate = (sessionData: string, baseUrl: string) => {
185186
return Effect.runPromise(
186187
Effect.gen(function* () {
187188
const workos = yield* WorkOSClient;
188-
return yield* workos.authenticateSealedSession(sessionData);
189+
return yield* workos.authenticateSealedSession(sessionData, proof);
189190
}).pipe(Effect.provide(WorkOSClient.Default)),
190191
);
191192
};
192193

193194
describe("authenticateSealedSession", () => {
195+
it("binds admin verification to the authenticated session and refuses expired proofs", async () => {
196+
const keypair = await generateKeypair("k_admin_mfa");
197+
await withWorkOSStub(keypair, async (stub) => {
198+
const session = await sealSession(
199+
await signAccessToken(keypair, { sessionId: "session_admin" }),
200+
);
201+
const now = Date.now();
202+
const makeProof = (sessionId: string, timestamp: number) =>
203+
Effect.runPromise(
204+
signAdminMfaProof(
205+
COOKIE_PASSWORD,
206+
{ userId: USER.id, sessionId },
207+
"verified",
208+
{
209+
factorId: "factor_test",
210+
challengeId: "challenge_test",
211+
mode: "challenge",
212+
exp: Math.floor(timestamp / 1000) + 900,
213+
},
214+
timestamp,
215+
),
216+
);
217+
expect(
218+
(await runAuthenticate(session, stub.baseUrl, await makeProof("session_admin", now)))
219+
?.adminVerified,
220+
).toBe(true);
221+
expect(
222+
(await runAuthenticate(session, stub.baseUrl, await makeProof("session_other", now)))
223+
?.adminVerified,
224+
).toBe(false);
225+
expect(
226+
(
227+
await runAuthenticate(
228+
session,
229+
stub.baseUrl,
230+
await makeProof("session_admin", now - 901_000),
231+
)
232+
)?.adminVerified,
233+
).toBe(false);
234+
});
235+
});
236+
194237
it("validates a sealed session locally with the cached JWKS", async () => {
195238
const keypair = await generateKeypair("k_valid");
196239
await withWorkOSStub(keypair, async (stub) => {
@@ -211,6 +254,8 @@ describe("authenticateSealedSession", () => {
211254
organizationId: "org_test",
212255
sessionId: "session_valid",
213256
refreshedSession: undefined,
257+
adminVerified: false,
258+
adminVerificationExpiresAt: null,
214259
});
215260
expect(stub.requests()).toEqual([
216261
{ method: "GET", path: `/sso/jwks/${CLIENT_ID}`, body: null },

‎apps/cloud/src/mcp/auth-provider.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ describe("cloud MCP org-authorization classification", () => {
165165
expect(principal?.accountId).toBe(ACCOUNT_ID);
166166
expect(principal?.organizationId).toBe(ORG_ID);
167167
expect(principal?.orgRoleModel).toBe("organization");
168-
expect(principal?.orgRole, "the live membership role reaches the MCP session").toBe("admin");
168+
expect(principal?.orgRole, "machine tokens never grant workspace administration").toBe(
169+
"member",
170+
);
169171
const legacyAccess = principal
170172
? orgWriteAccessForPrincipal(
171173
(({ orgRole: _orgRole, ...legacyMissingRole }) => legacyMissingRole)(principal),

0 commit comments

Comments
 (0)