Files
aperonight/docs/ticket-download-security.md
kbe 8ad2194d48 Add security documentation for ticket download implementation and minor UI fixes
- Created comprehensive documentation for implementing secure unique IDs for ticket PDF downloads
- Document includes migration steps, model updates, controller changes, and security best practices
- Fixed minor spacing issues in orders index page
- Updated breadcrumb spacing for better visual hierarchy

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-09-07 01:09:00 +02:00

8.2 KiB

Ticket Download Security Implementation

Overview

This document describes how to implement secure unique identifiers for ticket PDF downloads to enhance security and prevent unauthorized access to user tickets.

Problem Statement

Currently, the ticket download functionality uses the QR code directly as an identifier in URLs. This approach presents several security risks:

  1. Predictability: QR codes may follow predictable patterns
  2. Information Disclosure: QR codes might reveal internal system information
  3. Brute Force Vulnerability: Attackers can enumerate valid tickets
  4. Lack of Revocability: Cannot invalidate download links without affecting the QR code

Solution

Implement a separate, cryptographically secure unique identifier specifically for PDF downloads.

Implementation Steps

1. Database Migration

Create a migration to add the new column:

# db/migrate/xxx_add_pdf_download_token_to_tickets.rb
class AddPdfDownloadTokenToTickets < ActiveRecord::Migration[7.0]
  def change
    add_column :tickets, :pdf_download_token, :string, limit: 50
    add_column :tickets, :pdf_download_token_expires_at, :datetime
    add_index :tickets, :pdf_download_token, unique: true
  end
end

2. Model Implementation

Update the Ticket model to generate secure tokens:

# app/models/ticket.rb
class Ticket < ApplicationRecord
  before_create :generate_pdf_download_token
  
  # Generate a secure token for PDF downloads
  def generate_pdf_download_token
    self.pdf_download_token = SecureRandom.urlsafe_base64(32)
    self.pdf_download_token_expires_at = 24.hours.from_now
  end
  
  # Check if the download token is still valid
  def pdf_download_token_valid?
    pdf_download_token.present? && 
    pdf_download_token_expires_at.present? && 
    pdf_download_token_expires_at > Time.current
  end
  
  # Regenerate token (useful for security or when token expires)
  def regenerate_pdf_download_token
    generate_pdf_download_token
    save!
  end
  
  # Ensure tokens are generated for existing records
  def ensure_pdf_download_token
    if pdf_download_token.blank?
      generate_pdf_download_token
      save!
    end
  end
end

3. Controller Updates

Update the TicketsController to use the new token system:

# app/controllers/tickets_controller.rb
class TicketsController < ApplicationController
  before_action :authenticate_user!
  
  def show
    @ticket = Ticket.joins(order: :user)
      .includes(:event, :ticket_type, order: :user)
      .find_by(tickets: { qr_code: params[:qr_code] })
    
    if @ticket.nil?
      redirect_to dashboard_path, alert: "Billet non trouvé"
      return
    end
    
    @event = @ticket.event
    @order = @ticket.order
  end
  
  def download
    # Find ticket by PDF download token instead of QR code
    @ticket = Ticket.find_by(pdf_download_token: params[:pdf_download_token])
    
    # Check if ticket exists
    if @ticket.nil?
      redirect_to dashboard_path, alert: "Lien de téléchargement invalide ou expiré"
      return
    end
    
    # Verify token validity
    unless @ticket.pdf_download_token_valid?
      redirect_to dashboard_path, alert: "Le lien de téléchargement a expiré"
      return
    end
    
    # Verify ownership
    unless @ticket.order.user == current_user
      redirect_to dashboard_path, alert: "Vous n'avez pas l'autorisation d'accéder à ce billet"
      return
    end
    
    # Generate and send PDF
    pdf_content = @ticket.to_pdf
    
    # Optionally regenerate token to make it single-use
    # @ticket.regenerate_pdf_download_token
    
    send_data pdf_content,
              filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf",
              type: "application/pdf",
              disposition: "attachment"
  rescue => e
    Rails.logger.error "Error generating ticket PDF: #{e.message}"
    redirect_to dashboard_path, alert: "Erreur lors de la génération du billet"
  end
end

4. Route Configuration

Update routes to use the new token-based system:

# config/routes.rb
Rails.application.routes.draw do
  # Existing routes...
  
  # Update ticket download route
  get "tickets/:pdf_download_token/download", to: "tickets#download", as: "ticket_download"
  
  # Keep existing show route for QR code functionality
  get "tickets/:qr_code", to: "tickets#show", as: "ticket"
end

5. View Updates

Update views to use the new download URL:

<!-- In app/views/tickets/show.html.erb -->
<%= link_to ticket_download_path(@ticket.pdf_download_token),
    class: "flex-1 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white font-medium py-3 px-6 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transform hover:-translate-y-0.5 text-center" do %>
  <svg class="w-4 h-4 inline-block mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
  </svg>
  Télécharger le PDF
<% end %>

6. Background Job for Token Management

Create a job to clean up expired tokens periodically:

# app/jobs/cleanup_expired_ticket_tokens_job.rb
class CleanupExpiredTicketTokensJob < ApplicationJob
  queue_as :default
  
  def perform
    # Clear expired tokens to free up database space
    Ticket.where("pdf_download_token_expires_at < ?", 1.week.ago)
          .update_all(pdf_download_token: nil, pdf_download_token_expires_at: nil)
  end
end

Schedule this job to run regularly:

# config/schedule.rb (if using whenever gem)
every 1.day, at: '4:30 am' do
  rake "tickets:cleanup_expired_tokens"
end

Security Benefits

  1. Unpredictability: Tokens are cryptographically secure and random
  2. Separation of Concerns: QR codes for physical entry, tokens for digital downloads
  3. Revocability: Tokens can be regenerated without affecting QR codes
  4. Expirability: Time-limited access prevents long-term exposure
  5. Ownership Verification: Additional checks ensure only ticket owners can download
  6. Audit Trail: Token usage can be logged for security monitoring

Additional Security Considerations

Rate Limiting

Implement rate limiting to prevent abuse:

# In ApplicationController or specific controller
before_action :rate_limit_downloads, only: [:download]

def rate_limit_downloads
  if Rails.cache.read("download_attempts_#{current_user.id}")&.to_i > 10
    render json: { error: "Too many download attempts" }, status: :too_many_requests
  else
    Rails.cache.write("download_attempts_#{current_user.id}", 
                      (Rails.cache.read("download_attempts_#{current_user.id}") || 0) + 1,
                      expires_in: 1.hour)
  end
end

Logging

Add logging for security monitoring:

# In TicketsController#download
Rails.logger.info "Ticket PDF download attempted - User: #{current_user.id}, Ticket: #{@ticket.id}, Token: #{params[:pdf_download_token]}"

Migration Process

  1. Run the database migration
  2. Update existing tickets with tokens:
    # In rails console or a rake task
    Ticket.find_each(&:ensure_pdf_download_token)
    
  3. Deploy code changes
  4. Update any external references to use the new system
  5. Monitor for issues and adjust expiration times as needed

Testing

Ensure comprehensive testing of the new functionality:

# spec/controllers/tickets_controller_spec.rb
RSpec.describe TicketsController, type: :controller do
  describe "GET #download" do
    it "downloads PDF for valid token" do
      # Test implementation
    end
    
    it "rejects expired tokens" do
      # Test implementation
    end
    
    it "rejects invalid tokens" do
      # Test implementation
    end
    
    it "verifies ticket ownership" do
      # Test implementation
    end
  end
end

Conclusion

This implementation provides a robust security framework for ticket PDF downloads while maintaining usability. The separation of QR codes (for physical entry) and download tokens (for digital access) follows security best practices and provides multiple layers of protection against unauthorized access.