Docs

Password Reset API

Secure password reset flow with email verification and token validation

Password Reset API

Secure password reset flow allowing users to regain access to their accounts. Includes email verification, token validation, and password update endpoints.

Overview

The password reset flow consists of three steps:

  1. Request Reset - User submits email to receive reset link
  2. Validate Token - System verifies the reset token
  3. Update Password - User sets new password

Request Password Reset

Initiate the password reset process by sending a reset link to the user's email.

Endpoint

POST /api/auth/password-reset

Request

{
  "email": "user@example.com",
  "callbackUrl": "https://yourdomain.com/reset-password" // Optional
}

Success Response (200)

{
  "success": true,
  "data": {
    "sent": true,
    "expiresIn": 3600
  },
  "message": "If an account exists, a password reset email has been sent"
}

Security Note: The success response is identical whether the email exists or not to prevent email enumeration attacks.

Rate Limit Response (429)

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many password reset attempts. Please try again in 15 minutes."
  }
}

Validate Reset Token

Verify that a password reset token is valid and not expired.

Endpoint

GET /api/auth/password-reset/validate?token=xyz123

Success Response (200)

{
  "success": true,
  "data": {
    "valid": true,
    "email": "user@example.com",
    "expiresAt": "2024-01-15T11:30:00Z"
  }
}

Error Response (400)

{
  "success": false,
  "error": {
    "code": "INVALID_TOKEN",
    "message": "The password reset link is invalid or has expired"
  }
}

Update Password

Set a new password using a valid reset token.

Endpoint

POST /api/auth/password-reset/confirm

Request

{
  "token": "xyz123...",
  "password": "newSecurePassword123!",
  "confirmPassword": "newSecurePassword123!"
}

Password Requirements

RuleRequirement
Minimum length8 characters
UppercaseAt least 1 letter (A-Z)
LowercaseAt least 1 letter (a-z)
NumberAt least 1 digit (0-9)
Special charAt least 1 symbol (!@#$%^&*)
Common passwordsBlocked (e.g., "password123")

Success Response (200)

{
  "success": true,
  "data": {
    "reset": true,
    "session": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "expiresAt": "2024-01-22T10:30:00Z"
    }
  },
  "message": "Password updated successfully. You are now signed in."
}

Error Response (422)

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Password does not meet requirements",
    "details": [
      {
        "field": "password",
        "message": "Password must contain at least one special character"
      }
    ]
  }
}

Change Password (Authenticated)

Allow authenticated users to change their password directly.

Endpoint

POST /api/auth/password-reset/change

Request

{
  "currentPassword": "oldPassword123!",
  "newPassword": "newSecurePassword123!",
  "confirmPassword": "newSecurePassword123!",
  "revokeOtherSessions": true // Optional: sign out from other devices
}

Success Response (200)

{
  "success": true,
  "data": {
    "changed": true,
    "sessionsRevoked": 3
  },
  "message": "Password changed successfully"
}

Code Examples

Request Password Reset

async function requestPasswordReset(email: string) {
  const response = await fetch('/api/auth/password-reset', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email,
      callbackUrl: `${window.location.origin}/reset-password`
    })
  });

  const result = await response.json();
  
  // Always show success message regardless of result
  // to prevent email enumeration
  return {
    success: true,
    message: 'If an account exists with this email, you will receive a password reset link.'
  };
}

Complete Password Reset Flow

// Step 1: Validate token on page load
async function validateResetToken(token: string) {
  const response = await fetch(
    `/api/auth/password-reset/validate?token=${encodeURIComponent(token)}`
  );
  
  const result = await response.json();
  
  if (!result.success) {
    throw new Error(result.error?.message || 'Invalid token');
  }
  
  return result.data;
}

// Step 2: Update password
async function resetPassword(token: string, password: string) {
  const response = await fetch('/api/auth/password-reset/confirm', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token, password, confirmPassword: password })
  });

  const result = await response.json();
  
  if (!result.success) {
    throw new Error(result.error?.message);
  }
  
  // Session is automatically created
  return result.data;
}

React Password Reset Form

import { useState } from 'react';
import { useSearchParams } from 'next/navigation';

export function PasswordResetForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [step, setStep] = useState<'request' | 'email-sent' | 'reset' | 'success'>('request');
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  
  const searchParams = useSearchParams();
  const token = searchParams.get('token');

  // If token is in URL, show reset form
  useState(() => {
    if (token) {
      validateToken(token).then(() => setStep('reset'));
    }
  });

  const handleRequestSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsLoading(true);
    setError('');

    try {
      await requestPasswordReset(email);
      setStep('email-sent');
    } catch {
      setError('Failed to send reset email');
    } finally {
      setIsLoading(false);
    }
  };

  const handleResetSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (password !== confirmPassword) {
      setError('Passwords do not match');
      return;
    }

    setIsLoading(true);
    setError('');

    try {
      await resetPassword(token!, password);
      setStep('success');
      // Redirect after short delay
      setTimeout(() => window.location.href = '/dashboard', 2000);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to reset password');
    } finally {
      setIsLoading(false);
    }
  };

  if (step === 'email-sent') {
    return (
      <div className="success-message">
        <h2>Check Your Email</h2>
        <p>We've sent a password reset link to {email}</p>
        <p>Link expires in 1 hour</p>
      </div>
    );
  }

  if (step === 'success') {
    return (
      <div className="success-message">
        <h2>Password Updated!</h2>
        <p>Redirecting to dashboard...</p>
      </div>
    );
  }

  if (step === 'reset') {
    return (
      <form onSubmit={handleResetSubmit}>
        <h2>Set New Password</h2>
        {error && <div className="error">{error}</div>}
        
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="New password"
          required
        />
        
        <input
          type="password"
          value={confirmPassword}
          onChange={(e) => setConfirmPassword(e.target.value)}
          placeholder="Confirm new password"
          required
        />
        
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Updating...' : 'Update Password'}
        </button>
      </form>
    );
  }

  return (
    <form onSubmit={handleRequestSubmit}>
      <h2>Reset Password</h2>
      {error && <div className="error">{error}</div>}
      
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Sending...' : 'Send Reset Link'}
      </button>
    </form>
  );
}

Provider-Specific Behavior

BetterAuth

  • Tokens stored in database with 1-hour expiry
  • SHA-256 hashed tokens (raw token only in email)
  • Automatic session creation after reset

NextAuth

  • Uses NextAuth's built-in email provider
  • Token sent via configured email service
  • Handles callback URL automatically

Clerk

  • Delegates to Clerk's password reset flow
  • Built-in email templates
  • Automatic session handling

AuthKit

  • Uses WorkOS password reset
  • Enterprise SSO password policies supported
  • Directory sync considerations

Security Considerations

FeatureImplementation
Token Expiration1 hour (configurable)
Token FormatCryptographically secure random (32 bytes)
Token StorageHashed in database
Rate Limiting3 requests per email per hour
IP LoggingTracked for security audit
Old PasswordsCannot reuse last 5 passwords
Session HandlingOptional revoke other sessions

Email Template

The password reset email includes:

Subject: Reset your password

Hi {name},

You requested a password reset for your account.

Click the link below to reset your password:
{resetUrl}

This link will expire in 1 hour.

If you didn't request this, you can safely ignore this email.

---
{companyName} Security Team

Webhook Events

EventTrigger
password_reset.requestedReset email sent
password_reset.completedPassword successfully changed
password_reset.failedInvalid/expired token used

Rate Limits

ActionLimit
Request reset3 per email per hour
Validate token10 per IP per minute
Confirm reset5 per IP per hour

On this page