Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
5.1 KiB
Self-serve forgot-password — gap & recommended design
Assessment date: 2026-08-08
Verdict: Out of scope for a minimal safe reuse. Document only until a dedicated reset feature is approved.
Mail infra: Existing SMTP/mail.Send is enough; do not build a new mail stack. Confirm product copy/TTL/rate limits before implementing.
Related: status-and-gaps.md, ux-backlog.md (P0-10 set-password), live-auth-security.md, migration-readiness.md.
What exists today (not forgot-password)
| Piece | Role | Why it cannot serve “I forgot my password” |
|---|---|---|
must_set_password + auth.SetPassword |
First password after migration / bootstrap | Update requires must_set_password = true; returns ErrPasswordAlreadySet otherwise |
ReissueSetPasswordInvite |
Admin re-issue durable invite | Requires must_set_password + active membership; ineligible for accounts that already set a password |
AcceptInvite |
Invite / first-password accept | Existing users with password must verify current password — does not reset |
HMAC IssueSetPasswordToken + handleCompleteSetPassword |
Token complete for first-set | Calls SetPassword → same must_set_password gate |
Admin POST …/admin/… set-password email |
Operator-only bulk/single send | AuthZ + eligibility same as above; login CTA points here (no public resend) — see P0-10 |
mail.SetPasswordMessage / MigratedSetPasswordMessage |
Email copy + /accept-invite links |
Templates/links are first-set oriented; reusable patterns only |
| Login UI | password_not_set CTA → /accept-invite + admin Users |
No “Forgot password?” link |
Operator workarounds today: platform admin re-issues set-password for must_set_password users; local/dev ForceSetPassword / migrator -set-password for bootstrap. Neither is self-serve recovery for users who already know they had a password.
Why a “small patch” is unsafe
Reusing invite/HMAC set-password for established accounts would require relaxing must_set_password (or calling ForceSetPassword from a public token path). That collapses first-set and reset semantics, weakens single-use guarantees for HMAC tokens (no DB row until success), and risks account takeover if the public request endpoint is naively bolted on.
Intentional product gap (Wave 8): no public self-serve resend for set-password (P0-10).
Recommended design (when approved)
Reuse mail delivery and rate-limit patterns from admin set-password; add a separate reset purpose. Do not overload must_set_password / AcceptInvite.
API (sketch)
-
POST /api/auth/forgot-password{ "email": "…" }- Always return the same opaque success (anti-enumeration).
- Rate-limit by IP + normalized email (mirror admin set-password limiters).
- If active user with deliverable email: issue reset token (prefer durable hashed row, invite-style; HMAC-only only if single-use store is added).
- Skip synthetic
@legacy.localsilently. - Send
ForgotPasswordMessage(new; link to/reset-password?token=…, not accept-invite first-set mode).
-
POST /api/auth/reset-password{ "token", "password" }- Validate token; set new argon2id hash for that user regardless of
must_set_password(dedicatedResetPassword, notSetPassword/ not publicForceSetPassword). - Invalidate token; optionally revoke other sessions.
- Clear
must_set_passwordif still set.
- Validate token; set new argon2id hash for that user regardless of
Web
- Login: “Forgot password?” →
/forgot-password. /forgot-password: email form + generic confirmation copy./reset-password: password + confirm; strip?token=from URL like accept-invite.
Security checklist
- Constant-time / uniform responses and timing where practical.
- Token TTL short (e.g. 1h); one-time consume.
- CSRF on cookie-authenticated POSTs; public forgot/reset still need CSRF if under same cookie middleware.
- No token in list APIs or HTML; prefer
#token=/ exchange code later (live-auth-security.md residual). - Tests: happy path, expired/reuse, unknown email response shape, rate limit, inactive user, synthetic email.
Explicitly out of this design
- New mail provider, queue, or template engine (extend
internal/mail). - Turning admin set-password into a public endpoint.
- Logged-in “change password” (optional follow-up; separate from forgot).
Implementation status
| Item | Status |
|---|---|
| Self-serve forgot / reset | Implemented (041_password_reset_tokens, POST /api/auth/forgot-password, POST /api/auth/reset-password, /forgot-password + /reset-password, i18n) |
| Migration first-set + admin re-issue | Implemented (P0-10) |
| Mail SMTP send path | Implemented; forgot-password reuses mail.Send + EMAIL_DRY_RUN / ApplyDryRun |
ASSUMPTION: Opaque {status:"ok"} responses; TTL 1h; IP 10/min + email 3/hour limiters; no session revoke on reset (optional follow-up).