trackslash
TRACK-100 P2 For an agent

Let people reset a forgotten password by email

0
All issues

Description

Let someone who has forgotten their password get a reset link by email.

Depends on TRACK-96. That issue adds SMTP sending (SMTP2GO's free plan) and verified addresses. A reset only ever goes to a verified address.

Where things stand (checked 2026-09-28)

  • Sign-in. Password sign-in takes a username and password (POST /login → uiLogin → store.AuthenticatePassword). The form sits inside the "Log in with password" disclosure on login.html (details[data-password-login]).
  • No way back in. Someone who has lost their password and has no passkey can't get back in.
  • Accounts without a password. Some accounts have no password (they signed up with a passkey). Others have password sign-in turned off. Both are tracked by PasswordLoginState (HasPassword, Enabled, ActivePasskeys) in store/settings.go.
  • Pieces to reuse:
    • store.ValidatePassword (at least 12 characters), and bcrypt at the default cost, as ChangePassword does.
    • RevokeSessionAuthTokensForUser, which ends every web session. A password change already signs other browsers out (uiUpdatePassword).
    • timingDecoyPasswordHash in store/accounts.go. Sign-in already hides whether a user exists through response timing.
    • Pre-sign-in routes use uiPreAuthCSRFMiddleware and authIPRateLimited. Per-account budgets use allowAuthIdentifier (auth_rate_limit.go).
    • Referrer-Policy: no-referrer is already set site-wide, so a token in the URL doesn't leak to other sites.
  • SECURITY_MODEL.md accepted trade-offs:
    • Usernames aren't secret, and sign-up already says "Username already exists".
    • A password change and "Sign out everywhere" end web sessions only. API tokens and connected apps stay.

Flow

  1. Ask.

    • Add a "Forgot password?" link under the password form on /login, shown only when email is configured.
    • /forgot-password asks for a username or an email address.
  2. Give the same answer every time.

    • The POST always shows "If that account has a verified email, we've sent a reset link." It's the same whether or not the account exists, has a verified email, uses only passkeys, or has been rate-limited.
    • Don't send inline here, unlike TRACK-96. Respond first, then send in a background goroutine with its own timeout. Otherwise the response time would show whether an email was sent.
    • This is still no queue. A lost send just means the person asks again.
  3. The email.

    • It goes to the verified address only, as plain text plus minimal HTML.
    • It links to TRACK_SLASH_PUBLIC_ORIGIN/reset-password?token=….
    • It says the link expires in 30 minutes, and "If you didn't ask for this, ignore this email. Your password hasn't changed."
  4. The token.

    • 32 random bytes, of which only a SHA-256 hash is stored.
    • Single use, expiring after 30 minutes.
    • Asking again, or changing the password any other way, cancels older tokens.
  5. Opening the link.

    • GET /reset-password only shows the form (new password, entered twice) and never uses up the token.
    • This matters because mail scanners such as Outlook Safe Links open links before the person does.
  6. Setting the password. POST /reset-password:

    • checks the token;
    • applies ValidatePassword;
    • stores the bcrypt hash and marks the token used;
    • ends every web session (RevokeSessionAuthTokensForUser);
    • redirects to /login with "Password updated. Sign in with your new password."

    Don't sign the person in automatically.

  7. The notice. Send "Your Trackslash password was reset" to the same address.

Rate limits

  • Per IP: the existing authIPRateLimited.
  • Per account: at most 3 reset emails an hour. Beyond that, sending is silently skipped and the page gives the same answer.
  • Together these also keep us inside SMTP2GO's 200 emails a day.

Open decisions

  • Passkey-only and password-off accounts. Recommended: respect the account's choice.
    • The email says the account signs in with a passkey and links to /login, with no reset link.
    • An inbox should never be able to add a password to an account that chose passkeys only.
    • The alternative is to let the link add a password. That's friendlier, but it turns the inbox into a way into those accounts.
  • Also revoke API tokens and connected apps on reset? Recommended: no, matching a password change and SECURITY_MODEL.md. The notice email points to Settings → Tokens instead.
  • Accept an email address as well as a username on the form? Recommended: yes. The lookup matches verified addresses only, ignoring case.

Out of scope

  • Account recovery for someone with no working passkey and no verified email.
  • Resets started by an admin.
  • API or MCP reset endpoints. Reset is browser-only, and connectors can't touch credentials (SECURITY_MODEL.md).

Done when

  • With email on: a user with a verified address can reset from the login page and sign in with the new password. Every other session is signed out, and the notice arrives.
  • With email off: there's no "Forgot password?" link and the routes return 404.
  • No leaks: an unknown user, an unverified email, a passkey-only account and a rate-limited request all get the identical page, with similar response times.
  • Scanner-safe: a GET of the link doesn't use up the token.
  • Bad links: an expired, used or cancelled token shows "This link has expired. Ask for a new one." with a link to /forgot-password.
  • Tests: integration tests cover all of the above using the capturing mailer, and check that tokens are stored only as hashes.
  • SECURITY_MODEL.md says:
    • a reset goes to a verified email only;
    • it uses a single-use 30-minute token;
    • it ends web sessions;
    • what happens to API tokens, connected apps and passkey-only accounts, as decided above.

GitHub

0

No branches or pull requests linked.

Comments

0
No comments.