- 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>
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:
- Predictability: QR codes may follow predictable patterns
- Information Disclosure: QR codes might reveal internal system information
- Brute Force Vulnerability: Attackers can enumerate valid tickets
- 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
- Unpredictability: Tokens are cryptographically secure and random
- Separation of Concerns: QR codes for physical entry, tokens for digital downloads
- Revocability: Tokens can be regenerated without affecting QR codes
- Expirability: Time-limited access prevents long-term exposure
- Ownership Verification: Additional checks ensure only ticket owners can download
- 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
- Run the database migration
- Update existing tickets with tokens:
# In rails console or a rake task Ticket.find_each(&:ensure_pdf_download_token) - Deploy code changes
- Update any external references to use the new system
- 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.