Skip to content
Draft
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
33 changes: 33 additions & 0 deletions apps/cloud/src/auth/access-token-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "@effect/vitest";
import { SignJWT, jwtVerify } from "jose";
import { workosAccessTokenOptions } from "./access-token-options";

describe("WorkOS token age boundary", () => {
it("accepts a fresh token, rejects it after 24 hours, and rejects future issuance", async () => {
const key = new TextEncoder().encode("synthetic-signing-key-for-unit-test-only");
const issuedAt = 1_700_000_000;
const token = await new SignJWT({})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(issuedAt)
.setExpirationTime(issuedAt + 7 * 86400)
.sign(key);
await expect(
jwtVerify(token, key, {
...workosAccessTokenOptions,
currentDate: new Date((issuedAt + 86399) * 1000),
}),
).resolves.toHaveProperty("payload.iat", issuedAt);
await expect(
jwtVerify(token, key, {
...workosAccessTokenOptions,
currentDate: new Date((issuedAt + 86401) * 1000),
}),
).rejects.toHaveProperty("code", "ERR_JWT_EXPIRED");
await expect(
jwtVerify(token, key, {
...workosAccessTokenOptions,
currentDate: new Date((issuedAt - 1) * 1000),
}),
).rejects.toHaveProperty("code", "ERR_JWT_CLAIM_VALIDATION_FAILED");
});
});
18 changes: 17 additions & 1 deletion apps/cloud/src/auth/workos.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const signAccessToken = (
readonly organizationId?: string;
readonly sessionId?: string;
readonly expiresIn?: string | number;
readonly issuedAt?: number;
} = {},
) => {
const jwt = new SignJWT({
Expand All @@ -67,7 +68,7 @@ const signAccessToken = (
})
.setProtectedHeader({ alg: "RS256", kid: keypair.kid })
.setSubject(claims.subject ?? USER.id)
.setIssuedAt();
.setIssuedAt(claims.issuedAt);

return (
typeof claims.expiresIn === "number"
Expand Down Expand Up @@ -317,6 +318,21 @@ describe("authenticateSealedSession", () => {
});
});

it("refreshes a token beyond the local age limit even when WorkOS exp is later", async () => {
const keypair = await generateKeypair("k_old");
await withWorkOSStub(keypair, async (stub) => {
const now = Math.floor(Date.now() / 1000);
const token = await signAccessToken(keypair, {
issuedAt: now - 86401,
expiresIn: now + 86400,
});
const result = await runAuthenticate(await sealSession(token), stub.baseUrl);
expect(result?.sessionId).toBe("session_refreshed");
expect(result?.refreshedSession).toEqual(expect.any(String));
expect(stub.requests()[1]?.body).toMatchObject({ grant_type: "refresh_token" });
});
});

it("returns null for garbage session data", async () => {
const keypair = await generateKeypair("k_garbage");
await withWorkOSStub(keypair, async (stub) => {
Expand Down
13 changes: 8 additions & 5 deletions apps/cloud/src/mcp/mcp-auth.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe("access token expiry and identity boundaries", () => {
}),
);
}
it.effect(`${kind} accepts a token issued more than a day ago that has not expired`, () =>
it.effect(`${kind} rejects a token issued more than a day ago even when exp is later`, () =>
Effect.gen(function* () {
const { publicKey, privateKey } = yield* Effect.promise(() => generateKeyPair("RS256"));
const jwk = yield* Effect.promise(() => exportJWK(publicKey));
Expand All @@ -169,10 +169,13 @@ describe("access token expiry and identity boundaries", () => {
.setProtectedHeader({ alg: "RS256", kid: "expiry-key" })
.sign(privateKey),
);
const verified = yield* kind === "mcp"
? verifyMcpAccessToken(token, jwks, { issuer, audience: resource })
: verifyWorkosUserManagementToken(token, jwks);
expect(verified).toEqual({ accountId: "user_test", organizationId: "org_test" });
const error = yield* Effect.flip(
kind === "mcp"
? verifyMcpAccessToken(token, jwks, { issuer, audience: resource })
: verifyWorkosUserManagementToken(token, jwks),
);
expect(error).toBeInstanceOf(McpJwtVerificationError);
expect(error.reason).toBe("expired");
}),
);
it.effect(`${kind} rejects a non-string organization claim`, () =>
Expand Down
48 changes: 48 additions & 0 deletions apps/cloud/src/observability/observability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,3 +409,51 @@ describe("Durable Object platform reset noise", () => {
expect(options.beforeSend(event)).toBeNull();
});
});

describe("Sentry privacy boundary", () => {
it("strips secrets from auto-captured errors while retaining diagnostic locations", () => {
const secret = "SYNTHETIC_PRIVATE_MARKER";
const sent = cloudSentryOptions({
SENTRY_DSN: "https://public@example.invalid/1",
} as Env).beforeSend({
type: undefined,
event_id: "safe-event-id",
message: secret,
user: { email: secret },
request: {
url: `https://example.test/?token=${secret}`,
headers: { authorization: secret },
data: secret,
},
extra: { cause: secret },
breadcrumbs: [{ message: secret }],
tags: { token: secret, otel_trace_id: traceId },
exception: {
values: [
{
type: "TypeError",
value: secret,
stacktrace: {
frames: [
{
filename: `/assets/example.js?token=${secret}`,
function: "handleRequest",
lineno: 42,
vars: { secret },
},
],
},
},
],
},
});
expect(JSON.stringify(sent)).not.toContain(secret);
expect(sent?.event_id).toBe("safe-event-id");
expect(sent?.tags?.otel_trace_id).toBe(traceId);
expect(sent?.exception?.values?.[0]?.stacktrace?.frames?.[0]).toMatchObject({
filename: "/assets/example.js",
function: "handleRequest",
lineno: 42,
});
});
});
25 changes: 22 additions & 3 deletions apps/cloud/src/observability/redact-span-urls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,13 @@ describe("UrlRedactingSpanProcessor", () => {
span.setStatus({ code: SpanStatusCode.ERROR, message });
});

// Non-vacuous: the exception event exists and kept its scrubbed URL.
// The exception event and classification survive without raw error text.
const events = JSON.stringify(exported?.events);
expect(events).toContain("exception");
expect(events).toContain("https://api.test/graphql");
expect(events).toContain("[REDACTED]");
expect(events).not.toContain("synthetic-userinfo-secret");
expect(events).not.toContain("synthetic-key-secret");
expect(exported?.status.message).toBe("Transport: fetch failed (GET https://api.test/graphql)");
expect(exported?.status.message).toBe("[REDACTED]");
});
});

Expand Down Expand Up @@ -284,3 +284,22 @@ describe("credential canary — no export channel carries the secret", () => {
},
);
});

describe("non-URL secrets in exceptions", () => {
it("does not export a provider response or SQL values as error text", () => {
const secret = "synthetic-plain-secret";
const exported = exportSpanWith({ "http.response.status_code": "500" }, (span) => {
span.recordException({
name: "ProviderError",
message: `Failed query values: ${secret}`,
stack: `at provider: ${secret}`,
});
span.setStatus({ code: SpanStatusCode.ERROR, message: secret });
});
expect(JSON.stringify({ events: exported?.events, status: exported?.status })).not.toContain(
secret,
);
expect(exported?.status.code).toBe(SpanStatusCode.ERROR);
expect(exported?.events[0]?.attributes?.["exception.type"]).toBe("ProviderError");
});
});
183 changes: 183 additions & 0 deletions e2e/cloud/auth-evidence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { randomBytes } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";

import { expect } from "@effect/vitest";
import { Effect, Schema } from "effect";

import { RUNS_DIR, scenario } from "../src/scenario";
import { RunDir, Target, Telemetry } from "../src/services";

const decodeString = Schema.decodeUnknownSync(Schema.String);

const decodeKey = Schema.decodeUnknownSync(
Schema.Struct({ id: Schema.String, value: Schema.String }),
);

scenario(
"Authentication · valid credentials work in headers but not query parameters",
{},
Effect.gen(function* () {
const target = yield* Target;
const runDir = yield* RunDir;
const identity = yield* target.newIdentity({ adminMfa: false });
const keyResponse = yield* Effect.promise(() =>
fetch(new URL("/api/account/api-keys", target.baseUrl), {
method: "POST",
headers: {
...identity.headers,
origin: target.baseUrl,
"content-type": "application/json",
},
body: JSON.stringify({ name: "authentication-evidence" }),
}),
);
expect(keyResponse.status).toBe(200);
const key = decodeKey(yield* Effect.promise(() => keyResponse.json()));
yield* Effect.promise(async () => {
const cases: Array<{ surface: string; carrier: string; status: number }> = [];
for (const surface of ["api", "mcp"]) {
const send = async (query: string | null, header: boolean) => {
const url = new URL(surface === "api" ? "/api/policies" : "/mcp", target.baseUrl);
if (query) url.searchParams.set(query, key.value);
const response = await fetch(url, {
method: surface === "api" ? "GET" : "POST",
headers: {
accept: "application/json, text/event-stream",
"content-type": "application/json",
...(header ? { authorization: `Bearer ${key.value}` } : {}),
},
...(surface === "mcp"
? {
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "authentication-evidence", version: "1" },
},
}),
}
: {}),
});
await response.text();
cases.push({
surface,
carrier: query ?? "Authorization header",
status: response.status,
});
return response.status;
};
expect(await send(null, true), `${surface} accepts the valid header credential`).toBe(200);
for (const query of [
"api_key",
"apikey",
"key",
"token",
"access_token",
"authorization",
]) {
expect(await send(query, false), `${surface} rejects query-only ${query}`).toBe(
surface === "api" ? 403 : 401,
);
}
}
await writeFile(
join(runDir, "authentication-carriers.json"),
JSON.stringify({ cases }, null, 2),
);
}).pipe(
Effect.ensuring(
Effect.promise(async () => {
const response = await fetch(new URL(`/api/account/api-keys/${key.id}`, target.baseUrl), {
method: "DELETE",
headers: { ...identity.headers, origin: target.baseUrl },
});
expect(response.status, "the disposable key is revoked").toBe(200);
}),
),
);
}),
);

scenario(
"Authentication · successful login exports diagnostics without its credentials",
{},
Effect.gen(function* () {
const target = yield* Target;
const telemetry = yield* Telemetry;
const runDir = yield* RunDir;
const identity = yield* target.newIdentity({ adminMfa: false });
const bootLog = join(RUNS_DIR, "cloud", "server-logs", "boot.log");
const initialLogLength = (yield* Effect.promise(() => readFile(bootLog, "utf8"))).length;
const traceId = randomBytes(16).toString("hex");
const headers = { traceparent: `00-${traceId}-${randomBytes(8).toString("hex")}-01` };
const credentials = yield* Effect.promise(async () => {
const login = await fetch(new URL("/api/auth/login", target.baseUrl), { redirect: "manual" });
expect(login.status).toBe(302);
const authorize = new URL(decodeString(login.headers.get("location")));
const state = decodeString(authorize.searchParams.get("state"));
const stateCookie = login.headers
.getSetCookie()
.find((cookie) => cookie.startsWith("wos-login-state="));
expect(stateCookie !== undefined).toBe(true);
authorize.searchParams.set("login_hint", identity.label);
const consent = await fetch(authorize, { redirect: "manual" });
expect(consent.status).toBe(302);
const callback = new URL(decodeString(consent.headers.get("location")));
const code = decodeString(callback.searchParams.get("code"));
const signedIn = await fetch(callback, {
redirect: "manual",
headers: {
...headers,
cookie: decodeString(stateCookie).split(";")[0] ?? "",
},
});
expect(signedIn.status).toBe(302);
const session = signedIn.headers
.getSetCookie()
.find((cookie) => cookie.startsWith("wos-session="));
const sessionPair = decodeString(session).split(";")[0] ?? "";
const verified = await fetch(new URL("/api/auth/me", target.baseUrl), {
headers: { cookie: sessionPair },
});
expect(verified.status).toBe(200);
return [state, code, sessionPair.slice("wos-session=".length)];
});
yield* telemetry.expectSpan({ traceId });
const spans = yield* telemetry.searchSpans({ traceId });
const exported = JSON.stringify(spans);
const logs = (yield* Effect.promise(() => readFile(bootLog, "utf8"))).slice(initialLogLength);
const matches = credentials.map((credential) => ({
trace: exported.includes(credential),
serverLog: logs.includes(credential),
}));
// Assert booleans so even a failure cannot print a credential.
expect(matches.every((match) => !match.trace && !match.serverLog)).toBe(true);
yield* Effect.promise(() =>
writeFile(
join(runDir, "login-diagnostics.json"),
JSON.stringify(
{
environment: "isolated cloud Worker with WorkOS emulator",
loginStatus: 302,
authenticatedSessionStatus: 200,
traceId,
exportedSpanCount: spans.length,
checkedCredentials: ["OAuth state", "authorization code", "sealed session cookie"],
credentialMatches: matches,
sample: spans.map(({ span }) => ({
operation: span.operationName,
status: span.status,
attributeNames: Object.keys(span.tags),
})),
},
null,
2,
),
),
);
}),
);
Loading
Loading