Handling AWS SNS notifications in Ruby on Rails

Today I’m going to share with you how to handle AWS SNS notifications in a Ruby on Rails app. An SNS topic publishes every message it receives to every subscriber, and our app is subscribed to receive these messages via HTTP.

SNS messages come in 2 types:

  • SubscriptionConfirmation: Is sent when a subscription is created to confirm that the endpoint is up an running. The confirm_subscription method handles this.
  • Notification: Once the subscription is confirmed, all the following messages will be of this type. These are the messages that are sent to a topic and then fanned out to every subscriber out there.

The verify_request_authenticity method is called first to verify the authenticity of the SNS message. It checks that the message is not empty and verifies its origin using the MessageVerifier class. Finally, the create method parses the message content accordingly.

I hope this can be of great help to everyone out there. Feel free to comment if you have any questions or feedback.

# frozen_string_literal: true

# Notifications coming from SNS
class NotificationsController < ActionController::Base
  before_action :verify_request_authenticity

  # @url /notifications
  #
  # @action POST
  #
  # Broadcasts a SNS message to an ActionCable channel
  #
  #
  def create
    case message_body['Type']
    when 'SubscriptionConfirmation'
      confirm_subscription ? (head :ok) : (head :bad_request)
    when 'Notification'
      message = JSON.parse(message_body['Message'])
      BroadcastExecutionNotificationJob.perform_later(message)
      head :ok
    end
  end

  private

  def verify_request_authenticity
    head :unauthorized if raw_post.blank? || !message_verifier.authentic?(raw_post)
  end

  def raw_post
    @raw_post ||= request.raw_post
  end

  def message_body
    @message_body ||= JSON.parse(raw_post)
  end

  def message_verifier
    @message_verifier ||= Aws::SNS::MessageVerifier.new
  end

  def confirm_subscription
    AWS_SNS_CLIENT.confirm_subscription(
      topic_arn: message_body['TopicArn'],
      token: message_body['Token']
    )
    true
  rescue Aws::SNS::Errors::ServiceError => e
    Rails.logger.info(e.message)
    false
  end
end