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:
ekklesia
2026-07-26 12:20:51 -05:00
co-authored by Claude Sonnet 5
parent 5e09330001
commit cafd513019
22 changed files with 1772 additions and 38 deletions
+52 -1
View File
@@ -7,11 +7,17 @@ import {
} from "@ekklesia/contracts";
import { getTableName } from "drizzle-orm";
import { type PermissionRegistry, PermissionRegistryError } from "../authorization/index.js";
import {
type ClientAddressConfigInput,
ClientAddressConfigError,
createClientAddressConfig,
} from "../identity/client-address.js";
import {
type CreateIdentityConfigOptions,
createIdentityConfig,
IdentityConfigError,
} from "../identity/config.js";
import { TRUSTED_CLIENT_ADDRESS_HEADER } from "../identity/runtime.js";
import { identityAdapterSchema } from "../identity/schema.js";
import {
createPostgresMigrationDatabase,
@@ -44,6 +50,7 @@ export type DoctorCheckId =
| "postgresql.connection"
| "postgresql.plan"
| "identity.configuration"
| "identity.client-address"
| "object-storage.integration";
export type DoctorCheckStatus = "failure" | "pass" | "skipped" | "warning";
@@ -64,11 +71,13 @@ export interface DoctorReport {
export interface DoctorIdentityConfigurationOptions {
readonly baseUrl?: string | undefined;
readonly environment?: CreateIdentityConfigOptions["environment"];
readonly publicSignupEnabled?: string | undefined;
readonly secret?: string | undefined;
readonly trustedOrigins?: readonly string[] | undefined;
}
export interface DoctorOptions {
readonly clientAddress?: ClientAddressConfigInput | undefined;
readonly databaseUrl?: string | undefined;
readonly identityConfiguration?: DoctorIdentityConfigurationOptions | undefined;
readonly moduleDiscovery: ModuleDiscoveryOptions;
@@ -363,6 +372,7 @@ function identityConfigurationCheck(
const hasAnyValue =
input?.secret !== undefined ||
input?.baseUrl !== undefined ||
input?.publicSignupEnabled !== undefined ||
(input?.trustedOrigins?.length ?? 0) > 0;
if (!hasAnyValue) {
@@ -373,9 +383,10 @@ function identityConfigurationCheck(
}
try {
createIdentityConfig(
const config = createIdentityConfig(
{
baseUrl: input?.baseUrl,
publicSignupEnabled: input?.publicSignupEnabled,
secret: input?.secret,
trustedOrigins: input?.trustedOrigins,
},
@@ -383,6 +394,9 @@ function identityConfigurationCheck(
);
return freezeCheck("identity.configuration", "pass", "Identity configuration valid", [
schemaDetail,
config.publicSignupEnabled
? "Public signup is explicitly ENABLED."
: "Public signup is disabled (safe default).",
]);
} catch (error) {
const code =
@@ -394,6 +408,41 @@ function identityConfigurationCheck(
}
}
/**
* Pure configuration validation only, mirroring `identityConfigurationCheck`: never resolves a real
* peer address or reads a real request. Omitted configuration is a `pass`, not a skip — direct mode
* (trusting only the actual immediate socket peer) is itself a complete, safe default, unlike
* identity configuration, which has no meaningful default at all. An explicitly configured
* trusted-proxy mode reports its header name and trusted-proxy count; an invalid or contradictory
* combination is a `failure`, distinguishing the three states this pass's audit specifically calls
* for: safe direct mode, explicitly configured proxy mode, and an invalid configuration.
*/
function clientAddressCheck(input: ClientAddressConfigInput | undefined): DoctorCheck {
try {
const config = createClientAddressConfig(input, TRUSTED_CLIENT_ADDRESS_HEADER);
if (config.mode === "direct") {
return freezeCheck("identity.client-address", "pass", "Direct client-address mode", [
"Trusts only the actual immediate socket peer for rate limiting; no forwarded header is trusted.",
]);
}
return freezeCheck("identity.client-address", "pass", "Trusted-proxy client-address mode", [
`Trusts header "${config.header}" only from a request whose immediate socket peer matches one of ${config.trustedProxies.length} configured trusted ${plural(config.trustedProxies.length, "proxy")}.`,
"The reverse proxy must overwrite (never append to) this header, and the origin must not be reachable except through it.",
]);
} catch (error) {
const code =
error instanceof ClientAddressConfigError
? error.code
: "CLIENT_ADDRESS_CONFIGURATION_INVALID";
return freezeCheck(
"identity.client-address",
"failure",
"Client-address configuration invalid",
[`[${code}] Configured client-address values failed validation.`],
);
}
}
function skippedPostgresChecks(detail: string): readonly [DoctorCheck, DoctorCheck] {
return [
freezeCheck("postgresql.connection", "skipped", "Connection check skipped", [detail]),
@@ -548,6 +597,7 @@ export async function diagnoseEkklesia(
}
const identityCheck = identityConfigurationCheck(options.identityConfiguration);
const clientAddressDoctorCheck = clientAddressCheck(options.clientAddress);
const storageCheck = freezeCheck("object-storage.integration", "skipped", "Check unavailable", [
"Core storage integration has not been implemented yet.",
@@ -563,6 +613,7 @@ export async function diagnoseEkklesia(
postgresConnectionCheck,
postgresPlanCheck,
identityCheck,
clientAddressDoctorCheck,
storageCheck,
]);
}