v4: federated sign-in closes the holes a review found — a callback link made from someone else's sign-in signed the visitor in to that account, a password reset left the user's sessions on other instances open, and events could be lost, reordered or delayed. FederatedSignIn keeps the sign-in's state in sessionStorage (utils/auth/federation.ts) and FederationCallback finishes only a sign-in this tab started. A password reset tells the instances the user signed in to, as a change does. The outbox delivers each instance's events in the order they were made, the first that fails holding back the rest, so a late older update no longer undoes a newer one, and delivers to every instance at once, so a dead one delays none of the others; an event queued during a run is sent by it.

Receiving, a home's keys that can't be fetched answer 502, which the sender retries, instead of 401, which it dropped; a bad update answers 400 rather than 502; a blocked instance's events answer 403; events are limited to a burst of 120 per instance, and a token naming an unknown key refetches the keys at most once a minute. An avatar must be on its issuer's origin, and its URL is versioned by its file's write time instead of updatedAt, so a profile edit no longer makes clients download it again. An instance that doesn't answer can be blocked, keeping its name, though not trusted.

303 backend tests and 703 frontend tests pass, the federation tests 8 times in a row, and deno check, tsc, eslint, doc:lint and i18n:compile --strict are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 16:05:11 +00:00
parent c77e173767
commit 66e402bd91
19 changed files with 447 additions and 128 deletions

View File

@@ -11,14 +11,17 @@
* instance.
* - `POST /api/federation/sessions` — public. Finishes it
* (`FederationSessionRequest`) and answers the user and their tokens, as a
* sign-in does. Rate-limited per instance.
* sign-in does. Each sign-in started finishes once, so the limit on
* `/logins` bounds it.
* - `POST /api/federation/events` — public, server to server. Applies a
* home instance's signed event about one of its users (`{ event }`).
* Rate-limited per instance; a 4xx tells the sender to give up on the
* event, a 429 or a 5xx to try again.
* - `GET /api/federation/peers` — optional sign-in. Answers the trusted
* instances, or every listed one to `federation:manage`.
* - `PUT /api/federation/peers` — permission `federation:manage`. Trusts or
* blocks an instance (`SetPeerRequest`) and answers it; 502 when it
* doesn't answer as a gerbeur.
* blocks an instance (`SetPeerRequest`) and answers it; 502 when one to
* trust doesn't answer as a gerbeur.
* - `DELETE /api/federation/peers/:host` — permission `federation:manage`.
* Takes an instance off the lists.
*

View File

@@ -6,7 +6,8 @@
* - `POST /api/password-resets` — public. Emails a reset link to the account
* with that `email`, if any; always answers 200.
* - `PUT /api/password-resets/:token` — public, with a reset token. Sets the
* `newPassword`.
* `newPassword`, which ends the user's sessions here and on the instances
* they signed in to.
*
* @module
*/
@@ -20,6 +21,7 @@ import {
requestPasswordReset,
resetPassword,
} from "../services/password-reset-service.ts";
import { notifyClients } from "../services/identity-provider-service.ts";
/**
* Mounted by `api/main.ts` behind the error, Open Graph and CORS middleware.
@@ -48,7 +50,8 @@ router.put("/:token", async (ctx) => {
if (typeof newPassword !== "string") {
throw new APIException(400, "Invalid request");
}
await resetPassword(ctx.params.token, newPassword);
const userId = await resetPassword(ctx.params.token, newPassword);
await notifyClients(userId, { type: "revoke" });
const responseBody: APIResponse<null> = { success: true, data: null };
ctx.response.body = responseBody;
});