Skip to content

feat(admin-auth): sign in with a password only and remove admin 2FA - #27

Open
kris1027 wants to merge 19 commits into
mainfrom
feat/defer-analytics
Open

kris1027 wants to merge 19 commits into
mainfrom
feat/defer-analytics

Conversation

@kris1027

Copy link
Copy Markdown
Owner

What

Admins now sign in to /admin with email and password only. The TOTP step (authenticator app codes) is gone, and so is the admin password reset by email: an admin password is now reset only from the terminal with the new pnpm admin:reset-password. The branch also moves "Analytics & error tracking" out of the launch scope into the deferred list.

Why

Implements spec 0019 (rationale in rationale.md). It supersedes the TOTP and admin reset parts of spec 0004 and the admin branch of spec 0012 AC-8 and AC-19.

With the password as the only lock, an inbox alone must never be able to change it or open the panel. So the email reset goes, the guard requires a session that was opened by a password, and admin passwords get stricter.

Changes

Password only guard

  • decideAdminAccess drops the assurance level check and the mfa verdict. An admin session must now carry a password entry in its amr list (both the string and the { method } shape are read, via the new hasPasswordMethod). A session opened by an email link (recovery, otp, sign up confirm) is treated like no session.
  • The guard reads amr from getClaims(), so no auth.mfa API is used anymore.
  • The proxy now answers a real redirect to /admin/sign-in for a password less session on page loads, instead of letting the page's redirect() answer 200.
  • signIn goes straight to the validated next path or /admin. safeAdminPath and publicAdminPages list /admin/sign-in only.
  • Unchanged: the 12 hour cap, the 404 for non admins and disabled admins, sign out on this device only, admin:create and admin:disable.

TOTP removed

  • Deleted /admin/mfa, the enroll and verify actions, factors.ts, mfa-lockout.ts, mfa-view.ts, the TOTP form components, admin:reset-mfa, the MFA log events and messages, and their tests (including tests/stack/mfa-challenges.stack.test.ts).
  • supabase/config.toml sets TOTP enroll and verify to false. The otpauth dev dependency is gone.
  • The e2e helpers now sign an admin in with a password only (signInAsAdmin), which touches most admin and catalog specs.

Admin email reset replaced by a terminal script

  • New pnpm admin:reset-password --email <e> [--env-file <path>]: finds the admin in admin_users (enabled or disabled), prompts twice for a hidden password, and sets it with auth.admin.updateUserById. Supabase then ends every session the admin had; a stack test pins that.
  • Deleted /admin/forgot-password, /admin/reset-password, /auth/confirm (now 404, and the proxy no longer runs on /auth/*), the admin reset action, the admin_reset email kind and emails/admin-reset-password.tsx.
  • The sign in page has no "Forgot password?" link. Its reason messages are only expired and signed_out; old keys show nothing.

Closing the inbox path for store runners

  • runsTheStore checks admin_users only (no more auth.mfa_factors read).
  • The Send Email Hook answers 200 and sends nothing for a store runner's recovery email, logging auth_email.skipped with reason: "runs_the_store". The customer forgot password page replies the same way, at the same speed.
  • confirmEmail ends the session and redirects to ?failed=1&type=recovery if an old recovery link for a store runner still arrives.

Stronger admin passwords

  • New adminPasswordSchema: the shared password rule plus at least 14 characters. Both admin scripts use it.
  • admin:create on an existing auth user (for example a customer) now always prompts for and sets a new password with email_confirm: true, so a password planted through public sign up can never become an admin password.

Docs

  • docs/runbooks/admin-accounts.md: sign in without TOTP, admin:reset-password, the 14 character rule, the compromised account steps, deleting existing factors, the hook check, and the Firewall rule on POST /admin/sign-in and POST /account/sign-in.
  • Status notes on specs 0004 and 0012, README.md, src/features/account/AGENTS.md, the scope, the spec 0019 verify steps and the review record.

How to test / verify

  • Restart the local stack once so it picks up TOTP off: pnpm db:stop && pnpm db:start.
  • pnpm lint && pnpm typecheck && pnpm test
  • pnpm test:stack: admin-amr.stack.test.ts shows a password sign in has a password amr entry and a verified recovery link has none; admin-scripts.stack.test.ts covers the reset (enabled and disabled admins, customer only email refused, 13 characters rejected, every session ended) and admin:create overwriting a planted password.
  • pnpm test:db and pnpm test:e2e
  • By hand, the steps in verify.md. The main ones: sign in at /admin/sign-in lands on /admin with no code step; /admin/mfa, /admin/forgot-password and /admin/reset-password redirect a visitor to sign in and 404 for an admin; /auth/confirm is 404; /account/forgot-password with an admin email shows the usual reply and Mailpit stays empty.

Risk & rollout

  • No database migration and no new env vars. Nothing is deployed yet.
  • Accepted risk (spec 0019): a leaked, phished or guessed admin password now opens the panel. Until the Vercel Firewall rule is live, brute force protection is only Supabase's shared rate limit.
  • Production steps before the first admin exists (scope feature 19, runbook): delete existing TOTP factors, turn TOTP enroll and verify off in the production Supabase project, apply the Firewall rule on both sign in paths (10 per minute per IP, deny 10 minutes), and check the Send Email Hook is on (a recovery request for an admin email must send nothing).
  • Locally, leftover dev factors can be removed with DELETE FROM auth.mfa_factors; or a stack reset.
  • Rollback: revert the merge. Deleted factors cannot come back, so each admin would enroll again at the next sign in.
  • Review (docs/reviews/2026-10-27-feat-defer-analytics.md): approve with nits. The stale account AGENTS.md lines and the double admin lookup in the reset script are fixed on this branch; the remaining nits are cosmetic.

Notes for reviewers

  • The branch name (feat/defer-analytics) is older than its main content: only the first commit is the analytics deferral, the rest is spec 0019.
  • Worth extra eyes: the amr rule in src/features/admin-auth/access.ts and the proxy redirect in src/features/admin-auth/proxy-decision.ts, since they now carry what TOTP used to.
  • admin:reset-password relies on Supabase ending every session on a password change. If a future Supabase version stops doing that, the stack test fails, and the runbook's fallback is to wait 12 hours before re-enabling a compromised admin.
  • One verify step (AC-12, no auth.mfa, auth.reset.requested or auth.password.changed left in src) is still unticked in verify.md; the grep returns nothing today.

🤖 Generated with Claude Code

kris1027 and others added 12 commits October 27, 2026 13:47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…word

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a failed recovery sign out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI already finds the admin before prompting, so it now passes that id to
the new setAdminPassword instead of looking the email up a second time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ticks the verify steps and the scope boxes, and adds the review findings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kris1027 kris1027 added feature New user-facing functionality auth Sign-in, sign-out, sessions, and account recovery security Authorization, secret protection, and vulnerability fixes labels Oct 27, 2026
@kris1027 kris1027 self-assigned this Oct 27, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in BeStore Oct 27, 2026
@kris1027 kris1027 moved this from Backlog to In review in BeStore Oct 27, 2026
kris1027 and others added 7 commits October 27, 2026 15:05
… and stack test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n:reset-password end to end

findAdminId and disableAdmin return null instead of throwing; the scripts turn that into
the same "No admin has that email." exit. resetAdminPassword, which only the tests called,
is gone: the stack test now runs the real script with piped passwords, including the 13
character and mismatch refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… type

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not expired

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also ticks the AC-12 grep step in the 0019 verify list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Sign-in, sign-out, sessions, and account recovery feature New user-facing functionality security Authorization, secret protection, and vulnerability fixes

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

1 participant