// Cloud-only: the WorkOS login → AuthKit → callback session contract, driven// over the wire against the real handlers (AuthKit is the emulator). The happy// path itself is what `newIdentity()` performs on every scenario — these// scenarios pin the EDGE guarantees around it: the CSRF state handshake, the// callback refusing forged/incomplete redirects, the sealed-session cookie// actually authorizing the session API, and logout dropping the cookie.import { expect } from "@effect/vitest";import { Effect, Encoding, Result, Schema } from "effect"; import { scenario } from "../src/scenario";import { Api, Target } from "../src/services"; /** First `Set-Cookie` header for `name`, as the raw header string. */const setCookieFor = (response: Response, name: string): string => { for (const header of response.headers.getSetCookie()) { if (header.startsWith(`${name}=`)) return header; } return "";}; // state = base64url(JSON { nonce, returnTo? }) — the app's login-state// envelope (apps/cloud/src/auth/login-state.ts).const decodeLoginState = Schema.decodeUnknownOption( Schema.fromJsonString( Schema.Struct({ nonce: Schema.String, returnTo: Schema.optional(Schema.String) }), ),); scenario( "Auth · login redirects to hosted AuthKit carrying a short-lived CSRF state cookie", {}, Effect.gen(function* () { // Gate: the REST API plane is mounted on this target. yield* Api; const target = yield* Target; const response = yield* Effect.promise(() => fetch(new URL("/api/auth/login", target.baseUrl), { redirect: "manual" }), ); expect(response.status, "login hands the browser to AuthKit").toBe(302); const authorizeUrl = new URL(response.headers.get("location") ?? ""); const state = authorizeUrl.searchParams.get("state") ?? ""; // The nonce is the CSRF secret; returnTo (absent here, no query was // passed) rides beside it. const decoded = decodeLoginState( Result.getOrElse(Encoding.decodeBase64UrlString(state), () => ""), ); expect(decoded._tag, "the state decodes as our login-state envelope").toBe("Some"); expect( decoded._tag === "Some" ? decoded.value.nonce : "", "the state carries an unguessable CSRF nonce", ).toMatch(/^[0-9a-f]{64}$/); expect( authorizeUrl.searchParams.get("redirect_uri"), "AuthKit is told to come back to this deployment's callback", ).toBe(new URL("/api/auth/callback", target.baseUrl).toString()); const stateCookie = setCookieFor(response, "wos-login-state"); expect(stateCookie, "the same state is pinned in a cookie for the callback").toContain( `wos-login-state=${state}`, ); expect(stateCookie, "the login state expires quickly").toContain("Max-Age=600"); expect(stateCookie, "the login state is not readable by page scripts").toContain("HttpOnly"); }),); scenario( "Auth · the callback rejects forged or incomplete redirects without exchanging the code", {}, Effect.gen(function* () { // Gate: the REST API plane is mounted on this target. yield* Api; const target = yield* Target; const callback = (search: string, cookie?: string) => Effect.promise(() => fetch(new URL(`/api/auth/callback${search}`, target.baseUrl), { redirect: "manual", ...(cookie ? { headers: { cookie } } : {}), }), ); // A state that matches no login-state cookie (no login ever started). const noCookie = yield* callback("?code=attacker-code&state=attacker-state"); expect(noCookie.status, "a state with no matching login cookie is refused").toBe(400); expect( setCookieFor(noCookie, "wos-session"), "no session is minted from the forged redirect", ).toBe(""); // A state that contradicts the login-state cookie the browser holds. const mismatched = yield* callback( "?code=attacker-code&state=attacker-state", "wos-login-state=0000000000000000000000000000000000000000000000000000000000000000", ); expect(mismatched.status, "a state contradicting the login cookie is refused").toBe(400); expect( setCookieFor(mismatched, "wos-session"), "no session is minted from the mismatched redirect", ).toBe(""); // A redirect that never carried the authorization code at all. const noCode = yield* callback(""); expect(noCode.status, "a callback without a code is refused").toBe(400); }),); scenario( "Auth · a completed login's session cookie authorizes the session API", {}, Effect.gen(function* () { // Gate: the REST API plane is mounted on this target. yield* Api; const target = yield* Target; // newIdentity() IS the full product login (login → AuthKit → callback); // its headers carry the genuine sealed-session cookie the callback set. const identity = yield* target.newIdentity(); const response = yield* Effect.promise(() => fetch(new URL("/api/auth/me", target.baseUrl), { headers: identity.headers ?? {} }), ); expect(response.status, "the sealed-session cookie is accepted").toBe(200); const me = (yield* Effect.promise(() => response.json())) as { user: { email: string }; organization: { id: string } | null; }; expect(me.user.email, "the session belongs to the user who logged in").toBe(identity.label); expect(me.organization?.id, "the session carries the active organization").toMatch(/^org_/); }),); scenario( "Auth · logout ends the AuthKit session upstream and tells the browser to drop the cookie", {}, Effect.gen(function* () { // Gate: the REST API plane is mounted on this target. yield* Api; const target = yield* Target; const identity = yield* target.newIdentity(); const response = yield* Effect.promise(() => fetch(new URL("/api/auth/logout", target.baseUrl), { method: "POST", redirect: "manual", headers: identity.headers ?? {}, }), ); expect(response.status, "logout hands the browser to WorkOS").toBe(302); const cleared = setCookieFor(response, "wos-session"); expect(cleared, "the session cookie is expired immediately").toContain("Max-Age=0"); expect(cleared, "the cookie value is wiped").toContain("wos-session=;"); // The destination is WorkOS's session-end endpoint carrying this // session's id — without this hop the hosted AuthKit session survives // and the next sign-in silently re-authenticates. const logoutUrl = new URL(response.headers.get("location") ?? ""); expect(logoutUrl.pathname, "the destination ends the WorkOS session").toBe( "/user_management/sessions/logout", ); expect( logoutUrl.searchParams.get("session_id"), "the WorkOS session being ended is this identity's", ).toMatch(/^session_/); const upstream = yield* Effect.promise(() => fetch(logoutUrl, { redirect: "manual" })); expect(upstream.status, "WorkOS ends the session and sends the user onward").toBe(302); expect( upstream.headers.get("location"), "the sign-out return URL lands back on this deployment", ).toBe(new URL("/", target.baseUrl).toString()); }),);
// Cloud-only: the WorkOS login → AuthKit → callback session contract, driven
// over the wire against the real handlers (AuthKit is the emulator). The happy
// path itself is what `newIdentity()` performs on every scenario — these
// scenarios pin the EDGE guarantees around it: the CSRF state handshake, the
// callback refusing forged/incomplete redirects, the sealed-session cookie
// actually authorizing the session API, and logout dropping the cookie.
import { expect } from "@effect/vitest";
import { Effect, Encoding, Result, Schema } from "effect";
import { scenario } from "../src/scenario";
import { Api, Target } from "../src/services";
/** First `Set-Cookie` header for `name`, as the raw header string. */
const setCookieFor = (response: Response, name: string): string => {
for (const header of response.headers.getSetCookie()) {
if (header.startsWith(`${name}=`)) return header;
}
return "";
};
// state = base64url(JSON { nonce, returnTo? }) — the app's login-state
// envelope (apps/cloud/src/auth/login-state.ts).
const decodeLoginState = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({ nonce: Schema.String, returnTo: Schema.optional(Schema.String) }),
),
);
scenario(
"Auth · login redirects to hosted AuthKit carrying a short-lived CSRF state cookie",
{},
Effect.gen(function* () {
// Gate: the REST API plane is mounted on this target.
yield* Api;
const target = yield* Target;
const response = yield* Effect.promise(() =>
fetch(new URL("/api/auth/login", target.baseUrl), { redirect: "manual" }),
);
expect(response.status, "login hands the browser to AuthKit").toBe(302);
const authorizeUrl = new URL(response.headers.get("location") ?? "");
const state = authorizeUrl.searchParams.get("state") ?? "";
// The nonce is the CSRF secret; returnTo (absent here, no query was
// passed) rides beside it.
const decoded = decodeLoginState(
Result.getOrElse(Encoding.decodeBase64UrlString(state), () => ""),
);
expect(decoded._tag, "the state decodes as our login-state envelope").toBe("Some");
expect(
decoded._tag === "Some" ? decoded.value.nonce : "",
"the state carries an unguessable CSRF nonce",
).toMatch(/^[0-9a-f]{64}$/);
expect(
authorizeUrl.searchParams.get("redirect_uri"),
"AuthKit is told to come back to this deployment's callback",
).toBe(new URL("/api/auth/callback", target.baseUrl).toString());
const stateCookie = setCookieFor(response, "wos-login-state");
expect(stateCookie, "the same state is pinned in a cookie for the callback").toContain(
`wos-login-state=${state}`,
);
expect(stateCookie, "the login state expires quickly").toContain("Max-Age=600");
expect(stateCookie, "the login state is not readable by page scripts").toContain("HttpOnly");
}),
);
scenario(
"Auth · the callback rejects forged or incomplete redirects without exchanging the code",
{},
Effect.gen(function* () {
// Gate: the REST API plane is mounted on this target.
yield* Api;
const target = yield* Target;
const callback = (search: string, cookie?: string) =>
Effect.promise(() =>
fetch(new URL(`/api/auth/callback${search}`, target.baseUrl), {
redirect: "manual",
...(cookie ? { headers: { cookie } } : {}),
}),
);
// A state that matches no login-state cookie (no login ever started).
const noCookie = yield* callback("?code=attacker-code&state=attacker-state");
expect(noCookie.status, "a state with no matching login cookie is refused").toBe(400);
expect(
setCookieFor(noCookie, "wos-session"),
"no session is minted from the forged redirect",
).toBe("");
// A state that contradicts the login-state cookie the browser holds.
const mismatched = yield* callback(
"?code=attacker-code&state=attacker-state",
"wos-login-state=0000000000000000000000000000000000000000000000000000000000000000",
);
expect(mismatched.status, "a state contradicting the login cookie is refused").toBe(400);
expect(
setCookieFor(mismatched, "wos-session"),
"no session is minted from the mismatched redirect",
).toBe("");
// A redirect that never carried the authorization code at all.
const noCode = yield* callback("");
expect(noCode.status, "a callback without a code is refused").toBe(400);
}),
);
scenario(
"Auth · a completed login's session cookie authorizes the session API",
{},
Effect.gen(function* () {
// Gate: the REST API plane is mounted on this target.
yield* Api;
const target = yield* Target;
// newIdentity() IS the full product login (login → AuthKit → callback);
// its headers carry the genuine sealed-session cookie the callback set.
const identity = yield* target.newIdentity();
const response = yield* Effect.promise(() =>
fetch(new URL("/api/auth/me", target.baseUrl), { headers: identity.headers ?? {} }),
);
expect(response.status, "the sealed-session cookie is accepted").toBe(200);
const me = (yield* Effect.promise(() => response.json())) as {
user: { email: string };
organization: { id: string } | null;
};
expect(me.user.email, "the session belongs to the user who logged in").toBe(identity.label);
expect(me.organization?.id, "the session carries the active organization").toMatch(/^org_/);
}),
);
scenario(
"Auth · logout ends the AuthKit session upstream and tells the browser to drop the cookie",
{},
Effect.gen(function* () {
// Gate: the REST API plane is mounted on this target.
yield* Api;
const target = yield* Target;
const identity = yield* target.newIdentity();
const response = yield* Effect.promise(() =>
fetch(new URL("/api/auth/logout", target.baseUrl), {
method: "POST",
redirect: "manual",
headers: identity.headers ?? {},
}),
);
expect(response.status, "logout hands the browser to WorkOS").toBe(302);
const cleared = setCookieFor(response, "wos-session");
expect(cleared, "the session cookie is expired immediately").toContain("Max-Age=0");
expect(cleared, "the cookie value is wiped").toContain("wos-session=;");
// The destination is WorkOS's session-end endpoint carrying this
// session's id — without this hop the hosted AuthKit session survives
// and the next sign-in silently re-authenticates.
const logoutUrl = new URL(response.headers.get("location") ?? "");
expect(logoutUrl.pathname, "the destination ends the WorkOS session").toBe(
"/user_management/sessions/logout",
);
expect(
logoutUrl.searchParams.get("session_id"),
"the WorkOS session being ended is this identity's",
).toMatch(/^session_/);
const upstream = yield* Effect.promise(() => fetch(logoutUrl, { redirect: "manual" }));
expect(upstream.status, "WorkOS ends the session and sends the user onward").toBe(302);
expect(
upstream.headers.get("location"),
"the sign-out return URL lands back on this deployment",
).toBe(new URL("/", target.baseUrl).toString());
}),
);