fix: fail closed on production signup
Production configuration accepted publicSignupEnabled: true and started a working Identity/API runtime, even though Ekklesia has no verified email, invitation requirement, CAPTCHA, or durable shared/edge limiter — Doctor merely reported "Public signup is explicitly ENABLED" as a pass. validatePublicSignupEnabled now takes the same IdentityEnvironment the rest of this config validates against and throws the stable PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION whenever the parsed value is true in production. Parsing stays separate from the policy, so a malformed value still reports PUBLIC_SIGNUP_ENABLED_INVALID everywhere. Because the executable entry point validates configuration before startApi, such a deployment fails before any pool, Better Auth instance, module runtime, or listener exists, and Doctor reports NOT READY via its existing IdentityConfigError path. Development and test composition still enable it explicitly through the identical typed path. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d3c6bd73d3
commit
1161f5a076
@@ -76,12 +76,32 @@ is never consulted at all. The in-memory rate-limit store remains per-process (`
|
||||
runtime.ts`, unchanged from ADR 0011) — this pass does not add distributed storage; a genuinely
|
||||
multi-instance deployment still needs a shared or edge limiter, reserved for pass 5.
|
||||
|
||||
### Public signup is disabled by default
|
||||
### Public signup is disabled by default, and cannot be enabled in production at all
|
||||
|
||||
`IdentityConfig.publicSignupEnabled` (`core/src/identity/config.ts`) is a strict boolean:
|
||||
`undefined` input means `false` (disabled); only an explicit boolean or the exact strings
|
||||
`"true"`/`"false"` are accepted, so an absent, misspelled, or loosely parsed environment variable
|
||||
can never enable it. `core/src/identity/runtime.ts` wires this directly to Better Auth's own
|
||||
can never enable it.
|
||||
|
||||
**Production refuses enablement outright.** `validatePublicSignupEnabled` takes the same
|
||||
`IdentityEnvironment` the rest of this config already validates against, and throws the stable
|
||||
`PUBLIC_SIGNUP_UNSUPPORTED_IN_PRODUCTION` error whenever the parsed value is `true` in production.
|
||||
The durable signup-abuse prerequisites a public registration endpoint requires — verified email or
|
||||
an invitation requirement, plus durable shared/edge throttling rather than this release's
|
||||
per-process in-memory limiter — do not exist in this repository, so there is no production
|
||||
configuration in which the flag could be honored safely; permitting a deliberate opt-in would be
|
||||
offering a switch whose safe position does not exist yet. Parsing is kept separate from this policy
|
||||
so a malformed value still reports `PUBLIC_SIGNUP_ENABLED_INVALID` in every environment, and an
|
||||
operator who typo'd the flag is never told the misleading "unsupported in production" story
|
||||
instead. Because `apps/api`'s executable entry point calls `createApiRuntimeConfig` *before*
|
||||
`startApi`, such a deployment fails before any PostgreSQL pool, Better Auth instance, module
|
||||
runtime, or listener exists. `pnpm doctor` surfaces the same state as a blocking `NOT READY`
|
||||
failure (via its existing `IdentityConfigError` path), never a warning. Development and test
|
||||
composition may still enable it explicitly through the identical typed path — that is how this
|
||||
repository's own integration suite creates accounts over HTTP. When the prerequisites are actually
|
||||
implemented and proven, this one function is the single place that changes.
|
||||
|
||||
`core/src/identity/runtime.ts` wires the validated flag directly to Better Auth's own
|
||||
`emailAndPassword.disableSignUp` flag: when disabled, `POST /sign-up/email` rejects every call
|
||||
with a fixed `400 {"code":"EMAIL_PASSWORD_SIGN_UP_DISABLED"}` before Better Auth ever looks up the
|
||||
email, so a disabled deployment cannot leak whether an address is already registered, and
|
||||
@@ -92,9 +112,10 @@ platform-owned check, since the pinned release's own mechanism already satisfies
|
||||
property (no email-existence leak, no business logic invoked, a stable code). Since email/password
|
||||
is the only enabled provider (ADR 0011), there is no second signup entry point to separately guard.
|
||||
|
||||
This amends ADR 0011's framing ("Anyone may create an Ekklesia account") for the production
|
||||
default only — public registration remains the intended eventual product policy, and development
|
||||
or test composition may still enable it explicitly (`publicSignupEnabled: true`); ADR 0011's
|
||||
This amends ADR 0011's framing ("Anyone may create an Ekklesia account") for production entirely —
|
||||
public registration remains the intended eventual product policy, but is currently reachable only
|
||||
in development/test composition (`publicSignupEnabled: true` with `environment: "development"`);
|
||||
ADR 0011's
|
||||
architectural description of what a registered account *is* (an authentication identity, not a
|
||||
membership record) is otherwise unchanged and still authoritative. This pass adds no email
|
||||
verification, CAPTCHA, invitation, or durable abuse control — those remain explicitly deferred
|
||||
@@ -198,8 +219,9 @@ Direct mode (the default) trusts only the real socket peer for every request; ex
|
||||
trusted-proxy mode requires a header name and a concrete, non-empty trusted-proxy list, validated
|
||||
the same way in `core/src/identity/client-address.ts` and surfaced by Doctor
|
||||
(`identity.client-address`), which distinguishes safe direct mode, an explicitly configured proxy
|
||||
mode, and an invalid configuration. Public signup is closed by default everywhere except where a
|
||||
deployment explicitly opts in. Every body-consuming public route — auth, Core, and every module
|
||||
mode, and an invalid configuration. Public signup is closed by default everywhere, and production
|
||||
cannot open it at all until the durable abuse-control prerequisites exist — only development and
|
||||
test composition may enable it. Every body-consuming public route — auth, Core, and every module
|
||||
route — now has an explicit, enforced byte ceiling with one sanitized `413` contract. The Node
|
||||
server itself now bounds a slow-header or slow-body connection instead of relying on Node's
|
||||
generous multi-minute defaults. None of this constitutes distributed rate limiting, a durable
|
||||
|
||||
Reference in New Issue
Block a user