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()
}
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()
}