{"post":{"id":"84","slug":"nextjs-auth-supabase-nextauth-part-3","title":"Next.js Authentication with Supabase and NextAuth.js: Password Reset & Recovery (Part 3 of 3)","excerpt":"The final chapter of the series. Learn how to implement a complete password reset and recovery flow using Supabase email links, plus essential security considerations like rate limiting, token expiry, and one-time-use tokens.","content":"\r\n# Next.js Authentication with Supabase and NextAuth.js: Password Reset & Recovery (Part 3 of 3) 🔐\r\n\r\n> **By Sidharrth Mahadevan** | Published: March 3, 2025\r\n> [Original Article on Medium](https://medium.com/@sidharrthnix/next-js-authentication-with-supabase-and-nextauth-js-password-reset-recovery-part-3-of-3-0859f89a9ad1)\r\n\r\n---\r\n\r\n## Introduction\r\n\r\nWelcome to the **final installment** of our Next.js authentication adventure! 🎉 Here's where we've been:\r\n\r\n- In **Part 1**, we set up the foundation with NextAuth.js and Supabase.\r\n- In **Part 2**, we implemented sign-up, sign-in, and sign-out flows.\r\n- And here in **Part 3**, we're adding the crucial **password reset and recovery** functionality.\r\n\r\nBuilding authentication from scratch is challenging — there are countless security considerations, edge cases, and user experience details to get right. By combining Supabase's robust auth services with NextAuth.js's elegant React integration, we've created a system that's both secure and developer-friendly.\r\n\r\n---\r\n\r\n## Series Overview\r\n\r\n- **Part 1**: Basic setup, minimal NextAuth config, and project structure\r\n- **Part 2**: Full sign-in/sign-up implementation, session management, and protecting routes\r\n- **Part 3**: Password reset & recovery, security tips *(You are here! 👈)*\r\n\r\n---\r\n\r\n## The Password Reset Flow\r\n\r\nThe full reset flow works like this:\r\n\r\n```\r\nUser requests reset → Supabase sends email with a reset link\r\n  → User clicks link → Redirected back to your app with a token in the URL hash\r\n    → User enters new password → Your API updates it via Supabase\r\n      → User is redirected to sign in\r\n```\r\n\r\n> ⚠️ **Watch Out**: Make sure your `redirectTo` URL is an **absolute URL** including your domain. Relative URLs won't work as expected with Supabase email links!\r\n\r\n---\r\n\r\n## 1. Request a Password Reset\r\n\r\nCreate an API route to trigger the reset email:\r\n\r\n```ts\r\n// app/api/auth/reset-password/route.ts\r\nimport { NextRequest, NextResponse } from 'next/server';\r\nimport { supabase } from '@/lib/supabase';\r\n\r\nexport async function POST(request: NextRequest) {\r\n  const { email } = await request.json();\r\n\r\n  if (!email) {\r\n    return NextResponse.json({ error: 'Email is required' }, { status: 400 });\r\n  }\r\n\r\n  const { error } = await supabase.auth.resetPasswordForEmail(email, {\r\n    redirectTo: `${process.env.NEXTAUTH_URL}/auth/update-password`,\r\n  });\r\n\r\n  if (error) {\r\n    return NextResponse.json({ error: error.message }, { status: 400 });\r\n  }\r\n\r\n  // Always return success to avoid leaking whether an email exists\r\n  return NextResponse.json({\r\n    message: 'If an account with that email exists, a reset link has been sent.'\r\n  });\r\n}\r\n```\r\n\r\n---\r\n\r\n## 2. The \"Forgot Password\" Page\r\n\r\n```tsx\r\n// app/auth/forgot-password/page.tsx\r\n'use client';\r\n\r\nimport { useState } from 'react';\r\n\r\nexport default function ForgotPasswordPage() {\r\n  const [email, setEmail]       = useState('');\r\n  const [message, setMessage]   = useState('');\r\n  const [error, setError]       = useState('');\r\n  const [loading, setLoading]   = useState(false);\r\n\r\n  const handleSubmit = async (e: React.FormEvent) => {\r\n    e.preventDefault();\r\n    setLoading(true);\r\n    setError('');\r\n    setMessage('');\r\n\r\n    const res = await fetch('/api/auth/reset-password', {\r\n      method:  'POST',\r\n      headers: { 'Content-Type': 'application/json' },\r\n      body:    JSON.stringify({ email }),\r\n    });\r\n\r\n    const data = await res.json();\r\n    setLoading(false);\r\n\r\n    if (!res.ok) {\r\n      setError(data.error);\r\n    } else {\r\n      setMessage(data.message);\r\n    }\r\n  };\r\n\r\n  return (\r\n    <div>\r\n      <h1>Forgot Password</h1>\r\n      <form onSubmit={handleSubmit}>\r\n        <input\r\n          type=\"email\"\r\n          value={email}\r\n          onChange={(e) => setEmail(e.target.value)}\r\n          placeholder=\"Enter your email\"\r\n          required\r\n        />\r\n        <button type=\"submit\" disabled={loading}>\r\n          {loading ? 'Sending...' : 'Send Reset Link'}\r\n        </button>\r\n      </form>\r\n      {message && <p style={{ color: 'green' }}>{message}</p>}\r\n      {error   && <p style={{ color: 'red'   }}>{error}</p>}\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n---\r\n\r\n## 3. The Update Password Page\r\n\r\nThis page:\r\n1. Extracts authentication tokens from the URL hash (Supabase puts them there after the user clicks the reset link).\r\n2. Provides a form for users to enter and confirm their new password.\r\n3. Validates the password input.\r\n4. Sends everything to our API endpoint for the actual update.\r\n\r\n```tsx\r\n// app/auth/update-password/page.tsx\r\n'use client';\r\n\r\nimport { useState, useEffect } from 'react';\r\nimport { useRouter } from 'next/navigation';\r\nimport { supabase } from '@/lib/supabase';\r\n\r\nexport default function UpdatePasswordPage() {\r\n  const router = useRouter();\r\n  const [password, setPassword]   = useState('');\r\n  const [confirm, setConfirm]     = useState('');\r\n  const [error, setError]         = useState('');\r\n  const [message, setMessage]     = useState('');\r\n  const [loading, setLoading]     = useState(false);\r\n\r\n  // Supabase places the session tokens in the URL hash after the reset link is clicked\r\n  useEffect(() => {\r\n    const hash = window.location.hash;\r\n    if (hash) {\r\n      const params = new URLSearchParams(hash.substring(1));\r\n      const accessToken  = params.get('access_token');\r\n      const refreshToken = params.get('refresh_token');\r\n\r\n      if (accessToken && refreshToken) {\r\n        supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken });\r\n      }\r\n    }\r\n  }, []);\r\n\r\n  const handleSubmit = async (e: React.FormEvent) => {\r\n    e.preventDefault();\r\n    setError('');\r\n\r\n    if (password !== confirm) {\r\n      setError('Passwords do not match.');\r\n      return;\r\n    }\r\n\r\n    if (password.length < 8) {\r\n      setError('Password must be at least 8 characters.');\r\n      return;\r\n    }\r\n\r\n    setLoading(true);\r\n\r\n    const { error: updateError } = await supabase.auth.updateUser({ password });\r\n\r\n    setLoading(false);\r\n\r\n    if (updateError) {\r\n      setError(updateError.message);\r\n    } else {\r\n      setMessage('Password updated successfully! Redirecting to sign in...');\r\n      setTimeout(() => router.push('/auth/signin'), 2000);\r\n    }\r\n  };\r\n\r\n  return (\r\n    <div>\r\n      <h1>Set New Password</h1>\r\n      <form onSubmit={handleSubmit}>\r\n        <input\r\n          type=\"password\"\r\n          value={password}\r\n          onChange={(e) => setPassword(e.target.value)}\r\n          placeholder=\"New password\"\r\n          required\r\n        />\r\n        <input\r\n          type=\"password\"\r\n          value={confirm}\r\n          onChange={(e) => setConfirm(e.target.value)}\r\n          placeholder=\"Confirm new password\"\r\n          required\r\n        />\r\n        <button type=\"submit\" disabled={loading}>\r\n          {loading ? 'Updating...' : 'Update Password'}\r\n        </button>\r\n      </form>\r\n      {message && <p style={{ color: 'green' }}>{message}</p>}\r\n      {error   && <p style={{ color: 'red'   }}>{error}</p>}\r\n    </div>\r\n  );\r\n}\r\n```\r\n\r\n> 💡 **Tip from Experience**: Testing this flow can be tricky since it involves email links. Set up a test email account and configure Supabase to use a **short token expiration** during development so you're not waiting around.\r\n\r\n---\r\n\r\n## 4. 🔒 Security Considerations\r\n\r\nA secure password reset system must account for the following:\r\n\r\n### One-Time Use Tokens\r\nReset tokens should be one-and-done — thankfully Supabase handles this automatically. Each reset link can only be used once.\r\n\r\n### Rate Limiting\r\nImplement rate limiting to prevent brute-force attempts. A good starting point is **5 attempts per hour** per email address or IP. You can enforce this using middleware or an edge function.\r\n\r\n### Secure Transmission\r\nAlways serve your app over **HTTPS** in production. Reset links sent over plain HTTP can be intercepted.\r\n\r\n### Password Requirements\r\nEnforce reasonable password strength — but don't drive users crazy with overly complex rules!\r\n\r\n> 💪 **Strength Tip**: A long, memorable passphrase is often more secure *and* more user-friendly than a short, complex password packed with special characters.\r\n\r\n### Don't Leak Email Existence\r\nNotice our reset API always returns a generic success message — even if the email doesn't exist in our database. This prevents an attacker from enumerating valid email addresses.\r\n\r\n---\r\n\r\n## 🎉 Series Wrap-Up\r\n\r\nWe've reached the end of our three-part authentication adventure! Let's take a moment to reflect on what we've built together:\r\n\r\n| Part   | What We Covered                                    |\r\n|--------|----------------------------------------------------|\r\n| Part 1 | NextAuth setup, Supabase client, env variables     |\r\n| Part 2 | Sign-up, sign-in, sign-out, JWT sessions, route protection |\r\n| Part 3 | Password reset flow, security best practices       |\r\n\r\n### Core Principles We Applied\r\n\r\n1. **Separation of concerns** — Authentication logic lives in dedicated, focused files.\r\n2. **Error handling** — Clear feedback to users when things go wrong.\r\n3. **Security first** — Proper token management, one-time-use links, and rate limiting.\r\n4. **User experience** — Intuitive flows that don't frustrate users.\r\n\r\nBy combining Supabase's robust auth services with NextAuth.js's elegant React integration, we've created a system that's both **secure** and **developer-friendly**.\r\n\r\nThank you sincerely for following along with this series — it's been a genuine pleasure writing it. If you found it valuable, consider sharing it with other developers who might benefit!\r\n\r\n---\r\n\r\n## Resources\r\n\r\n- [NextAuth.js Official Docs](https://next-auth.js.org/getting-started/introduction)\r\n- [Supabase Auth Docs](https://supabase.com/docs/guides/auth/server-side/nextjs)\r\n- [Supabase Password Reset Guide](https://supabase.com/docs/guides/auth/passwords#resetting-a-password)\r\n  ","category":"Tutorial","author":"Sidharrth Mahadevan","published_at":"2025-03-03T00:00:00.000Z","read_time":"8 min read","image_url":"https://miro.medium.com/v2/resize:fit:1100/format:webp/1*GZUM5Hs7RS3mU8e6IjDSsw.png","tags":["Next.js","Authentication","Supabase","NextAuth","JavaScript","Security"],"featured":false,"created_at":"2026-04-01T07:09:40.429Z","image_attribution":"Generated by AI-Hub"}}