module Webhooks
class StripeController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
begin
event = Stripe::Webhook.construct_event(
payload, sig_header, Rails.application.credentials.stripe[:webhook_secret]
)
rescue Stripe::SignatureVerificationError => e
Rails.logger.warn("Webhook signature verification failed: #{e.message}")
return render json: { error: 'INVALID_SIGNATURE' }, status: :unauthorized
end
# Process the verified event
case event.type
when 'payment_intent.succeeded'
ProcessPaymentSuccessWorker.perform_async(event.data.object.id)
when 'customer.subscription.deleted'
ProcessSubscriptionCancelledWorker.perform_async(event.data.object.id)
end
render json: { received: true }, status: :ok
end
end
end
When receiving webhooks from external services, signature verification ensures the payload comes from the claimed sender and hasn't been tampered with. Services like Stripe and GitHub include an HMAC signature in headers computed from the request body and a shared secret. I recompute the HMAC on the received body using the same secret and compare it to the provided signature using constant-time comparison to prevent timing attacks. The raw request body must be used for signature verification, not the parsed version, which is why I sometimes need to read request.body.read before Rails parses it. Failed verification should return 401 immediately without processing the payload to avoid acting on forged requests.
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.