# 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: ```ruby # 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: ```ruby # 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: ```ruby # 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: ```ruby # 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: ```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 %> Télécharger le PDF <% end %> ``` ### 6. Background Job for Token Management Create a job to clean up expired tokens periodically: ```ruby # 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: ```ruby # 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: ```ruby # 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: ```ruby # 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: ```ruby # 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: ```ruby # 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.