Repository navigation
feat(backend)!: close public sign-up and require a one-time setup token - #149
Conversation
Public sign-up is closed. Better Auth's databaseHooks.user.create.before refuses every user creation (email/password, social, plugins) with 403 SIGNUP_DISABLED, and a hooks.before on /sign-up/email refuses it before the endpoint runs. Social providers get disableSignUp, so social login only signs in or links existing accounts. The first admin comes only from the setup wizard: createFirstAdmin runs signUpEmail inside runAsServerUserCreation, an AsyncLocalStorage mark that no HTTP request can set. With no user at boot, the backend prints a one-time setup token to the log and keeps only its SHA-256 in memory. POST /api/v1/setup/admin requires it (setupToken) and the token is consumed when the admin is created. THINKER_ALLOW_SIGNUP=true (provisional, removed in v0.3) reopens sign-up once the first admin exists. GET /api/v1/setup/status exposes signupEnabled. Closes #29 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Setup: one-time setup token field (help points at the "one-time setup token" server log line), sent as setupToken; friendly message on 403. - setup-status: signupEnabled (false unless the backend says true), SetupError carries the HTTP status. - Register redirects to /login when sign-up is disabled; Login hides the sign-up link and explains signup_disabled / unable_to_create_user social callback errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of PR #149: close public sign-up and require a one-time setup tokenReviewed Blockers
Security analysis (no bugs found)
Should-fix
Nit
TestsThe e2e tests in CI on the SHALint, Typecheck, Build, Unit backend/shared/cli, both Backend e2e jobs, Docker build amd64/arm64, install.sh, CodeQL and Changeset all pass. Failures: Unit (frontend) and Compose smoke test (read-only). High riskEven after the blockers are fixed, the PR changes auth (closes sign-up, adds the setup token): risco alto per ADR 0002. Fixing the smoke test also touches Head revisado: 206e466 |
- docker-smoke: read the one-time setup token from the container logs and send it to the setup wizard endpoint. - install.sh: tell the operator where the setup token is printed. - Frontend tests that mock setup-status now provide checkSignupEnabled. - createFirstAdmin requires the setup token before completing a half-created (unverified) admin too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/dev/manifest-guide.md
Re-review of PR #149 (closed sign-up + setup token)What I reviewed:
Previous blockers: resolved
Previous should-fix items
Merge with main
New findingsNone. The security analysis from the previous review still holds: hooks on every creation path, a 192-bit token kept as a hash, constant-time comparison, single use guaranteed by CI on 00ed9e3All jobs passed, including Compose smoke, Unit frontend/backend, both e2e jobs, Docker amd64/arm64, install.sh, CodeQL and Lint/Typecheck/Build. qa-dockerThe PR body says qa-docker is required, but no run is reported yet. It is a merge gate. High riskThese criteria apply:
The maintainer decides the merge (ADR 0002). Any authorization from the maintainer has to come from the maintainer directly, not through an agent. Head revisado: 00ed9e3 |
|
The multi-replica setup-token limitation (docs should-fix) is tracked in #152. |
QA Docker (qa-docker): PR #149Image
Finding (should-fix)
MinorThe setup page hint says Tested SHA: 00ed9e3 |
Summary
This is the backend half of #29, following the maintainer's 2026-10-10 decision on the issue: minimal lock in v0.1, a one-time setup token printed in the logs, and the provisional
THINKER_ALLOW_SIGNUPwith defaultfalse.auth/signup-policy.ts,auth/auth.instance.ts)databaseHooks.user.create.beforerefuses every user creation (email/password, social, plugins) with403 { code: "SIGNUP_DISABLED" }.hooks.beforeon/sign-up/emailrefuses earlier. It is needed because, withrequireEmailVerificationon, Better Auth 1.7 turns a 403 thrown during user creation into a fake success (anti-enumeration).disableSignUp, so social login still signs in and links existing accounts but never creates one.createFirstAdminrunsauth.api.signUpEmailinsiderunAsServerUserCreation, anAsyncLocalStoragemark that only in-process code can enter. No header or body can set it. A directPOST /api/auth/sign-up/emailon an empty database is refused. The dev-only seeder (SEED_DATA) uses the same mark.setup/setup-token.service.ts)No admin account exists yet. Open /setup and enter this one-time setup token: <token>.POST /api/v1/setup/admintakessetupToken. A missing or wrong token gets 403, compared in constant time. If no token is pending (for example, the database was wiped after boot), a new one is printed.9001, two concurrent setups still yield exactly one admin (one 201, one 409).THINKER_ALLOW_SIGNUP=true(provisional, removed in v0.3) reopens email and social sign-up, but never for the first account.GET /api/v1/setup/statusnow includessignupEnabled. It istrueonly when the valve is open and setup is done.docs/dev/manifest-guide.md(Better Auth Setup and env vars) andpackages/backend/.env.example.Frontend change needed (not in this PR; the frontend agent is on #25 and owns these files)
Until this lands the dashboard wizard cannot complete, because it does not send the token. Merge this together with, or right after, the frontend PR.
pages/Setup.tsx: add a required "Setup token" field with this hint: "Printed in the server log:docker compose logs thinker". Send it assetupTokeninPOST /api/v1/setup/admin. On 403, show the server message.services/setup-status.ts: addsignupEnabled: booleanto the status type.pages/Register.tsx: redirect to/loginwhensignupEnabledis false.pages/Login.tsxandcomponents/SocialButtons.tsx: hide the "Create account" links whensignupEnabledis false.error=signup_disabled(provider sign-up disabled) orunable_to_create_user(hook refusal). The login page should show a friendly "Sign-up is closed on this instance" message.Also out of scope here:
docker/install.shand the PT-BR install docs should tell operators where to find the token. Those files are owned by devops and docs-ptbr, and release notes are owed indocs/releases/0.1.0.md.Tests
signup-policy.spec.ts: valve parsing, default refusal without a DB query, first user refused even with the valve, the trusted mark scoped across awaits.setup-token.service.spec.ts: boot issue/skip, verify, consume, re-issue.setup.service.spec.ts: user created inside the trusted context, token consumed, 403 for a missing or wrong token with a re-print, token kept on failure,isSignupEnabled.setup.controller.spec.ts:signupEnabled.auth.instance.spec.ts: hooks refuse by default,/sign-up/emailmiddleware, trusted context allowed, valve semantics, socialdisableSignUp.test/signup-lock.e2e-spec.ts(new) boots the realsrc/main.tstwice, default andTHINKER_ALLOW_SIGNUP=true, on one disposable DB:[201, 409]and one verified admin;signupEnabledis reported correctly throughout.test/setup.e2e-spec.ts: status keys includesignupEnabled; 403 without a token or with a wrong one; 400 for a non-string token.Local runs:
npm run lint(0 errors),npm run typecheck,npm run build: passnpx jest(backend): 477 suites, 9111 tests passedTZ=UTC npm run test:e2e:docker --workspace=packages/backend: 72 suites, 612 tests passedqa-docker: required (runtime and auth behavior changed). The acceptance criteria in #29 are:
POST /api/auth/sign-up/emailbefore setup is refused;Until the frontend change above lands, run the setup step through the API.
Closes #29
Dashboard (included in this PR)
/setup: new Setup token field, with help text pointing at the server log line "one-time setup token" (e.g.docker compose logs thinker). Sent assetupTokentoPOST /api/v1/setup/admin; a 403 shows a friendly "token invalid or missing" message.services/setup-status.ts:signupEnabled(false unless the backend reportstrue),checkSignupEnabled(), andSetupErrorcarrying the HTTP status./registerredirects to/login(query kept) when sign-up is disabled./loginhides the "Sign up" link when sign-up is disabled and shows a friendly message for the social callback errorssignup_disabledandunable_to_create_user. (SocialButtonshas no sign-up link, so it needed no change.)🤖 Generated with Claude Code