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:
- Request Reset - User submits email to receive reset link
- Validate Token - System verifies the reset token
- 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-resetRequest
{
"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=xyz123Success 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/confirmRequest
{
"token": "xyz123...",
"password": "newSecurePassword123!",
"confirmPassword": "newSecurePassword123!"
}Password Requirements
| Rule | Requirement |
|---|---|
| Minimum length | 8 characters |
| Uppercase | At least 1 letter (A-Z) |
| Lowercase | At least 1 letter (a-z) |
| Number | At least 1 digit (0-9) |
| Special char | At least 1 symbol (!@#$%^&*) |
| Common passwords | Blocked (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/changeRequest
{
"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
| Feature | Implementation |
|---|---|
| Token Expiration | 1 hour (configurable) |
| Token Format | Cryptographically secure random (32 bytes) |
| Token Storage | Hashed in database |
| Rate Limiting | 3 requests per email per hour |
| IP Logging | Tracked for security audit |
| Old Passwords | Cannot reuse last 5 passwords |
| Session Handling | Optional 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 TeamWebhook Events
| Event | Trigger |
|---|---|
password_reset.requested | Reset email sent |
password_reset.completed | Password successfully changed |
password_reset.failed | Invalid/expired token used |
Rate Limits
| Action | Limit |
|---|---|
| Request reset | 3 per email per hour |
| Validate token | 10 per IP per minute |
| Confirm reset | 5 per IP per hour |