fix: secure authentication request identity
Replace Better Auth's default trust of a caller-controlled X-Forwarded-For with a platform-owned client-address resolution boundary: direct mode (the safe default) trusts only the real socket peer, and an explicit trusted-proxy mode requires a header name, a concrete trusted-proxy list, and only honors a forwarded chain from an actual trusted peer. This closes a rate-limit bypass where rotating a spoofed header let a caller choose a fresh bucket on every request. Public sign-up is also disabled by default and requires an explicit typed opt-in, using Better Auth's own disableSignUp gate so no email-existence leak or second signup path is introduced. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5e09330001
commit
cafd513019
@@ -209,6 +209,7 @@ describe("Ekklesia Doctor diagnostics", () => {
|
||||
"postgresql.connection",
|
||||
"postgresql.plan",
|
||||
"identity.configuration",
|
||||
"identity.client-address",
|
||||
"object-storage.integration",
|
||||
]);
|
||||
expect(check(report, "modules.discovery").details).toEqual(["people 0.1.0", "calendar 0.2.0"]);
|
||||
@@ -613,6 +614,137 @@ describe("Ekklesia Doctor diagnostics", () => {
|
||||
expect(JSON.stringify(report)).not.toContain(hostileSecret);
|
||||
});
|
||||
|
||||
it("passes identity configuration and reports public signup disabled by default", async () => {
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
identityConfiguration: {
|
||||
baseUrl: "https://identity.example.test",
|
||||
environment: "production",
|
||||
secret: "correct-horse-battery-staple-0123456789",
|
||||
trustedOrigins: ["https://identity.example.test"],
|
||||
},
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const identity = check(report, "identity.configuration");
|
||||
expect(identity.status).toBe("pass");
|
||||
expect(identity.details.some((detail) => detail.includes("disabled (safe default)"))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("reports public signup as explicitly enabled when configured", async () => {
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
identityConfiguration: {
|
||||
baseUrl: "https://identity.example.test",
|
||||
environment: "production",
|
||||
publicSignupEnabled: "true",
|
||||
secret: "correct-horse-battery-staple-0123456789",
|
||||
trustedOrigins: ["https://identity.example.test"],
|
||||
},
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const identity = check(report, "identity.configuration");
|
||||
expect(identity.status).toBe("pass");
|
||||
expect(identity.details.some((detail) => detail.includes("explicitly ENABLED"))).toBe(true);
|
||||
});
|
||||
|
||||
it("fails identity configuration for a misspelled public-signup value, never silently disabling or enabling it", async () => {
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
identityConfiguration: {
|
||||
baseUrl: "https://identity.example.test",
|
||||
environment: "production",
|
||||
publicSignupEnabled: "yes",
|
||||
secret: "correct-horse-battery-staple-0123456789",
|
||||
trustedOrigins: ["https://identity.example.test"],
|
||||
},
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const identity = check(report, "identity.configuration");
|
||||
expect(identity.status).toBe("failure");
|
||||
expect(
|
||||
identity.details.some((detail) => detail.includes("PUBLIC_SIGNUP_ENABLED_INVALID")),
|
||||
).toBe(true);
|
||||
expect(report.result).toBe("not-ready");
|
||||
});
|
||||
|
||||
it("passes client-address configuration with direct mode when nothing is configured", async () => {
|
||||
const report = await diagnoseEkklesia(options(), dependencies());
|
||||
|
||||
const clientAddress = check(report, "identity.client-address");
|
||||
expect(clientAddress.status).toBe("pass");
|
||||
expect(clientAddress.summary).toContain("Direct");
|
||||
expect(report.result).toBe("ready-with-warnings");
|
||||
});
|
||||
|
||||
it("passes client-address configuration with an explicitly configured trusted-proxy mode", async () => {
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
clientAddress: {
|
||||
header: "x-forwarded-for",
|
||||
mode: "trusted-proxy",
|
||||
trustedProxies: ["10.0.0.1", "10.0.0.2"],
|
||||
},
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const clientAddress = check(report, "identity.client-address");
|
||||
expect(clientAddress.status).toBe("pass");
|
||||
expect(clientAddress.summary).toContain("Trusted-proxy");
|
||||
expect(clientAddress.details.some((detail) => detail.includes("x-forwarded-for"))).toBe(true);
|
||||
});
|
||||
|
||||
it("fails client-address configuration for an invalid trusted-proxy entry without leaking it", async () => {
|
||||
const hostileEntry = "not-an-ip-secret-value";
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
clientAddress: {
|
||||
header: "x-forwarded-for",
|
||||
mode: "trusted-proxy",
|
||||
trustedProxies: [hostileEntry],
|
||||
},
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const clientAddress = check(report, "identity.client-address");
|
||||
expect(clientAddress.status).toBe("failure");
|
||||
expect(
|
||||
clientAddress.details.some((detail) =>
|
||||
detail.includes("CLIENT_ADDRESS_TRUSTED_PROXY_INVALID"),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(JSON.stringify(report)).not.toContain(hostileEntry);
|
||||
expect(report.result).toBe("not-ready");
|
||||
});
|
||||
|
||||
it("fails client-address configuration for an incomplete trusted-proxy mode (missing header)", async () => {
|
||||
const report = await diagnoseEkklesia(
|
||||
{
|
||||
...options(),
|
||||
clientAddress: { mode: "trusted-proxy", trustedProxies: ["10.0.0.1"] },
|
||||
},
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
const clientAddress = check(report, "identity.client-address");
|
||||
expect(clientAddress.status).toBe("failure");
|
||||
expect(report.result).toBe("not-ready");
|
||||
});
|
||||
|
||||
it("never constructs a migration database or Better Auth merely to check identity configuration", async () => {
|
||||
const services = dependencies();
|
||||
await diagnoseEkklesia(
|
||||
|
||||
Reference in New Issue
Block a user