Modernizing User Authentication: A Secure Password Reset System

August 21, 2025

Web Development
Security
Game Development

Modernizing User Authentication: A Secure Password Reset System

User authentication is the cornerstone of any secure web application, and password reset functionality is often one of the most critical—and vulnerable—features developers implement. Today, I'm excited to share the journey of building a modern, secure password reset system for our Feng Shui 2 RPG campaign management platform, a comprehensive solution that prioritizes both security and user experience.

What We Built

The modernized password reset system is a full-stack feature that transforms how users recover their accounts when they forget their passwords. Built with Rails 8.0 on the backend and Next.js 15 on the frontend, this system implements industry-standard security practices while providing an intuitive, mobile-responsive user interface.

Key Components

How It Works

The password reset flow follows a secure, user-friendly process:

1. Password Reset Request

Users visit the /forgot-password page and enter their email address. The frontend validates the email format using RFC 5322 compliance before sending the request:

// Frontend validation with real-time feedback
const validateEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

export default function ForgotPasswordForm({
  onSubmit,
  loading = false,
  error = null,
  success = false
}: ForgotPasswordFormProps) {
  const [email, setEmail] = useState('')
  const [emailError, setEmailError] = useState<string | null>(null)

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!validateEmail(email)) {
      setEmailError('Please enter a valid email address')
      return
    }
    await onSubmit(email)
  }

  // Material UI form with professional styling...
}

2. Backend Security Processing

The Rails backend processes the request through our enhanced Users::PasswordsController with multiple security layers:

class Users::PasswordsController < Devise::PasswordsController
  before_action :rate_limit_password_resets, only: [:create]
  before_action :authenticate_rate_limit_bypass, only: [:create]

  def create
    email = resource_params[:email]&.downcase&.strip
    
    # Always return success to prevent email enumeration
    if email.present?
      resource = User.find_by(email: email)
      if resource&.persisted?
        resource.send_reset_password_instructions
        Rails.logger.info "Password reset sent for #{email}"
      else
        Rails.logger.info "Password reset attempted for non-existent email: #{email}"
      end
    end

    render json: {
      message: "If your email address exists in our database, you will receive password reset instructions shortly."
    }, status: :ok
  end

  private

  def rate_limit_password_resets
    email = resource_params[:email]&.downcase&.strip
    return unless email.present?

    email_key = "password_reset_rate_limit:email:#{email}"
    ip_key = "password_reset_rate_limit:ip:#{request.remote_ip}"
    
    email_count = Rails.cache.read(email_key) || 0
    ip_count = Rails.cache.read(ip_key) || 0
    
    if email_count >= 3 || ip_count >= 5
      render json: {
        error: "Too many password reset attempts. Please wait before trying again.",
        retry_after: 3600
      }, status: :too_many_requests and return
    end
    
    Rails.cache.write(email_key, email_count + 1, expires_in: 1.hour)
    Rails.cache.write(ip_key, ip_count + 1, expires_in: 1.hour)
  end
end

3. Professional Email Delivery

Users receive a professionally designed HTML email with clear instructions:

<p>Hello <%= @resource.email %>!</p>

<p>Someone has requested a link to change your password for your Feng Shui 2 campaign account. You can do this through the link below.</p>

<p>
  <%= link_to 'Reset My Password', 
      edit_password_url(@resource, reset_password_token: @token, protocol: 'https'),
      style: 'background-color: #1976d2; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;' %>
</p>

<p>If you didn't request this change, please ignore this email.</p>

<p>Your password won't change until you access the link above and create a new one.</p>

4. Secure Password Reset

When users click the email link, they're directed to /reset-password/[token] where token validation and password creation happen:

export default function ResetPasswordForm({ 
  token, 
  onSubmit, 
  loading, 
  error 
}: ResetPasswordFormProps) {
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')
  const [passwordError, setPasswordError] = useState<string | null>(null)

  const validatePassword = (pwd: string): string | null => {
    if (pwd.length < 8) return 'Password must be at least 8 characters long'
    if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(pwd)) {
      return 'Password must contain at least one letter and one number'
    }
    return null
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    
    const passwordValidation = validatePassword(password)
    if (passwordValidation) {
      setPasswordError(passwordValidation)
      return
    }
    
    if (password !== confirmPassword) {
      setPasswordError('Passwords do not match')
      return
    }

    await onSubmit(token, password, confirmPassword)
  }
  
  // Material UI form implementation...
}

Why We Implemented This Feature

Security-First Approach

Our previous password reset implementation had several security gaps that needed addressing:

  1. No rate limiting - Users could spam reset requests
  2. Email enumeration vulnerability - Different responses revealed account existence
  3. Weak token validation - Insufficient protection against token manipulation
  4. Poor error handling - Generic errors didn't guide users effectively

User Experience Pain Points

The old system also created friction for legitimate users:

Development Challenges

1. Balancing Security with UX

One of the biggest challenges was implementing security measures without creating a frustrating user experience. For example, we needed to prevent email enumeration attacks while still providing helpful feedback to users.

Solution: We implemented consistent messaging that always indicates success, regardless of whether the email exists:

// API response always returns success
const response = {
  message: "If your email address exists in our database, you will receive password reset instructions shortly."
}

// But we log different outcomes for monitoring
if (userExists) {
  logger.info(`Password reset sent for ${email}`)
} else {
  logger.info(`Password reset attempted for non-existent email: ${email}`)
}

2. Rate Limiting Implementation

Implementing effective rate limiting required careful consideration of legitimate use cases versus potential abuse:

# Different limits for email vs IP to handle shared networks
EMAIL_RATE_LIMIT = 3.per.hour    # Per email address
IP_RATE_LIMIT = 5.per.hour       # Per IP address

def rate_limit_password_resets
  email_key = "password_reset_rate_limit:email:#{email}"
  ip_key = "password_reset_rate_limit:ip:#{request.remote_ip}"
  
  # Check both limits and provide appropriate feedback
  if exceeded_limits?
    render json: {
      error: "Too many password reset attempts. Please wait before trying again.",
      retry_after: 3600
    }, status: :too_many_requests
  end
end

3. Token Security and Validation

Ensuring reset tokens were secure while maintaining a smooth user experience required implementing robust validation:

# Backend token validation
def update
  self.resource = resource_class.reset_password_by_token(resource_params)
  
  if resource.errors.empty?
    resource.unlock_access! if unlockable?(resource)
    sign_in(resource_name, resource)
    render json: { 
      message: "Password successfully updated",
      redirect_url: after_resetting_password_path_for(resource)
    }, status: :ok
  else
    render json: {
      errors: resource.errors.full_messages
    }, status: :unprocessable_entity
  end
end

4. Cross-Platform Consistency

Maintaining consistent behavior across the Rails API and React frontend required careful coordination:

// Frontend API client matching backend expectations
export const resetPassword = async (
  token: string, 
  password: string, 
  passwordConfirmation: string
): Promise<AxiosResponse<ResetPasswordResponse>> => {
  return put(api.resetUserPassword(), {
    user: {
      reset_password_token: token,
      password: password,
      password_confirmation: passwordConfirmation
    }
  })
}

User Experience Improvements

1. Real-Time Validation

Users now receive immediate feedback as they type, preventing submission errors:

// Password strength indicator
const getPasswordStrength = (password: string): 'weak' | 'medium' | 'strong' => {
  if (password.length < 8) return 'weak'
  if (!/(?=.*[a-zA-Z])(?=.*\d)/.test(password)) return 'weak'  
  if (password.length >= 12 && /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/.test(password)) {
    return 'strong'
  }
  return 'medium'
}

// Visual feedback component
<LinearProgress 
  variant="determinate" 
  value={strengthPercentage} 
  color={strengthColor}
  sx={{ height: 8, borderRadius: 4 }}
/>

2. Mobile-Responsive Design

The new interface adapts seamlessly across all device sizes using Material UI's responsive system:

<Container maxWidth="sm">
  <Paper 
    elevation={3}
    sx={{
      padding: { xs: 3, sm: 4 },
      marginTop: { xs: 2, sm: 4 },
      marginBottom: { xs: 2, sm: 4 }
    }}
  >
    <Typography variant="h4" component="h1" gutterBottom align="center">
      Reset Your Password
    </Typography>
    {/* Form content... */}
  </Paper>
</Container>

3. Loading States and Error Handling

Clear visual feedback keeps users informed throughout the process:

<LoadingButton
  type="submit"
  fullWidth
  variant="contained"
  loading={loading}
  disabled={!email || !!emailError}
  sx={{ mt: 3, mb: 2, height: 48 }}
>
  {loading ? 'Sending Reset Link...' : 'Send Reset Link'}
</LoadingButton>

{error && (
  <Alert severity="error" sx={{ mt: 2 }}>
    {error}
  </Alert>
)}

{success && (
  <Alert severity="success" sx={{ mt: 2 }}>
    Check your email for reset instructions
  </Alert>
)}

Future Enhancement Plans

1. Multi-Factor Authentication Integration

We're planning to integrate MFA options for users who want additional security:

// Planned MFA integration
interface MFAOptions {
  totpEnabled: boolean
  backupCodesGenerated: boolean
  lastMFAVerification: Date | null
}

// Enhanced password reset with MFA consideration
const resetPasswordWithMFA = async (token: string, password: string, mfaCode?: string) => {
  // Implementation for MFA-protected accounts
}

2. Passwordless Authentication

Exploring magic link authentication as an alternative to traditional passwords:

# Future: Magic link generation
class MagicLinkService
  def generate_login_link(email)
    # Secure token generation for passwordless login
  end
end

3. Enhanced Security Monitoring

Planning to add comprehensive security event logging:

# Future: Security event tracking
class SecurityEventLogger
  def log_password_reset_attempt(email:, ip:, success:, reason: nil)
    SecurityEvent.create!(
      event_type: 'password_reset_attempt',
      email: email,
      ip_address: ip,
      success: success,
      additional_info: { reason: reason }
    )
  end
end

Conclusion

Building a secure, user-friendly password reset system required balancing multiple competing priorities: security, user experience, performance, and maintainability. The result is a robust authentication feature that protects user accounts while providing a smooth recovery experience.

Key takeaways from this implementation:

  1. Security doesn't have to sacrifice UX - Thoughtful implementation can enhance both
  2. Rate limiting is essential - But must be designed to accommodate legitimate use cases
  3. Consistent messaging prevents enumeration - While still providing helpful feedback
  4. Real-time validation improves conversion - Users prefer immediate feedback
  5. Mobile-first design is crucial - Authentication often happens on mobile devices

The modernized password reset system now serves as a foundation for future authentication enhancements, including MFA and passwordless login options. It demonstrates how modern web development practices can create secure, accessible, and delightful user experiences.


This feature was implemented as part of the ongoing evolution of our Feng Shui 2 RPG campaign management platform, built with Rails 8.0, Next.js 15, and Material UI v7. The complete implementation includes comprehensive testing, documentation, and follows OWASP security guidelines.