Something went wrong. Try again.
The logic for serving and storing associations between users and apps
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import worker, { type Env } from "./index";import { DEVICE_TOKEN_TTL_SEC, mintDeviceToken } from "./device-token";import { clearMetadataCache } from "./metadata";
const SECRET = "integration-secret-at-least-32-chars";const APP = "https://your-app.example";const OTHER_APP = "https://other-app.example";const CALLBACK = "example.your-app://loggedin";const DID = "did:plc:z72i7hdynmk6r22z27h6tvur";
type Row = { did: string; origin: string };
/** In-memory stand-in for the Durable Object, keyed by session id. */function makeEnv(): Env & { rows: Map<string, Row[]> } { const rows = new Map<string, Row[]>(); const stubFor = (name: string) => ({ async list() { const byDid = new Map<string, string[]>(); for (const row of rows.get(name) ?? []) { const origins = byDid.get(row.did) ?? []; if (!origins.includes(row.origin)) origins.push(row.origin); byDid.set(row.did, origins); } return { accounts: [...byDid].map(([did, origins]) => ({ did, origins })) }; }, async loggedIn(did: string, origin: string) { const existing = rows.get(name) ?? []; if (!existing.some((row) => row.did === did && row.origin === origin)) { existing.push({ did, origin }); } rows.set(name, existing); return { ok: true }; }, async logout(did: string, origin: string) { rows.set( name, (rows.get(name) ?? []).filter((row) => !(row.did === did && row.origin === origin)), ); return { ok: true }; }, });
return { rows, DEVICE_TOKEN_SECRET: SECRET, SESSIONS: { idFromName: (name: string) => name, get: (id: string) => stubFor(id), }, } as unknown as Env & { rows: Map<string, Row[]> };}
function metadataFor(origin: string, redirectUris: string[]): Response { return new Response( JSON.stringify({ client_id: `${origin}/oauth-client-metadata.json`, client_name: "Your App", client_uri: origin, redirect_uris: redirectUris, }), { status: 200, headers: { "content-type": "application/json" } }, );}
beforeEach(() => { clearMetadataCache(); vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url === `${APP}/oauth-client-metadata.json`) { return metadataFor(APP, [CALLBACK, `${APP}/oauth/callback`]); } if (url === `${OTHER_APP}/oauth-client-metadata.json`) { return metadataFor(OTHER_APP, [`${OTHER_APP}/oauth/callback`]); } return new Response("not found", { status: 404 }); }), );});
afterEach(() => { vi.unstubAllGlobals();});
function startUrl(params: Record<string, string>): string { const url = new URL("https://loggedin.fyi/app/start"); for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value); return url.toString();}
async function freshToken(sid: string, origin = APP): Promise<string> { return mintDeviceToken(SECRET, { sid, origin, expiresAt: Math.floor(Date.now() / 1000) + DEVICE_TOKEN_TTL_SEC, });}
function tokenRequest(path: string, token: string, body?: unknown): Request { return new Request(`https://loggedin.fyi${path}`, { method: body ? "POST" : "GET", headers: { "X-Loggedin-Token": token, ...(body ? { "content-type": "application/json" } : {}), }, body: body ? JSON.stringify(body) : undefined, });}
describe("GET /app/start", () => { it("mints a token bound to the cookie session and embeds it in the page", async () => { const env = makeEnv(); const sid = "ab".repeat(16); const res = await worker.fetch( new Request(startUrl({ origin: APP, callback: CALLBACK, state: "xyz" }), { headers: { Cookie: `loggedin_session=${sid}` }, }), env, ); expect(res.status).toBe(200); const body = await res.text(); expect(body).toContain(CALLBACK); expect(body).toContain("xyz");
const token = /"(v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)"/.exec(body)?.[1]; expect(token).toBeTruthy();
// The token must address the same Durable Object the browser cookie does. const write = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.loggedIn", token!, { did: DID }), env, ); expect(write.status).toBe(200); expect(env.rows.get(sid)).toEqual([{ did: DID, origin: APP }]); });
it("does not let a crafted state break out of the inline script", async () => { const res = await worker.fetch( new Request( startUrl({ origin: APP, callback: CALLBACK, state: "</script><script>alert(1)</script>", }), ), makeEnv(), ); expect(res.status).toBe(200); const body = await res.text(); expect(body).not.toContain("</script><script>alert(1)"); expect(body).toContain("\\u003c/script\\u003e"); });
it("refuses a callback that is not in redirect_uris", async () => { const res = await worker.fetch( new Request(startUrl({ origin: APP, callback: "example.evil://steal" })), makeEnv(), ); expect(res.status).toBe(403); expect((await res.json<{ message: string }>()).message).toContain("redirect_uris"); });
it("refuses an origin whose metadata does not validate", async () => { const res = await worker.fetch( new Request(startUrl({ origin: "https://unknown.example", callback: CALLBACK })), makeEnv(), ); expect(res.status).toBe(403); });
it("is disabled when no signing secret is configured", async () => { const env = makeEnv(); delete env.DEVICE_TOKEN_SECRET; const res = await worker.fetch( new Request(startUrl({ origin: APP, callback: CALLBACK })), env, ); expect(res.status).toBe(501); });});
describe("token-authenticated XRPC", () => { it("lists the same accounts the browser session sees", async () => { const env = makeEnv(); const sid = "cd".repeat(16); env.rows.set(sid, [{ did: DID, origin: OTHER_APP }]); const res = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.list", await freshToken(sid)), env, ); expect(res.status).toBe(200); expect(await res.json()).toEqual({ accounts: [{ did: DID, origins: [OTHER_APP] }], }); });
it("never sets a session cookie for a native caller", async () => { const env = makeEnv(); const res = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.list", await freshToken("ef".repeat(16))), env, ); expect(res.headers.get("Set-Cookie")).toBeNull(); });
it("rejects a forged or expired token with 401", async () => { const env = makeEnv(); const forged = await mintDeviceToken("some-other-secret-value-32-characters", { sid: "ab".repeat(16), origin: APP, expiresAt: Math.floor(Date.now() / 1000) + 60, }); const forgedRes = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.list", forged), env, ); expect(forgedRes.status).toBe(401);
const expired = await mintDeviceToken(SECRET, { sid: "ab".repeat(16), origin: APP, expiresAt: Math.floor(Date.now() / 1000) - 1, }); const expiredRes = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.list", expired), env, ); expect(expiredRes.status).toBe(401); });
it("pins writes to the origin inside the token", async () => { const env = makeEnv(); const sid = "ab".repeat(16); const token = await freshToken(sid, APP);
// Claiming another app's origin is refused outright. const spoof = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.loggedIn", token, { did: DID, origin: OTHER_APP, }), env, ); expect(spoof.status).toBe(403); expect(env.rows.get(sid)).toBeUndefined();
// Omitting the origin falls back to the token's own origin. const ok = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.loggedIn", token, { did: DID }), env, ); expect(ok.status).toBe(200); expect(env.rows.get(sid)).toEqual([{ did: DID, origin: APP }]); });
it("only removes its own origin's row on logout", async () => { const env = makeEnv(); const sid = "ab".repeat(16); env.rows.set(sid, [ { did: DID, origin: APP }, { did: DID, origin: OTHER_APP }, ]); const res = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.logout", await freshToken(sid, APP), { did: DID }), env, ); expect(res.status).toBe(200); expect(env.rows.get(sid)).toEqual([{ did: DID, origin: OTHER_APP }]); });
it("rejects a bad did before touching storage", async () => { const env = makeEnv(); const sid = "ab".repeat(16); const res = await worker.fetch( tokenRequest("/xrpc/fyi.loggedin.account.loggedIn", await freshToken(sid), { did: "not-a-did", }), env, ); expect(res.status).toBe(400); expect(env.rows.get(sid)).toBeUndefined(); });});