node.jstwiliotwilio-apifastify

How to validate twilio signature in fastify


i have been struggling to get the twilio signature validation to work on a fastify server.

Has anyone here implemented the signature validation successfully in fastify, and can give me a quick hand?

VerifyTwilioSignature function:

export const verifyTwilioSignature = (req: FastifyRequest, reply: FastifyReply, done: any) => {
  const twilioSignature = req.headers["x-twilio-signature"] as string
  const fullUrl = `${req.protocol}://${req.headers.host}${req.raw.url}`
  const authToken = getEnvVar("TWILIO_AUTH_TOKEN")

  if (!authToken || !twilioSignature) {
    reply.status(403).send({ error: "Missing Twilio Signature or authToken" })
    return
  }

  const rawBody = req.rawBody as string
  const params = Object.fromEntries(new URLSearchParams(rawBody))

  const isValid = validateRequest(authToken, twilioSignature, fullUrl, params)

  if (!isValid) {
    reply.status(403).send({ error: "Invalid Twilio Signature" })
    return
  }

  done()
}

Route:

export const twilioWebhookRoutes: FastifyPluginCallback = (app, _, done) => {
  app.post("/webhook/twilio/sms", {
    config: { rawBody: true },
    preHandler: verifyTwilioSignature,
    schema: twilioSmsWebhookSchema,
    handler: twilioSmsWebhook,
  })
  done()
}

Solution

  • I ended up implementing the validation algorithm myself:

    import crypto from "crypto"
    import { FastifyReply, FastifyRequest } from "fastify"
    import { getEnvVar } from "../../../utils"
    
    export const verifyTwilioSignature = (req: FastifyRequest, reply: FastifyReply, done: any) => {
      const twilioSignature = req.headers["x-twilio-signature"] as string
      const authToken = getEnvVar("TWILIO_AUTH_TOKEN")
    
      if (!authToken || !twilioSignature) {
        reply.status(403).send({ error: "Missing Twilio Signature or authToken" })
        return
      }
    
      const fullUrl = `https://${req.headers.host}${req.raw.url}`
    
      const rawBody = req.rawBody as string
    
      const params = Object.fromEntries(new URLSearchParams(rawBody))
    
      const sortedParamsString = Object.keys(params)
        .sort()
        .map((key) => `${key}${params[key]}`)
        .join("")
    
      const data = fullUrl + sortedParamsString
    
      const computedSignature = crypto.createHmac("sha1", authToken).update(data).digest("base64")
    
      const isValid = crypto.timingSafeEqual(Buffer.from(twilioSignature), Buffer.from(computedSignature))
    
      if (!isValid) {
        reply.status(403).send({ error: "Invalid Twilio Signature" })
        return
      }
    
      done()
    }