Skip to content

Password Reset Flow Implementation Guide

This document provides comprehensive documentation of the password reset flow in Bartendie, including the ForgotPasswordPage and ResetPasswordPage components, security features, and backend integration requirements.

The password reset flow consists of two main components:

  1. ForgotPasswordPage - Initiates password reset by requesting email
  2. ResetPasswordPage - Completes password reset with token validation

The implementation includes security features like email enumeration protection, token expiration, and comprehensive validation.

User Flow:
1. User clicks "Forgot Password" → ForgotPasswordPage
2. User enters email → REQUEST_PASSWORD_RESET_MUTATION
3. User receives email with reset link
4. User clicks link → ResetPasswordPage (with token)
5. User enters new password → RESET_PASSWORD_MUTATION
6. Password reset complete → Redirect to login

Location: frontend/src/components/auth/ForgotPasswordPage.tsx

const REQUEST_PASSWORD_RESET_MUTATION = gql`
mutation RequestPasswordReset($email: String!) {
requestPasswordReset(email: $email) {
success
message
}
}
`;

Features:

  • Simple Input: Only requires email address
  • Success Response: Boolean success flag with optional message
  • Error Handling: Graceful error management
interface ForgotPasswordFormData {
email: string;
}
interface ForgotPasswordError {
message: string;
field?: string;
}

Design Principles:

  • Minimal Data: Only email required
  • Field-Specific Errors: Targeted error messaging
  • Type Safety: Full TypeScript support
try {
const { data } = await requestPasswordResetMutation({
variables: { email: formData.email },
});
if (data.requestPasswordReset.success) {
setIsSuccess(true);
} else {
setError({ message: data.requestPasswordReset.message || 'Failed to send reset email.' });
}
} catch (err: any) {
// For security reasons, we don't want to reveal if an email exists or not
// So we'll show success even if the email doesn't exist
setIsSuccess(true);
}

Security Benefits:

  • No Email Enumeration: Always shows success to prevent email discovery
  • Consistent Response: Same response time regardless of email existence
  • User-Friendly: Doesn’t confuse legitimate users
// Success screen with security messaging
<div className="text-center space-y-4">
<div className="mx-auto w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900">Check your email</h3>
<p className="text-sm text-gray-600">
If an account with <strong>{formData.email}</strong> exists,
we've sent you a password reset link.
</p>
<p className="text-sm text-gray-600">
The reset link will expire in <strong>1 hour</strong> for security reasons.
</p>
</div>
</div>
const validateEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validateForm = (): boolean => {
if (!formData.email) {
setError({ message: 'Email is required', field: 'email' });
return false;
}
if (!validateEmail(formData.email)) {
setError({ message: 'Please enter a valid email address', field: 'email' });
return false;
}
return true;
};
const handleTryAgain = () => {
setIsSuccess(false);
setFormData({ email: '' });
setError(null);
};
// Multiple ways to continue
<div className="space-y-3">
<Button onClick={handleTryAgain} variant="outline" className="w-full">
Send to a different email
</Button>
<Link to="/login" className="block">
<Button variant="ghost" className="w-full">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to sign in
</Button>
</Link>
</div>

Location: frontend/src/components/auth/ResetPasswordPage.tsx

const RESET_PASSWORD_MUTATION = gql`
mutation ResetPassword($token: String!, $password: String!, $confirmPassword: String!) {
resetPassword(token: $token, password: $password, confirmPassword: $confirmPassword) {
success
message
}
}
`;

Features:

  • Token Validation: Server-side token verification
  • Password Confirmation: Double-entry password validation
  • Success Response: Boolean success with optional message
interface ResetPasswordFormData {
password: string;
confirmPassword: string;
}
interface ResetPasswordError {
message: string;
field?: string;
}
interface PasswordRequirement {
label: string;
test: (password: string) => boolean;
}
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get('token');
// Redirect if no token is provided
useEffect(() => {
if (!token) {
navigate('/forgot-password', { replace: true });
}
}, [token, navigate]);

Security Features:

  • Automatic Redirect: Redirects to forgot password if no token
  • Token Validation: Server-side token verification
  • Replace Navigation: Prevents back button issues
try {
const { data } = await resetPasswordMutation({
variables: {
token,
password: formData.password,
confirmPassword: formData.confirmPassword,
},
});
if (data.resetPassword.success) {
setIsSuccess(true);
} else {
setError({ message: data.resetPassword.message || 'Failed to reset password.' });
}
} catch (err: any) {
if (err.message.includes('token')) {
setError({ message: 'This reset link has expired or is invalid. Please request a new one.' });
} else {
setError({ message: err.message || 'Failed to reset password. Please try again.' });
}
}
const passwordRequirements: PasswordRequirement[] = [
{ label: 'At least 8 characters', test: (pwd) => pwd.length >= 8 },
{ label: 'Contains uppercase letter', test: (pwd) => /[A-Z]/.test(pwd) },
{ label: 'Contains lowercase letter', test: (pwd) => /[a-z]/.test(pwd) },
{ label: 'Contains number', test: (pwd) => /\d/.test(pwd) },
{ label: 'Contains special character', test: (pwd) => /[!@#$%^&*(),.?":{}|<>]/.test(pwd) },
];
const getPasswordStrength = (): { score: number; label: string; color: string } => {
const metRequirements = passwordRequirements.filter(req => req.test(formData.password)).length;
if (metRequirements <= 2) return { score: metRequirements, label: 'Weak', color: 'text-red-600' };
if (metRequirements <= 3) return { score: metRequirements, label: 'Fair', color: 'text-yellow-600' };
if (metRequirements <= 4) return { score: metRequirements, label: 'Good', color: 'text-blue-600' };
return { score: metRequirements, label: 'Strong', color: 'text-green-600' };
};
{/* Password Strength Indicator */}
{formData.password && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">Password strength:</span>
<span className={`text-xs font-medium ${passwordStrength.color}`}>
{passwordStrength.label}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className={`h-1 rounded-full transition-all duration-300 ${
passwordStrength.score <= 2 ? 'bg-red-500' :
passwordStrength.score <= 3 ? 'bg-yellow-500' :
passwordStrength.score <= 4 ? 'bg-blue-500' : 'bg-green-500'
}`}
style={{ width: `${(passwordStrength.score / 5) * 100}%` }}
/>
</div>
</div>
)}
{/* Password Requirements */}
{showPasswordRequirements && (
<div className="space-y-1 p-3 bg-gray-50 rounded-md">
<p className="text-xs font-medium text-gray-700 mb-2">Password must contain:</p>
{passwordRequirements.map((req, index) => {
const isMet = req.test(formData.password);
return (
<div key={index} className="flex items-center gap-2 text-xs">
{isMet ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<X className="h-3 w-3 text-gray-400" />
)}
<span className={isMet ? 'text-green-600' : 'text-gray-500'}>
{req.label}
</span>
</div>
);
})}
</div>
)}
{/* Password Match Indicator */}
{formData.confirmPassword && (
<div className="flex items-center gap-2 text-xs">
{formData.password === formData.confirmPassword ? (
<>
<Check className="h-3 w-3 text-green-600" />
<span className="text-green-600">Passwords match</span>
</>
) : (
<>
<X className="h-3 w-3 text-red-600" />
<span className="text-red-600">Passwords do not match</span>
</>
)}
</div>
)}
// Success screen after password reset
{isSuccess && (
<div className="text-center space-y-4">
<div className="mx-auto w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="h-6 w-6 text-green-600" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold text-gray-900">Password reset successful!</h3>
<p className="text-sm text-gray-600">
Your password has been successfully updated. You can now sign in with your new password.
</p>
</div>
<Link to="/login" className="block">
<Button className="w-full">
Continue to sign in
</Button>
</Link>
</div>
)}
type Mutation {
requestPasswordReset(email: String!): PasswordResetResponse!
resetPassword(token: String!, password: String!, confirmPassword: String!): PasswordResetResponse!
}
type PasswordResetResponse {
success: Boolean!
message: String
}
  • Cryptographically Secure: Use secure random token generation
  • Expiration: 1-hour expiration for security
  • Single Use: Tokens should be invalidated after use
  • Database Storage: Store hashed tokens, not plain text
  • Rate Limiting: Prevent spam by limiting requests per IP/email
  • Template Security: Use secure email templates
  • Link Format: https://domain.com/reset-password?token=<secure_token>
  • Server-Side Validation: Validate all password requirements on backend
  • Hash Storage: Use bcrypt or similar for password hashing
  • Audit Logging: Log password reset attempts for security
// Invalid token
{
"errors": [
{
"message": "Invalid or expired reset token",
"extensions": {
"code": "INVALID_TOKEN"
}
}
]
}
// Password validation failure
{
"errors": [
{
"message": "Password does not meet security requirements",
"extensions": {
"code": "WEAK_PASSWORD"
}
}
]
}
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
  • Screen Reader Support: Proper ARIA labels and descriptions
  • Keyboard Navigation: Full keyboard accessibility
  • Focus Management: Logical tab order
  • Error Announcements: Live regions for dynamic content
describe('ForgotPasswordPage', () => {
test('validates email format', () => {
// Test email validation
});
test('shows success state after submission', () => {
// Test success flow
});
test('handles API errors gracefully', () => {
// Test error handling
});
});
describe('ResetPasswordPage', () => {
test('redirects when no token provided', () => {
// Test token requirement
});
test('validates password requirements', () => {
// Test password validation
});
test('handles expired tokens', () => {
// Test token expiration
});
});
test('complete password reset flow', async () => {
// 1. Request password reset
// 2. Simulate email click
// 3. Reset password
// 4. Verify success
});
  • No Sensitive Data Storage: Never store tokens in localStorage
  • HTTPS Only: Ensure all requests use HTTPS
  • Input Sanitization: Validate all user inputs
  • Error Message Security: Don’t reveal system information
  • Token Expiration: Short-lived tokens (1 hour)
  • Rate Limiting: Prevent abuse
  • Audit Logging: Track all reset attempts
  • Email Verification: Verify email ownership

This comprehensive password reset flow provides a secure, user-friendly experience while protecting against common security vulnerabilities.