class DownloadsController < ApplicationController
before_action :authenticate_user!, only: :create
skip_before_action :verify_authenticity_token, only: :show
def create
document = current_user.documents.find(params[:document_id])
params_hash = SignedUrl.for_download(resource_id: document.id, user_id: current_user.id)
render json: { url: download_url(params_hash) }
end
def show
valid = SignedUrl.verify(
resource_id: params[:resource_id],
user_id: params[:user_id],
expires: params[:expires],
signature: params[:signature]
)
return head(:forbidden) unless valid
document = Document.find_by(id: params[:resource_id], user_id: params[:user_id])
return head(:not_found) unless document&.file&.attached?
send_file(
ActiveStorage::Blob.service.path_for(document.file.key),
filename: document.file.filename.to_s,
disposition: :attachment
)
end
end
class SignedUrl
DELIMITER = "|".freeze
def self.secret
Rails.application.secret_key_base
end
def self.payload(resource_id:, user_id:, expires_at:)
[resource_id, user_id, expires_at.to_i].join(DELIMITER)
end
def self.sign(resource_id:, user_id:, expires_at:)
data = payload(resource_id: resource_id, user_id: user_id, expires_at: expires_at)
digest = OpenSSL::HMAC.digest("SHA256", secret, data)
Base64.urlsafe_encode64(digest, padding: false)
end
def self.for_download(resource_id:, user_id:, ttl: 15.minutes)
expires_at = ttl.from_now
{
resource_id: resource_id,
user_id: user_id,
expires: expires_at.to_i,
signature: sign(resource_id: resource_id, user_id: user_id, expires_at: expires_at)
}
end
def self.verify(resource_id:, user_id:, expires:, signature:)
expires_at = Time.at(expires.to_i)
return false if expires_at < Time.current
expected = sign(resource_id: resource_id, user_id: user_id, expires_at: expires_at)
ActiveSupport::SecurityUtils.secure_compare(expected, signature.to_s)
end
end
Rails.application.routes.draw do
resources :documents, only: [] do
post "downloads", to: "downloads#create", as: :create_download
end
get "download", to: "downloads#show", as: :download
end
This snippet shows how to hand out time-limited download links that carry their own proof of authenticity, so a controller can serve a protected file without a database lookup for every click. The core idea is an HMAC signature over the parameters that matter — the resource id, the recipient, and an expiry timestamp — using a server-only secret. Because the signature depends on the secret, a client cannot forge or tamper with the link, and because the expiry is part of the signed payload, the URL simply stops working after a deadline.
In SignedUrl, the service treats a link as a self-describing token. payload builds a canonical string by joining the fields with a delimiter; keeping the exact same field order on both sign and verify is critical, since HMAC is byte-sensitive. sign produces a URL-safe Base64 digest via OpenSSL::HMAC, and for_download packages the query params a controller will emit. verify recomputes the signature and compares it — importantly with ActiveSupport::SecurityUtils.secure_compare, which runs in constant time to avoid leaking information through timing differences. It also rejects expired tokens by checking expires_at against the clock, so a leaked-but-stale link is useless.
The Rails.application.secret_key_base default ties the secret to the app's existing key management. Deriving a purpose-specific key instead (for example via a key generator) would be a reasonable hardening step, since reusing secret_key_base across many features widens the blast radius if the signing logic is ever misused.
In DownloadsController, create is the authenticated endpoint that mints a link for the current user; only a logged-in owner can generate one. show is deliberately unauthenticated at the session level — it trusts the signature instead — which is what makes these links shareable and CDN-friendly. It calls SignedUrl.verify and returns 403 on any failure, whether the signature is wrong, the params were altered, or the deadline passed. On success it streams the file with send_file and disposition: :attachment.
The trade-off is that revocation is hard: once issued, a valid token works until it expires, so expiries should be short. This pattern fits presigned downloads, email links, and webhook callbacks where stateless verification beats a per-request database check.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post
data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)
HMAC signed API requests for webhook and partner integrity
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
Share this code
Here's the card — post it anywhere.