ruby

How to Implement Two-Factor Authentication in Ruby on Rails: Complete Guide 2024

Learn how to implement secure two-factor authentication (2FA) in Ruby on Rails. Discover code examples for TOTP, SMS verification, backup codes, and security best practices to protect your web application.

How to Implement Two-Factor Authentication in Ruby on Rails: Complete Guide 2024

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.

Keywords: two factor authentication rails, rails 2fa implementation, ruby on rails authentication security, TOTP rails, SMS verification rails, two factor auth devise, rails secure authentication, backup codes rails, rate limiting authentication rails, 2fa recovery rails, rails session security, QR code authentication rails, rails authentication best practices, secure user authentication rails, multi factor authentication ruby, rails authentication controller, rails security logging, devise two factor setup, rails OTP implementation, authentication monitoring rails, devise SMS verification, rails authentication flow, 2fa security measures rails, two step verification rails, ROTP implementation rails, rails auth token security, twilio rails authentication, rails password security, rails secure session management, rails authentication testing, 2fa backup solutions rails



Similar Posts
Blog Image
Mastering Rust's Advanced Trait System: Boost Your Code's Power and Flexibility

Rust's trait system offers advanced techniques for flexible, reusable code. Associated types allow placeholder types in traits. Higher-ranked trait bounds work with traits having lifetimes. Negative trait bounds specify what traits a type must not implement. Complex constraints on generic parameters enable flexible, type-safe APIs. These features improve code quality, enable extensible systems, and leverage Rust's powerful type system for better abstractions.

Blog Image
Unleash Ruby's Hidden Power: Enumerator Lazy Transforms Big Data Processing

Ruby's Enumerator Lazy enables efficient processing of large or infinite data sets. It uses on-demand evaluation, conserving memory and allowing work with potentially endless sequences. This powerful feature enhances code readability and performance when handling big data.

Blog Image
What Ruby Magic Can Make Your Code Bulletproof?

Magic Tweaks in Ruby: Refinements Over Monkey Patching

Blog Image
Unlocking Ruby's Hidden Gem: Mastering Refinements for Powerful, Flexible Code

Ruby refinements allow temporary, scoped modifications to classes without global effects. They offer precise control for adding or overriding methods, enabling flexible code changes and creating domain-specific languages within Ruby.

Blog Image
Ever Wonder How Benchmarking Can Make Your Ruby Code Fly?

Making Ruby Code Fly: A Deep Dive into Benchmarking and Performance Tuning

Blog Image
Rust's Const Trait Impl: Boosting Compile-Time Safety and Performance

Const trait impl in Rust enables complex compile-time programming, allowing developers to create sophisticated type-level state machines, perform arithmetic at the type level, and design APIs with strong compile-time guarantees. This feature enhances code safety and expressiveness but requires careful use to maintain readability and manage compile times.