class PasswordResetsController < ApplicationController
before_action :find_user_by_token, only: [:edit, :update]
def create
user = User.find_by(email: params[:email]&.downcase)
if user
token = user.signed_id(purpose: :password_reset, expires_in: 1.hour)
UserMailer.password_reset(user, token).deliver_later
end
# Always show success to prevent email enumeration
render json: { message: 'If that email exists, a reset link has been sent' }, status: :ok
end
def update
if @user.update(password: params[:password], password_confirmation: params[:password_confirmation])
# Invalidate all sessions
@user.update_column(:auth_token, SecureRandom.hex(32))
render json: { message: 'Password reset successfully' }, status: :ok
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
private
def find_user_by_token
@user = User.find_signed(params[:token], purpose: :password_reset)
unless @user
render json: { error: 'INVALID_OR_EXPIRED_TOKEN' }, status: :unauthorized
end
end
end
Password reset workflows require careful security design to prevent account takeover. I generate time-limited, single-use tokens using Rails' signed_id feature which creates tamper-proof tokens without database storage. The token expires after a short window (1-2 hours) and includes the user's password_digest in the signature, so it automatically invalidates when the password changes. I send reset links via email to the address on file, never to user-provided addresses. The reset form requires the new password twice and doesn't echo the current password. Rate limiting prevents brute force token guessing. After successful reset, I invalidate all active sessions to protect against session fixation. This flow balances security and user experience.