Skip to content

Forgot password says what happened; set-up email for people with no account here - #464

Merged
nambok merged 1 commit into
mainfrom
forgot-password-setup
Nov 4, 2026
Merged

nambok merged 1 commit into
mainfrom
forgot-password-setup

Conversation

@nambok

@nambok nambok commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

A person copied over from hosted CoderHelm (on the install's team, no sign-in account here) pressed Forgot password. The user pool had no user for that email, so Cognito sent nothing, the page implied a code was coming, and they waited for an email that never existed.

Now, on a self-hosted install, forgot-password says what happened for that email and sends the set-up email to people who have no account yet. The sign-in page also names the sender to look for. Admins get a bulk send for everyone who hasn't set up an account.

Changes

  • POST /auth/forgot-password (self-hosted) decides one of these cases with plan_forgot, and forgot_reply gives each one its own status and message:
    • code_sent: has an account here, so Cognito sends a code. "We sent a reset code to ."
    • setup_sent: on the team but no account here, so the existing set-up email goes. "You don't have an account here yet. We sent you a set-up link instead."
    • setup_recent: as setup_sent, but one already went within the hour (the same one an hour, five a week cap as the admin button).
    • invited: points at the invite email. Sends nothing.
    • no_account: "There's no account for on this CoderHelm. Ask an admin to add you." Sends nothing.
    • Limits: 10 an hour per IP and 5 an hour per email, both shared counters. The per-email key is a hash, so no addresses go into the counter table. These sit on top of the existing 20 per 5 minutes on the sign-in routes. The hosted service keeps its single answer for every email.
  • GET /auth/methods (self-hosted) adds senders: {setup, reset_codes}:
    • setup is the install's SES address, or the relay's from address.
    • reset_codes is read from the user pool's EmailConfiguration: no-reply@verificationemail.com for COGNITO_DEFAULT, otherwise the pool's From address or SES identity.
    • Cached for 5 minutes per instance. The gateway role gets cognito-idp:DescribeUserPool on its own pool.
  • POST /api/users/setup-emails (admins, people capability): sends the set-up email to everyone who needs one and skips anyone sent one in the last hour. Limited to 3 an hour per team. Returns the counts and when each email was sent.
  • Set-up email delivery is now one function (deliver_setup_email), used by the row button, the bulk send and forgot-password.
  • GET /api/users now sets needs_setup for every viewer on a self-hosted install, not only admins. That way nobody reads hosted activity as use of this install.
  • Docs: sign-in.md explains the three ways to get the set-up email, the forgot-password cases and limits, and both senders.

Verification

  • cargo fmt --check, cargo clippy -p gateway --all-targets -D warnings, cargo test -p gateway: 707 passed. The new tests:
    • a member with no account gets the set-up email
    • the hourly wait applies from the sign-in page too
    • a non-member, or a domain/listed/first-admin email, sends nothing
    • an account here always gets a reset code
    • an invite sends nothing
    • every case has a distinct status
    • the per-email counter key holds no address
    • the Cognito sender is resolved for default, From and SES-identity configs
    • the bulk send skips anyone emailed in the last hour
    • a self-service email stands on an owner, else an admin
    • the bulk route is in the permissions table
  • infra: npm test, 145 passed.
  • No email sent, nothing deployed.

…ple with no account here

On a self-hosted install, forgot-password now tells the person which case
it was: a reset code went out, they have no account here yet and a set-up
link went instead, they have an invite to use, or the email isn't on this
install. Per-IP and per-email limits keep it from being used to check
addresses quickly.

The sign-in methods endpoint names who sends each email (the install's own
sender, and Cognito's from the pool's EmailConfiguration), so the page can
tell a person what to ask IT to allow. Admins get a bulk set-up email that
skips anyone sent one in the last hour, and every member sees who hasn't
signed in here yet.
@nambok
nambok merged commit 204c677 into main Nov 4, 2026
10 checks passed
@nambok
nambok deleted the forgot-password-setup branch November 4, 2026 19:10
nambok added a commit that referenced this pull request Nov 4, 2026
Security fix. Since #464 forgot-password on a self-hosted install answered
with a different status per case (has an account, on the team without
one, invited, not on this install), so anyone could learn which emails
have accounts or are on the team.

Forgot password now answers the same status, body and headers for every
email on every install, and no sooner than 2.5 seconds after the ask, so
the case can't be timed either: the work (the Cognito reset code, or the
set-up email for a team member with no account here) runs while the
answer waits, and a slow send carries on after it. Send failures are
logged, never answered. The set-up email keeps its once-an-hour limit and
the 10/h per IP and 5/h per email limits stay.

Self-hosted sign-up answers the same way for invited, existing and
refused emails, with the password rules checked for every email first; an
email that signed up but never confirmed gets a fresh code. The app
client now has preventUserExistenceErrors on.

Pins the dashboard to d26225b (CoderHelm/coderhelm #314): the sign-in
page shows one message for every email and names both senders.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant