mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Until now the only ways back into an account were an admin setting your password for you
or shell access to run scripts/reset-admin.js. A self-hosted operator who forgot their
password had no path at all, and the admin-reset route explicitly refuses to reset a
platform admin's password — so a single-admin instance was unrecoverable without a shell.
The per-account login lockout added recently makes that sharper: a user who forgets their
password will hit the lockout and see the same generic error, with no way out.
Two unauthenticated endpoints (they must be — the user cannot log in):
POST /api/auth/forgot-password { email } -> always the same 200
POST /api/auth/reset-password { token, password } -> 200 / 400
The properties that matter, each covered by a test:
- NO ENUMERATION. The request endpoint answers identically — same status, same body —
for a real address, an unknown one, an SSO identity with no local password, and a
malformed string. The frontend shows the same confirmation even on a network error,
so the client cannot leak what the server refused to.
- NO MFA BYPASS. Completing a reset does NOT issue a session; the user signs in
afterwards, so a TOTP-enabled account still clears its second factor. Returning a token
here would turn "read one email" into a full session without the second factor.
- SINGLE USE, SHORT LIVED. 32 random bytes, stored only as a SHA-256 hash (same
discipline as email verification, recovery codes and API tokens), 1h TTL, and the
redeeming UPDATE is conditioned on the hash still being present so concurrent
redemptions cannot both win.
- LOCAL ACCOUNTS ONLY. SSO identities have no local password; no token is minted.
- IT ACTUALLY UNBLOCKS YOU. A completed reset clears the per-account login lockout and
must_change_password, otherwise someone who locked themselves out would reset and still
be locked out.
Rate limited: 5/min on the request (it sends mail to a caller-supplied address), 10/min
on the redeem. If no email transport is configured the response is unchanged — no oracle —
but the server logs loudly, because the user will otherwise wait for mail that cannot
arrive and the generic response cannot tell them.
Frontend: a "Forgot your password?" link on the sign-in card, a request card, and a
new-password card. app.js had to learn #/reset-password explicitly — the auth guard
rewrites any unauthenticated hash to #/login, which would have discarded the one-time
token in the emailed link and made it silently do nothing.
Migration adds users.password_reset_hash / password_reset_expires: additive, nullable,
idempotent; a code-only rollback leaves two dead columns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| activity.js | ||
| admin-player-debug.js | ||
| admin.js | ||
| billing.js | ||
| content-library.js | ||
| dashboard.js | ||
| designer.js | ||
| device-detail.js | ||
| force-password-change.js | ||
| help.js | ||
| kiosk.js | ||
| layout-editor.js | ||
| login.js | ||
| no-workspace.js | ||
| onboarding.js | ||
| playlists.js | ||
| reports.js | ||
| schedule.js | ||
| settings.js | ||
| teams.js | ||
| video-wall.js | ||
| widgets.js | ||
| workspace-members.js | ||