v4: an instance can be named by the address of its pages — sign-in and the trust list took only an instance's issuer, its API's origin, so where the pages live elsewhere the address people see in their browser named no instance: in development, localhost:3000 answered its /.well-known with index.html, and the sign-in failed as "doesn't answer as a gerbeur". resolveIssuer (remote/gerbeur-instance.ts) now reads the metadata at the address given and, when it names another issuer, takes it only if that issuer's own metadata names those pages back as where its /authorize is, so an impostor can't claim one; sign-in, the trust list and the check against this instance go through it. The Vite dev server passes /.well-known on to its API, as whatever serves the pages must in production (README). A handle still carries the API's host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 22:14:21 +00:00
parent 9608520675
commit f1bc23a65e
7 changed files with 161 additions and 27 deletions

View File

@@ -156,6 +156,57 @@ test("federation: a remote user's home link leads to its pages, not its API", as
);
});
test("federation: an instance is reached by its pages' address too, which its API must name back", async () => {
const home = await fakeInstance(HOME, {
authorization_endpoint: "https://www.home.test/authorize",
});
// Its pages pass `/.well-known` on to its API, as a proxy would; so does an
// impostor's, which the home doesn't name.
const proxy = (origin: string): FakeInstance => ({
...home,
origin,
handle: (url, init) =>
url.origin === origin
? home.handle(new URL(url.pathname + url.search, HOME), init)
: null,
});
using _net = network(
home,
proxy("https://www.home.test"),
proxy("https://evil.test"),
);
const peer = await trust("www.home.test");
assertEquals(peer.data.host, "home.test");
const start = await post<FederationLoginResponse>("/api/federation/logins", {
json: { instance: "https://www.home.test" },
});
assertEquals(start.status, 200);
const authorizeUrl = start.data.authorizeUrl;
const session = await post<AuthResponse>("/api/federation/sessions", {
json: {
code: home.issue(authorizeUrl, ALICE),
state: new URL(authorizeUrl).searchParams.get("state"),
},
});
assertEquals(session.data.user.username, "alice@home.test");
assertEquals(
(await post("/api/federation/logins", { json: { instance: "evil.test" } }))
.status,
502,
);
assertEquals((await trust("evil.test")).status, 502);
// Nor is this instance another, by its API's address or its pages'.
assertEquals(
(await post("/api/federation/logins", {
json: { instance: "gerbeur.test" },
}))
.status,
400,
);
});
test("federation: a login works once, with the code the home made for it", async () => {
const home = await fakeInstance(HOME);
using _net = network(home);