Skip to content
← Back to the blog
securityapps

How to build a secure password reset flow

What a safe forgot-password flow looks like, the mistakes AI-built apps often make with reset links, how it works in Supabase and how to test yours.

By Devonauts Team6 min read
Leer en español →
A monitor showing an authentication failed message with a login retry link

A secure password reset flow sends a single-use, short-lived link to the account's email, shows the same message whether or not the email is registered, lets the user set a new password only after that link is verified on the server, and then ends old sessions and tells the user their password changed. The link must point to your real domain, and the reset request must be rate-limited so nobody can flood an inbox or guess tokens.

This guide lays out the flow step by step, the mistakes that show up most in apps built with AI tools, how Supabase handles it and what you still have to configure, and a test you can run on your own app.

What does a secure reset flow look like?

  1. The user asks for a reset by entering their email on a public page.
  2. The app always answers the same way: "If an account exists for that address, we've sent a link." Same wording, same timing, whether the account exists or not. Otherwise the form tells anyone which emails are registered.
  3. The server creates a token that is random, long, tied to that one account, valid only once and only for a short time, and stored safely (hashed, like a password).
  4. The email contains a link to your domain with that token. The base URL comes from your configuration, never from the incoming request's host header.
  5. The reset page verifies the token on the server before showing the "new password" form. An invalid or used token gets a clear error and a way to request a new link.
  6. The new password is checked against the same rules as sign-up, typed twice.
  7. After the change, the token is invalidated, other sessions are signed out (or the user is offered that), and a "your password was changed" email goes to the account address.
  8. The user signs in normally. Logging them in automatically right after a reset is discouraged by OWASP's guidance.

The OWASP Forgot Password Cheat Sheet is the reference for these points if you want more depth.

What mistakes do AI-built apps make with password reset?

These come up repeatedly when the flow was generated from a prompt:

  • The reset page doesn't need a token. It calls "update password" for whatever email is in the URL or form. Anyone can reset anyone's password. This is the most serious one.
  • The link points to localhost or the preview URL, because the redirect setting was never updated for the live domain. Users click and land on a page that doesn't load. Our guide on login redirects going to localhost explains how to fix the redirect settings.
  • "No account with that email" messages on the reset form, which reveal who uses your app.
  • No rate limit, so a script can send hundreds of emails to one address, or hammer the verification endpoint.
  • Sessions stay alive. If someone had stolen a session, it keeps working after the real owner resets the password.
  • Third-party scripts on the reset page (analytics, chat widgets) that can see the URL with the token. Set a no-referrer policy on that page and keep it free of trackers.

How does password reset work in Supabase?

Supabase Auth gives you most of the secure pieces, and you wire them together:

  • resetPasswordForEmail(email, { redirectTo }) sends the email. Supabase's docs note that it does not reveal whether an account exists: no email is sent for unknown addresses, but the call still succeeds. Show your neutral message regardless.
  • redirectTo must be listed in Redirect URLs in your project's auth settings. If the live domain isn't there, the link falls back to your Site URL or fails.
  • With the PKCE flow, the email template points to a route on your site (for example /auth/confirm) that calls verifyOtp with the token hash, then sends the user to your "update password" page. That page should only work for a signed-in recovery session.
  • On that page, updateUser({ password }) sets the new password. Recent versions of supabase-js also accept the current password for normal (non-reset) password changes.

What you still have to set up yourself:

  1. Email delivery. Supabase's built-in email service is limited (its docs say 2 emails per hour) and meant for testing. Configure custom SMTP before launch. See Supabase not sending confirmation or reset emails.
  2. Password rules in the auth settings (minimum length, required characters) and, where your plan includes it, leaked password protection.
  3. Signing out other sessions after the change, for example with signOut({ scope: 'others' }).
  4. The "password changed" notification if your project's email templates don't already cover it.
  5. CAPTCHA on the reset form if you see abuse; Supabase Auth supports it.

How do I test my reset flow?

Use a test account you control, ideally in a staging environment.

  1. Request a reset for a registered email and for an unregistered one. The message on screen should be identical.
  2. Open the email. Check that the link goes to your live domain over HTTPS.
  3. Use the link and set a new password. Then press back and try to use the same link again. It must be rejected.
  4. Request a link, wait past the expiry configured in your auth settings, and try it. It must be rejected.
  5. Open the reset page with no token, or a made-up one. It must not show a working form.
  6. Before resetting, sign in on a second browser. After resetting, refresh that browser: the old session should be gone, or you should have been offered to end it.
  7. Submit the reset form many times quickly. You should hit a limit.
  8. Check the inbox for the "password changed" notice.

If step 5 lets you set a password, stop and fix that before anything else.

How does this connect to the rest of your login security?

Password reset is a way into an account, so it is only as strong as the email account behind it. Two things make the whole system safer:

  • Two-factor authentication. If a user has a second factor, a reset email alone should not be enough to take over the account. See adding two-factor authentication.
  • Treat email changes like password changes. Changing the account email should require the current password or a recent sign-in, and notify the old address. Otherwise someone with a stolen session can change the email, then reset the password.

Authentication problems are number seven on the OWASP Top 10; our overview of the OWASP Top 10 for AI-built apps puts them in context.

Quick answers

Should the reset form say "email not found"? No. Always show the same neutral message, so the form can't be used to check who has an account.

How long should a reset link last? Short enough that an old email in someone's inbox is useless: minutes to an hour is common. Use the expiry setting your auth provider offers.

Can I use security questions instead? OWASP advises against relying on them alone; answers are often guessable or findable online.

Should users be logged in automatically after resetting? Better not. Send them to the normal sign-in, which also confirms the new password works.

Not sure your flow passes?

The free health check looks at what your live app exposes from outside. If you'd like a person to review your sign-up, login and reset flows in the code, our Supabase security review covers them, or you can ask us a question.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.