1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// 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());