Repository navigation
feat(auth): add a forgot-password flow - #302
Merged
Merged
Conversation
The sign-in dialog had no way to recover a forgotten password, so email/password users who lost theirs were locked out for good. - "Forgot password?" in the sign-in dialog switches it to a reset mode that emails a Supabase recovery link (Turnstile-protected like sign-in/sign-up). - The link lands on /auth/callback, which forwards to a new localized /reset-password page. It offers the new-password form only to sessions opened from an emailed link, then returns the user to the page they started from. - /auth/callback now validates `next` by resolved origin: the old prefix check let `/\evil.com` through as an open redirect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The sign-in dialog had no way to recover a forgotten password, so email/password users who lost theirs were locked out for good.
What
AuthDialogto a third mode (signin | signup | reset). It asks only for an email and sendsresetPasswordForEmailwith the same fresh Turnstile token as sign-in/sign-up, since GoTrue captcha-checks/recovertoo. The confirmation is deliberately neutral ("if an account exists…"), matching Supabase, which returns 200 for unknown emails./auth/callback?next=/reset-password?next=<page you were on>. That keeps the existing rule that/auth/callbackis the only page accepting session tokens in the URL; Layout still treats a token anywhere else as a stale link./reset-passwordpage (en + cs,noindex, not in the sitemap). The new-password form is offered only to sessions opened from an emailed link (amrofotp/magiclink/recovery). That's exactly the set GoTrue'sSession.IsRecovery()exempts from the current-password check. A password or Google session, or no session at all, gets "this link is invalid or has expired" with a button that reopens the dialog in reset mode. After saving, the user returns to the page where they asked for the link, signed in, with a success snackbar./auth/callback.nextwas validated withstartsWith("/") && !startsWith("//"), which lets/\evil.comthrough, and browsers resolve that tohttps://evil.com/. Both the callback and the reset page now use a sharedsameOriginPath()that parses the URL and compares origins.Before this works in production
Supabase sends reset emails itself, not the backend. If the project still uses Supabase's built-in mailer, it only delivers to project team members. A custom SMTP server must be set under Authentication → Emails → SMTP Settings;
docs/SETUP.mdnow says so. Redirect URLs need no change, sincehttps://www.kalandra.tech/**already covers/auth/callback?next=….Testing
password-reset-flow.spec.tsagainst local Supabase + Mailpit. It requests a link from the dialog, opens the real emailed link, checks password-mismatch validation, sets a new password, and asserts the redirect back to/about. Then the old password fails and the new one signs in. A second case spends a link first and checks that reopening it lands on the "send a new link" state.noindex, and the sitemap excludes the page./cs/blog/reset-passwordgets the invalid-link state🤖 Generated with Claude Code