Password Reset Implementation: A Secure Guide for 2026

Password reset implementation - Learn how to implement a secure password reset in Expo and Hono. Follow this guide for best practices in 2026

Profile photo of DaminiDamini
19th Aug 2026
Featured image for Password Reset Implementation: A Secure Guide for 2026

You've shipped the login screen, tested the happy path, and then a tester taps Forgot password on an iPhone that has cold-started from an email link. The app opens to the wrong screen, the message doesn't arrive, and the reset endpoint has already revealed whether the address exists. Password reset implementation fails in these gaps, not in the button that submits an email address.

A reliable flow connects mobile UX, an edge API, authentication state, and email delivery. With Expo, Hono, Supabase, and a transactional email channel, you can make that connection explicit and ship a recovery journey users can complete without weakening account security.

Table of Contents

Why Password Reset Implementation Is a Full-Stack Problem

Password recovery is a contract between four parties. The Expo client collects the request and receives the deep link. The Hono API coordinates token issuance, validation, logging, and abuse controls. Supabase persists authentication state and applies the password change. The email or SMS provider carries the proof from the server to the person who controls the recovery channel.

A diagram illustrating the four-party contract process for implementing a secure password reset system in applications.A diagram illustrating the four-party contract process for implementing a secure password reset system in applications.

The request starts in the app. The user enters an email, and the client calls a reset endpoint. The API should create cryptographically random material, store only a protected representation, associate it with the intended account, and attach an expiry. Supabase records the relevant auth event or session transition, while the delivery provider handles queuing, throttling, bounces, and inbox placement. When the user taps the message, iOS or Android must route the callback back into the correct Expo state.

Each hop creates a failure mode. A cold-started app may receive a link before navigation is ready. A user may open the link in a browser rather than the installed app. An email provider may delay or suppress delivery. Supabase may reject a code that the client already tried to consume. Your implementation needs clear ownership for each error instead of returning a generic failure that leaves the user stranded.

Practical rule: Treat recovery as a distributed transaction. Every boundary needs an idempotent request, an observable result, and a safe fallback.

A large-scale 2021 analysis of 366 websites found that 302 sites, or 82.5%, used email-based reset links, while only 7 sites, or 1.9%, accepted the original password, and 27 sites, or 7.4%, used an emailed one-time code. The pattern is widespread, but it concentrates risk in the email account, which makes the recovery channel part of your threat model. The DIMVA 2021 security analysis is useful background when you're recovering account access and deciding which proof your own application should trust.

Designing the Expo Client Side of the Reset Flow

The client should make the recovery journey feel deliberate. Start with a Forgot password action on the login screen, then give each state a focused screen or route:

  1. RequestReset validates the email format locally and disables submission while the request is in flight.
  2. CheckInbox confirms that instructions were requested without saying whether the address is registered.
  3. ResetPassword receives the deep-link callback and collects the new password.
  4. Completion confirms the change and offers a controlled path back to sign-in.

The first screen shouldn't block a valid request because of an overly strict local validator. Use client validation for obvious formatting mistakes, but let the server own account lookup and policy decisions. On success, go to CheckInbox immediately, show a resend action with a visible cooldown, and preserve the entered address only as long as the product's privacy model allows.

A person holding a smartphone displaying a forgot password screen with an email input field and submit button.A person holding a smartphone displaying a forgot password screen with an email input field and submit button.

Deep links need two routes

Configure a custom Expo scheme for installed-app callbacks, then add an HTTPS universal-link or Android app-link fallback. expo-linking should parse both forms and normalize them into one internal reset event. Handle the event during a warm app, during a cold start, and after navigation has mounted. If the link opens in a browser, show a recovery page that can redirect to the app or explain what to do next instead of displaying a dead end.

Supabase recovery callbacks can contain sensitive authorization material. Prefer a browser-based handoff with expo-web-browser where the session is exchanged and then returned to the app, rather than leaving a credential-bearing fragment visible in a URL bar. Store only the minimum in-flight reference needed by the client, using expo-secure-store for sensitive state. AsyncStorage is suitable for ordinary UI preferences, not for a reset credential or session artifact.

Navigation must understand recovery state

Reset routes shouldn't sit inside the authenticated navigation tree. A user who is already signed in elsewhere may still open a recovery link, so resolve the callback into a controlled recovery session and prevent a back action from returning to an authenticated screen without the expected state transition. Clear the temporary reference after success, expiration, cancellation, or an unrecoverable error.

For broader Expo navigation patterns, the Expo mobile app guide provides useful context around structuring the surrounding application shell. Keep the reset flow independently testable, because deep-link bugs often hide behind otherwise correct navigation.

Issuing and Consuming Reset Tokens with Hono and Supabase

The Hono layer should own the reset contract rather than exposing a database table directly to the mobile app. Create two narrow routes, POST /auth/reset/request and POST /auth/reset/confirm, and keep the Supabase service-role client server-side. The client sends an email for the first route, then sends the recovery proof and new password to the second.

Request route

Validate input with a Zod schema before doing any lookup. Normalize the email consistently, but return the same successful response for an unknown address and a known address. That prevents the endpoint from becoming an account directory.

Use a cryptographically secure random token, with 32 bytes of randomness as the implementation target described in the brief. Send the raw value only through the delivery channel, and store its SHA-256 hash with user_id, expires_at, consumed_at, and a request identifier. A 15-minute expiry is the specified starter-kit policy, not a universal law. It creates a bounded window while leaving room for normal inbox delay.

const requestSchema = z.object({
  email: z.string().email().transform((value) => value.trim().toLowerCase()),
});

app.post("/auth/reset/request", zValidator("json", requestSchema), async (c) => {
  const { email } = c.req.valid("json");
  c.set("resetEvent", { type: "reset_requested" });

  // Look up the account internally, create and hash the token,
  // persist the record, and enqueue delivery.
  // Return the same response whether the account exists or not.

  return c.json({
    message: "If an account exists, recovery instructions will be sent.",
  }, 200);
});

Confirmation route

The confirm handler hashes the submitted token, finds an unconsumed matching record, checks expiry and account binding, and performs the password update through the Supabase admin API. Consume the record atomically with the update, then rotate or invalidate active sessions so a stolen session can't survive a password change.

const confirmSchema = z.object({
  token: z.string().min(1),
  password: z.string().min(1),
});

app.post("/auth/reset/confirm", zValidator("json", confirmSchema), async (c) => {
  const input = c.req.valid("json");
  c.set("resetEvent", { type: "reset_confirm_attempt" });

  // Hash token, verify account, expiry, and consumed_at,
  // update Supabase auth state, invalidate sessions,
  // and mark the record consumed in one protected operation.

  return c.json({ message: "Password updated." }, 200);
});

Add a Postgres trigger or scheduled cleanup process to prune expired records, but don't rely on cleanup for validity. The confirm query must enforce expiry itself. Structured c.set values should flow into logs containing event type, outcome, request correlation data, and safe risk signals. Never log the raw token, password, or full email address.

Delivering Magic Links and OTP Codes Through Supabase Auth

Supabase Auth can handle delivery while Expo handles the return path. On the client, call resetPasswordForEmail with a deliberate redirect URL, then listen for the callback and exchange the returned authorization code through the React Native Supabase adapter. Configure the project's Site URL and allowed redirect URLs for both development and production, and keep the mobile scheme aligned with the value declared in app.json.

The basic request is conceptually small:

await supabase.auth.resetPasswordForEmail(email, {
  redirectTo: "myapp://auth/reset",
});

The difficult part is the callback. With PKCE, the client starts an authorization transaction, retains the verifier securely, and exchanges the returned code only after the app has resumed. The resume handler must be idempotent. If the user reopens the email, the app should recognize that the code or recovery session has already been handled and show the appropriate state rather than attempting a second exchange.

A six-step diagram illustrating the Supabase authentication password reset workflow for Expo mobile applications.A six-step diagram illustrating the Supabase authentication password reset workflow for Expo mobile applications.

Choose the delivery shape deliberately

A magic link minimizes typing and works well when the user can open email on the same device. It also depends heavily on deep-link configuration and email-client behavior. An OTP code provides a practical alternative when the user is reading mail on a desktop or an email client strips link behavior. It adds entry friction and creates a guessing surface, so apply server-side attempt limits and clear lockout handling.

Supabase's OTP APIs can support email or SMS sign-in patterns, but password recovery still needs a clear account and session policy. Don't treat an OTP as a password replacement unless the authentication design explicitly supports that model.

Email templates should include plain text, localized copy, an accessible action, and a fallback explanation for users whose client doesn't open the link. A custom redirect can land directly on the reset screen, but it must never bypass the callback validation step. The Capgo tutorial offers additional perspective on mobile Supabase authentication handoffs, while this complete Supabase Auth guide for React Native covers the surrounding provider and session setup.

Hardening the Flow Against Enumeration and Token Replay

A reset route attracts attacks because it changes a high-value credential. The common risks are account enumeration, OTP guessing, replayed links, and session takeover after a successful change. Each defense belongs at a specific layer, and adding a client-side check alone doesn't protect the endpoint.

Start with a generic response for unknown emails and keep observable work reasonably uniform. Rate-limit by IP, normalized email, and broader traffic patterns, with separate thresholds for request and confirm routes. A user who mistypes an address should see a useful message, but an attacker shouldn't be able to make unlimited guesses.

Bind every proof to one recovery event

Store a hash, not the raw token. Give each request a unique identifier, mark it consumed atomically, and bind it to the intended auth account. If you use JWT claims in the recovery session, validate the expected authentication method, audience, issuer, and recovery context before allowing a password write. A valid token for one auth purpose mustn't be accepted by a general account-update endpoint.

NIST explicitly says self-service reset must authenticate the account owner and rejects knowledge-based authentication for resets because it leaves accounts vulnerable to takeover. Its Digital Identity Guidelines FAQ identifies lookup secrets and out-of-band device authentication as acceptable alternatives in suitable designs. Static questions such as a childhood address are not a meaningful substitute for possession of a protected recovery channel.

Security boundary: Password reset is a high-risk state change, not a form submission.

After a successful update, revoke refresh tokens or otherwise rotate active sessions according to your Supabase session policy. Notify the user before and after the change when the risk profile warrants it. Log request, delivery, verification, rejection, consumption, and session-invalidation events without logging secrets.

At the HTTP and edge layers, require HTTPS, apply restrictive CORS, set suitable cache controls, limit body size, and block suspicious automation with Cloudflare rules. Review these controls alongside the API security best practices guide, then verify that observability can distinguish expired, replayed, rate-limited, and malformed attempts.

Handling Edge Cases That Break Most Reset Implementations

A user requests a reset from a train with unreliable connectivity. The client times out, retries, and shows two success states. Later, both emails arrive, but the first link has been invalidated by the second request. Without clear state handling, the user sees an “invalid link” error and assumes the product lost their account.

The fix starts with explicit semantics. A request may be retried safely, but the client must avoid uncontrolled duplicate submissions. Use bounded retry behavior with backoff for transport failures, preserve the request state locally, and keep the server response generic. A resend action needs a visible countdown so users understand when another request is allowed.

A split screen image showing a phone with a no connection error and a laptop displaying an invalid link error.A split screen image showing a phone with a no connection error and a laptop displaying an invalid link error.

Plan for the wrong device

A link may open on a laptop while the reset request began on a phone. The web fallback should explain the situation and offer a secure continuation path, not expose the token in analytics or page content. If the app isn't installed, a universal-link landing page can direct the person to install it and then restart recovery through a safe handoff.

An OTP can arrive after the screen has timed out. Keep the form capable of accepting a fresh code, distinguish expired from incorrect input, and never clear a correctly entered value because the app briefly backgrounded. Keyboard handling matters here. KeyboardAvoidingView, dynamic focus, and an accessible paste action keep the code visible without making the user fight the interface.

Make backend errors actionable

Supabase may return a validation error such as a misleading 422 when the underlying problem is an expired session, malformed callback, or password policy rejection. Map provider errors to stable application errors at the Hono boundary, and give the client a small set of recoverable states: request a new link, return to the email screen, or contact support.

If the account has disappeared after the request, return the same request confirmation. If the user is signed in elsewhere, invalidate affected sessions after the reset and explain that they may need to sign in again. Reject reuse of the old password where your password policy and auth provider support that control, but don't reveal whether the submitted value matched a stored credential.

Shipping a Reset Flow Users Will Actually Complete

Security and usability meet in the copy. The request screen should say that instructions will be sent if an account is associated with the address, without confirming registration. The completion screen should tell the user what happened, what to do if the message is delayed, and how to start again after expiry. Avoid vague errors such as “Something went wrong” when the user can recover with a new request.

Accessibility belongs in the implementation, not in the polish queue. Give every field a programmatic label, support Dynamic Type, expose password visibility controls to VoiceOver, and announce validation results without moving focus unpredictably. On the OTP screen, keep the input above the keyboard and make paste behavior work with both system suggestions and manual entry.

Track the contract, not just the button

Useful events describe each boundary:

  • reset_requested records the client action and request outcome.
  • reset_email_sent represents the delivery provider's accepted handoff, not inbox arrival.
  • link_opened shows that a callback reached the app or web fallback.
  • otp_verified identifies successful proof completion.
  • password_updated confirms the auth mutation and session policy action.
  • reset_abandoned captures a journey that stopped before completion.

Keep analytics properties privacy-safe. Use a correlation identifier rather than the raw email or token, and align event names between Expo, Hono, Supabase logs, and the delivery provider. Password reset requests represent a substantial support burden. Gartner-cited figures place them at 20% to 50% of help desk calls, while Forrester estimates a manual reset at roughly $70 to $100 per request, as summarized in industry research on self-service reset adoption. Those figures make observability and self-service quality operational concerns, not decorative metrics.

Use a pre-launch ownership matrix

AreaVerificationOwner
UXTest request, resend, callback, expiry, and completion states on iOS, Android, and web fallbackProduct and mobile
SecurityVerify generic responses, token hashing, single use, expiry, rate limits, session invalidation, and secret-free logsAPI and security
AccessibilityTest VoiceOver, TalkBack, Dynamic Type, focus order, keyboard behavior, and error announcementsMobile and QA
DeliverabilityConfirm templates, localization, provider events, bounce handling, and delayed delivery messagingPlatform and operations
AnalyticsConfirm event names, correlation IDs, and funnel dashboards across each boundaryProduct analytics

The UCL password recovery research found that QR-code backup-code recovery was rated more user-friendly than trusted-party recovery, with mean ratings of 2.0 versus 3.58, and reported a test result of t(132)=9.83, p<0.001 in that comparison. The same review found about half of participants wrote passwords down, reinforcing the need for understandable recovery guidance rather than assuming users will manage credentials perfectly. Read the password usability and policy study when choosing fallback mechanisms.

For teams that want these boundaries pre-wired, AppLighter provides an Expo-based starter template with authentication UI, Supabase-adapter support, and a Hono/TypeScript edge API layer. Use it as an implementation base, then validate the token, delivery, accessibility, and session policies against your own threat model.


If you're building an Expo app and want the authentication shell, navigation, Supabase integration, and edge API structure in place before you implement recovery, visit AppLighter. Start from the wired starter template, add the reset contract and QA matrix above, and ship a password reset implementation your users can finish.

Stay Updated on the Latest UI Templates and Features

Be the first to know about new React Native UI templates and kits, features, special promotions and exclusive offers by joining our newsletter.