Two-factor authentication (2FA) has become essential for modern web applications, providing an additional security layer beyond traditional password-based authentication. I’ll share my experience implementing various 2FA methods in Ruby on Rails applications.
Authentication Foundation
The core of 2FA implementation starts with a solid authentication system. Rails provides excellent tools through libraries like Devise, but we need to extend them for 2FA support. Here’s the basic setup:
class User < ApplicationRecord
  devise :two_factor_authenticatable,
         :two_factor_backupable
  encrypts :otp_secret
  validates :phone_number, presence: true, if: :sms_enabled?
end
TOTP Implementation
Time-based One-Time Passwords (TOTP) offer reliable security. The implementation requires generating and storing a secret key:
class TotpService
  def initialize(user)
    @user = user
    @totp = ROTP::TOTP.new(@user.otp_secret)
  end
  def generate_secret
    ROTP::Base32.random
  end
  def verify_code(code)
    @totp.verify(code, drift_behind: 15)
  end
  def provisioning_uri
    @totp.provisioning_uri(@user.email)
  end
end
SMS Verification
SMS verification provides familiar security for users. Here’s how to implement it using Twilio:
class SmsVerification
  def initialize(user)
    @user = user
    @client = Twilio::REST::Client.new
  end
  def send_code
    code = generate_code
    @user.update(sms_code: code, code_sent_at: Time.current)
    
    @client.messages.create(
      from: Rails.application.credentials.twilio_phone,
      to: @user.phone_number,
      body: "Your verification code is: #{code}"
    )
  end
  private
  def generate_code
    SecureRandom.random_number(100000..999999).to_s
  end
end
Backup Codes Generation
Users need backup codes for account recovery. Generate them securely:
class BackupCodesGenerator
  def generate
    Array.new(10) do
      SecureRandom.hex(4)
    end
  end
  def hash_codes(codes)
    codes.map { |code| BCrypt::Password.create(code) }
  end
  def verify_code(input, hashed_codes)
    hashed_codes.any? do |stored_code|
      BCrypt::Password.new(stored_code) == input
    end
  end
end
Rate Limiting
Protect against brute force attacks with rate limiting:
class RateLimiter
  def initialize(user)
    @user = user
    @redis = Redis.new
  end
  def within_limit?
    attempts = @redis.get(cache_key).to_i
    attempts < max_attempts
  end
  def increment
    @redis.multi do |multi|
      multi.incr(cache_key)
      multi.expire(cache_key, timeout)
    end
  end
  private
  def cache_key
    "2fa_attempts:#{@user.id}"
  end
  def max_attempts
    5
  end
  def timeout
    1.hour.to_i
  end
end
Recovery Process
Implement a secure recovery workflow:
class RecoveryProcess
  def initialize(user)
    @user = user
  end
  def start_recovery
    token = generate_recovery_token
    send_recovery_email(token)
    @user.update(recovery_token: token)
  end
  def verify_token(token)
    return false unless @user.recovery_token_valid?
    @user.recovery_token == token
  end
  private
  def generate_recovery_token
    SecureRandom.urlsafe_base64(32)
  end
  def send_recovery_email(token)
    RecoveryMailer.send_instructions(@user, token).deliver_later
  end
end
Session Management
Maintain secure sessions during 2FA:
class TwoFactorSession
  def initialize(session)
    @session = session
  end
  def mark_as_pending(user_id)
    @session[:pending_2fa_user_id] = user_id
    @session[:pending_2fa_timestamp] = Time.current.to_i
  end
  def complete_authentication
    @session.delete(:pending_2fa_user_id)
    @session.delete(:pending_2fa_timestamp)
    @session[:authenticated_at] = Time.current.to_i
  end
  def expired?
    return true unless @session[:pending_2fa_timestamp]
    Time.current.to_i - @session[:pending_2fa_timestamp] > 10.minutes.to_i
  end
end
Security Logging
Track authentication attempts and security events:
class SecurityLogger
  def initialize(user)
    @user = user
  end
  def log_2fa_attempt(success:, method:, ip_address:)
    SecurityLog.create!(
      user: @user,
      event_type: '2fa_attempt',
      success: success,
      method: method,
      ip_address: ip_address,
      metadata: {
        user_agent: Current.user_agent,
        location: geolocate(ip_address)
      }
    )
  end
  private
  def geolocate(ip_address)
    Geocoder.search(ip_address).first&.country
  end
end
QR Code Generation
Generate QR codes for TOTP setup:
class QrCodeGenerator
  def initialize(user)
    @user = user
    @totp = ROTP::TOTP.new(@user.otp_secret)
  end
  def generate
    RQRCode::QRCode.new(
      @totp.provisioning_uri(@user.email)
    ).as_png(
      size: 300,
      border_modules: 2
    )
  end
end
Controller Integration
Tie everything together in the controller:
class TwoFactorAuthenticationController < ApplicationController
  before_action :require_login
  before_action :check_rate_limit, only: [:verify]
  def setup
    service = TotpService.new(current_user)
    @secret = service.generate_secret
    @qr_code = QrCodeGenerator.new(current_user).generate
  end
  def verify
    result = verify_2fa_code(params[:code])
    
    if result
      session_manager.complete_authentication
      redirect_to dashboard_path
    else
      rate_limiter.increment
      flash.now[:error] = 'Invalid code'
      render :verify
    end
  end
  private
  def verify_2fa_code(code)
    service = TotpService.new(current_user)
    result = service.verify_code(code)
    
    SecurityLogger.new(current_user).log_2fa_attempt(
      success: result,
      method: 'totp',
      ip_address: request.remote_ip
    )
    
    result
  end
  def session_manager
    @session_manager ||= TwoFactorSession.new(session)
  end
  def rate_limiter
    @rate_limiter ||= RateLimiter.new(current_user)
  end
  def check_rate_limit
    unless rate_limiter.within_limit?
      redirect_to lockout_path
    end
  end
end
The implementation of 2FA requires careful consideration of security, user experience, and edge cases. Regular security audits and updates are essential to maintain strong protection. Testing various scenarios, including backup code usage and rate limiting, ensures robust implementation.
Remember to implement proper error handling, validation, and user feedback throughout the authentication flow. Consider implementing progressive security measures based on user behavior and risk assessment.
Monitor authentication attempts and analyze patterns to detect potential security threats. Regular backups of 2FA-related data and proper encryption of sensitive information are crucial for maintaining security standards.