Sample HTTP SNS Subscriber

Description

Simple node express application that can confirm subscription to an AWS SNS Topic and process messages that are published to subscribed topics. The axios and express packages are dependencies of this example.

app.js

Main function that receives /POST requests:

    const express = require('express')
    const bodyParser = require('body-parser')
    const https = require('https')
    const app = express()
    const port = 3000
    const messageValidator = require('./messageValidator')

    app.use(bodyParser.text())

    app.post('/', async (req, res) => {
      const body = JSON.parse(req.body)

      if (req.get('x-amz-sns-message-type') == null){
        return
      }

      if (await messageValidator.isValidSignature(body)){
        handleMessage(body)
      } else {
        throw 'Message signature is not valid'
      }
    })

    app.listen(port, () => console.log(`HTTP subscriber listening at http://localhost:${port}`))

    function handleMessage(body){
      switch(body.Type) {
        case 'SubscriptionConfirmation':
          confirmSubscription(body.SubscribeURL)
          break
        case 'Notification':
          handleNotification(body)
          break
        default:
          return
      }
    }

    function confirmSubscription(subscriptionUrl){
      https.get(subscriptionUrl)
      console.log('Subscription confirmed')
    }

    function handleNotification(body){
      console.log(`Received message from SNS: ${body.Message}`)
    }

messageValidator.js

Checks signature version and verifies message signature by:

Futher information here: https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html

    const url = require('url');
    const crypto = require('crypto')
    const axios = require('axios')

    async function isValidSignature(body) {

      verifyMessageSignatureVersion(body.SignatureVersion)

      const certificate = await downloadCertificate(body.SigningCertURL)
      return validateSignature(body, certificate)
    }

    function verifyMessageSignatureVersion(version) {
      if (version != 1){
        throw "Signature verification failed"
      }
    }

    function verifyMessageSignatureURL(certURL) {
      if (url.parse(certURL).protocol != 'https:') {
        throw "SigningCertURL was not using HTTPS"
      }
    }

    async function downloadCertificate(certURL){
      verifyMessageSignatureURL(certURL)

      try {
        const response = await axios.get(certURL)
        return response.data
      } catch (err){
        throw `Error fetching certificate: ${err}`
      }
    }

    async function validateSignature(message, certificate) {
      const verify = crypto.createVerify('sha1WithRSAEncryption');
      verify.write(getMessageToSign(message))
      verify.end();

      return verify.verify(certificate, message.Signature, 'base64')
    }

    function getMessageToSign(body){
      switch(body.Type) {
        case 'SubscriptionConfirmation':
          return buildSubscriptionStringToSign(body)
        case 'Notification':
          return buildNotificationStringToSign(body)
        default:
          return
      }
    }

    function buildNotificationStringToSign(body) {
      let stringToSign = ''

      stringToSign = "Message\n"
      stringToSign += body.Message + "\n"
      stringToSign += "MessageId\n"
      stringToSign += body.MessageId + "\n"
      if (body.Subject) {
          stringToSign += "Subject\n"
          stringToSign += body.Subject + "\n"
      }
      stringToSign += "Timestamp\n"
      stringToSign += body.Timestamp + "\n"
      stringToSign += "TopicArn\n"
      stringToSign += body.TopicArn + "\n"
      stringToSign += "Type\n"
      stringToSign += body.Type + "\n"

      return stringToSign
    }

    function buildSubscriptionStringToSign(body) {
      let stringToSign = ''

      stringToSign = "Message\n";
      stringToSign += body.Message + "\n";
      stringToSign += "MessageId\n";
      stringToSign += body.MessageId + "\n";
      stringToSign += "SubscribeURL\n";
      stringToSign += body.SubscribeURL + "\n";
      stringToSign += "Timestamp\n";
      stringToSign += body.Timestamp + "\n";
      stringToSign += "Token\n";
      stringToSign += body.Token + "\n";
      stringToSign += "TopicArn\n";
      stringToSign += body.TopicArn + "\n";
      stringToSign += "Type\n";
      stringToSign += body.Type + "\n";

      return stringToSign;
    }


    module.exports = { isValidSignature }