Skip to content

[ADD] auth: self-service password reset (/auth/forgot-password)#180

Merged
dcorio merged 13 commits into
mainfrom
feat/forgot-password
Jul 17, 2026
Merged

[ADD] auth: self-service password reset (/auth/forgot-password)#180
dcorio merged 13 commits into
mainfrom
feat/forgot-password

Conversation

@beorngb

@beorngb beorngb commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Implements the self-service password reset that the login page already linked to
(/auth/forgot-password was a dead link — 404).

Flow

  • GET/POST /auth/forgot-password → emails a one-time reset link that lands on the
    existing /auth/setup-password route. Reuses the existing token infra
    (generate_token(purpose="reset"), 1h TTL) and Gmail-MCP delivery.
  • Constant-time response: the POST always returns the same generic page and does
    all lookup/token/email work in a BackgroundTask, so timing doesn't reveal
    whether an address is registered.
  • Rate limited per IP and per account (the per-account key is the real
    defence, since the per-IP limit trusts a proxy header).
  • Eligibility: only active users with an existing password can reset — a reset
    restores access, it never grants portal access to a bot-only account. 2FA is
    untouched.

Also in here

  • Partial unique index on lower(email) (migration 013) so duplicate emails
    are impossible; admin user create/update now handle the violation gracefully
    (and the previously-silent error codes on that page now render).
  • Fix: generate_token used ("used_at", "=", None), which is SQL = NULL and
    matched nothing — prior tokens were never invalidated. Now IS NULL.
  • Fix: reset link uses GRIDBEAR_BASE_URL instead of the request host.
  • setup_password form now shows the real min length (12, was a stale 8).
  • send_invite_email takes subject/ttl_hours (invite callers unchanged).
  • Test-isolation fix: a module-level sys.modules["webauthn"] = MagicMock() stub
    leaked across the suite; now only stubs when genuinely absent.

Tests — unit (routes, rate limit, reset logic, email params) + integration
(migration/idempotency, duplicate-email rejection, token invalidation). Verified
end-to-end against a live instance.

Follow-ups (not blocking)

  • MIN_PASSWORD_LENGTH is 12 in auth routes but 8 in admin user routes.
  • _get_client_ip trusts raw X-Forwarded-For (weakens the per-IP limit).
  • The token-invalidation integration test bootstraps by dropping tables in the
    shared test DB; fine today, worth tightening if more integration tests share it.

@beorngb
beorngb force-pushed the feat/forgot-password branch from 8b0e71e to 7f8ec17 Compare July 17, 2026 15:50
@dcorio
dcorio merged commit 7adf47c into main Jul 17, 2026
8 checks passed
@dcorio
dcorio deleted the feat/forgot-password branch July 17, 2026 18:14
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.

2 participants