// node --test javascript/src/oauth-server/oauth-server.test.mjs (needs `npm install` here and `cargo wasm`; skips without them) // A Node server acting for signed-in users with THEIR OAuth sessions, against a fake authorization server + PDS // (fake.mjs, no live accounts): sign-in through a reverse-proxied public origin (Host never trusted), 8 // concurrent statusphere runs on an expiring token (one refresh, also across two processes' clients), 3 users x // 3 runs at once (each run writes only as its own user), an unknown session refused, and sign-out revoking. import assert from "node:assert/strict"; import { existsSync } from "node:fs"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { after, before, test } from "node:test"; import { localModule, runModule } from "../run-module/index.js"; import { contextOptions, createNodeContext, fileFetch, hostCall } from "../server/index.js"; import { browser, fakeAtproto } from "./fake.mjs"; const ROOT = new URL("../../../", import.meta.url); const WASM = new URL("target/wasm32-unknown-unknown/release/statusphere.wasm", ROOT); const installed = existsSync(new URL("node_modules/@atproto/oauth-client-node", import.meta.url)); const skip = (!installed && "npm install in javascript/src/oauth-server") || (!existsSync(WASM) && "cargo wasm"); const ORIGIN = "https://site.example"; // the FQDN a reverse proxy forwards to this server let fake, statusphere; before(async () => { if (skip) return; statusphere = { ...(await localModule(WASM, new URL("examples/rust/statusphere/module.json", ROOT), fileFetch)), id: "statusphere" }; }); after(() => fake?.close()); // A host: the oauth routes and POST /run/ as the request's user (the shape examples/node-hono has). async function server(ctx, origin, fetch = (input, init) => globalThis.fetch(input, init)) { const { oauthServer } = await import("./index.js"); const oauth = await oauthServer(ctx, { origin, scope: "atproto transition:generic", fetch, handleResolver: fake.base, plcDirectoryUrl: fake.base, allowHttp: true }); const app = async (request) => { const url = new URL(request.url); if (url.host !== new URL(origin).host) return Response.json({ error: "Forbidden" }, { status: 403 }); // the Host guard: only the configured origin's const answered = await oauth.handle(request); if (answered) return answered; if (request.method === "POST" && url.pathname.startsWith("/run/")) { let session; try { session = await oauth.sessionOf(request); } catch (error) { return Response.json({ error: "AuthRequired", message: error.message }, { status: error.status ?? 401 }); } if (!session) return Response.json({ error: "AuthRequired" }, { status: 401 }); const call = hostCall({ ctx, xrpc: session.xrpc, pds: session.pds }); const ran = await runModule(ctx, statusphere, url.pathname.slice(5), { did: session.did, now: new Date().toISOString(), ...(await request.json()) }, call); return Response.json(ran); } return new Response("not found", { status: 404 }); }; return { oauth, app }; } async function signIn(app, user) { // the AS reaches the client documents at the public origin; the browser reaches both const b = browser((url) => (url.startsWith(ORIGIN) ? app : globalThis.fetch)); const back = await b.go(`${ORIGIN}/oauth/login?handle=${user}.test`); assert.equal(back.status, 404, "signed in, then sent to the origin's / (this test app has no page there)"); assert.equal(back.finalUrl, `${ORIGIN}/`); const handle = b.cookie(ORIGIN, "atlibs_session"); assert.ok(handle, `${user} has a session cookie`); return { b, handle }; } const run = (app, handle, status = "🙂") => app(new Request(`${ORIGIN}/run/set_status`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${handle}` }, body: JSON.stringify({ status }) })).then((r) => r.json()); test("a server acts for each signed-in user with their own OAuth session: one refresh for N runs, no cross-use", { skip }, async () => { const dir = await mkdtemp(join(tmpdir(), "atlibs-oauth-server-")); let app; fake = await fakeAtproto({ fetchClient: (url) => app(new Request(url)) }); const ctx = createNodeContext(contextOptions(join(dir, "state"))); try { ({ app } = await server(ctx, ORIGIN)); const meta = await (await app(new Request(`${ORIGIN}/oauth/client-metadata.json`))).json(); assert.equal(meta.client_id, `${ORIGIN}/oauth/client-metadata.json`, "client_id on the configured origin"); assert.deepEqual(meta.redirect_uris, [`${ORIGIN}/oauth/callback`]); assert.equal(meta.token_endpoint_auth_method, "private_key_jwt"); assert.equal((await app(new Request("http://evil.example/oauth/login?handle=alice.test"))).status, 403, "Host is not trusted"); // N=8 concurrent runs for alice on an expiring token: one refresh, all succeed, all as alice fake.accessTtl = 1; const alice = await signIn(app, "alice"); fake.accessTtl = 3600; await new Promise((resolve) => setTimeout(resolve, 1100)); fake.refreshes = 0; const ran = await Promise.all(Array.from({ length: 8 }, () => run(app, alice.handle))); assert.deepEqual(ran.map((r) => r.error), Array(8).fill(undefined), JSON.stringify(ran.find((r) => r.error))); assert.ok(ran.every((r) => r.result.uri.startsWith(`at://${fake.did("alice")}/`))); assert.equal(fake.refreshes, 1, "one refresh for 8 runs"); // a second process on the same state dir (its own client): tokens expire server-side. Forced order: process // 2's token is refused, process 1 renews it (refused, refresh, saved), and only then does process 2 go on with // its refused token: it must take the saved one, not refresh again (refresh tokens rotate) const ctx2 = createNodeContext(contextOptions(join(dir, "state"))); try { let refusedHeld, release; const held = new Promise((resolve) => (refusedHeld = resolve)); const go = new Promise((resolve) => (release = resolve)); const { app: app2 } = await server(ctx2, ORIGIN, async (input, init) => { const response = await globalThis.fetch(input, init); if (response.headers.get("www-authenticate")?.includes("invalid_token")) (refusedHeld(), await go); return response; }); fake.expireAll(); fake.refreshes = 0; const second = run(app2, alice.handle); await held; assert.equal((await run(app, alice.handle)).error, undefined); assert.equal(fake.refreshes, 1, "process 1 renewed the refused token"); release(); assert.equal((await second).error, undefined); assert.equal(fake.refreshes, 1, "process 2 used process 1's renewal: one refresh across both processes"); // and 4 + 4 concurrent runs, tokens refused again: one refresh fake.expireAll(); fake.refreshes = 0; const both = await Promise.all(Array.from({ length: 8 }, (_, i) => run(i % 2 ? app2 : app, alice.handle))); assert.deepEqual(both.map((r) => r.error), Array(8).fill(undefined), JSON.stringify(both.find((r) => r.error))); assert.equal(fake.refreshes, 1, "one refresh across both processes"); } finally { ctx2.close(); } // 3 users x 3 concurrent runs: every write is in the writer's own repo, with their own token const users = { alice }; for (const user of ["bob", "carol"]) users[user] = await signIn(app, user); fake.writes.length = 0; const all = await Promise.all(Object.entries(users).flatMap(([user, { handle }]) => [1, 2, 3].map(async () => [user, await run(app, handle)]))); for (const [user, ranAs] of all) { assert.equal(ranAs.error, undefined, `${user}: ${ranAs.error}`); assert.ok(ranAs.result.uri.startsWith(`at://${fake.did(user)}/`), `${user}'s run wrote ${ranAs.result.uri}`); } assert.equal(fake.writes.length, 9); assert.ok(fake.writes.every((w) => w.sub === w.repo), "no token wrote another user's repo"); // a session handle not issued here is refused, not run signed out or as someone else assert.equal((await app(new Request(`${ORIGIN}/run/set_status`, { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer forged" }, body: "{}" }))).status, 401); // sign-out revokes bob's tokens; his handle stops working; alice is untouched const out = await bob(app, users.bob.handle); assert.equal(out.signedOut, fake.did("bob")); assert.equal(fake.revocations, 1); assert.equal((await app(new Request(`${ORIGIN}/run/set_status`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${users.bob.handle}` }, body: "{}" }))).status, 401); assert.equal((await run(app, alice.handle)).error, undefined); } finally { ctx.close(); } }); const bob = (app, handle) => app(new Request(`${ORIGIN}/oauth/logout`, { method: "POST", headers: { authorization: `Bearer ${handle}` } })).then((r) => r.json());