~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
153 lines · 8.6 KB Raw History
// 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/<entry> 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());