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:
@@ -30,6 +30,7 @@ import { APIException } from "../lib/api-exception.ts";
|
||||
import { FEDERATION } from "../config.ts";
|
||||
import { db, transaction } from "../db/connection.ts";
|
||||
import { verifyInstanceToken } from "../lib/jwt.ts";
|
||||
import { isSelfHost } from "../lib/self-url.ts";
|
||||
import { type RateLimit, takeToken } from "../lib/rate-limit.ts";
|
||||
import {
|
||||
type FederationLoginRequest,
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
fetchAvatar,
|
||||
fetchKeys,
|
||||
fetchMetadata,
|
||||
resolveIssuer,
|
||||
} from "../remote/gerbeur-instance.ts";
|
||||
import { ISSUER, REDIRECT_URI } from "./identity-provider-service.ts";
|
||||
import { getPeerStatus } from "./federation-peer-service.ts";
|
||||
@@ -95,17 +97,33 @@ function assertIncoming(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the origin of the instance `instance` names, checked as a home: not
|
||||
* this instance, nor a blocked one.
|
||||
* Returns the issuer of the instance `instance` names, by its API's address or
|
||||
* its pages' (`resolveIssuer`), checked as a home: not this instance, nor a
|
||||
* blocked one.
|
||||
*
|
||||
* @throws {APIException} 400 when it names no instance or this one, 403 when
|
||||
* it is blocked.
|
||||
* it is blocked, 502 when it doesn't answer as a gerbeur.
|
||||
*/
|
||||
function homeOf(instance: string): string {
|
||||
const issuer = parseInstance(instance);
|
||||
if (!issuer) throw new APIException(400, "Not an instance's address");
|
||||
async function homeOf(instance: string): Promise<string> {
|
||||
const origin = parseInstance(instance);
|
||||
if (!origin) throw new APIException(400, "Not an instance's address");
|
||||
const blocked = (issuer: string) => getPeerStatus(issuer) === "blocked";
|
||||
if (isSelfHost(new URL(origin).host)) {
|
||||
throw new APIException(400, "That is this instance");
|
||||
}
|
||||
// Typed by its API's address, a blocked instance is refused unreached.
|
||||
if (blocked(origin)) {
|
||||
throw new APIException(403, "This instance is blocked here");
|
||||
}
|
||||
let issuer: string;
|
||||
try {
|
||||
issuer = await resolveIssuer(origin);
|
||||
} catch (err) {
|
||||
console.error(`[federation] ${origin} unreachable:`, err);
|
||||
throw new APIException(502, "That instance doesn't answer as a gerbeur");
|
||||
}
|
||||
if (issuer === ISSUER) throw new APIException(400, "That is this instance");
|
||||
if (getPeerStatus(issuer) === "blocked") {
|
||||
if (blocked(issuer)) {
|
||||
throw new APIException(403, "This instance is blocked here");
|
||||
}
|
||||
return issuer;
|
||||
@@ -128,7 +146,7 @@ export async function startLogin(
|
||||
request: FederationLoginRequest,
|
||||
): Promise<string> {
|
||||
assertIncoming();
|
||||
const issuer = homeOf(request.instance);
|
||||
const issuer = await homeOf(request.instance);
|
||||
if (request.inviteToken) {
|
||||
validateInvite(request.inviteToken);
|
||||
} else if (
|
||||
|
||||
Reference in New Issue
Block a user