In this guide, you'll learn how to secure your Gin application by validating incoming requests to your Twilio webhooks are, in fact, from Twilio.
With a few lines of code, you'll write a custom middleware for our Gin project that uses the Twilio Go SDK's validator struct method. You can then apply that middleware to any route which accepts Twilio webhooks to confirm that incoming requests genuinely originated from Twilio.
Let's get started!
The Twilio Go SDK includes a RequestValidator
struct that you can use to validate incoming requests.
Building this into a middleware is a great way to reuse our validation logic across all routes that accept incoming requests from Twilio.
Confirm incoming requests to your Gin routes are genuine with this custom middleware.
_61package main_61_61import (_61 "fmt"_61 "net/http"_61 "os"_61_61 "github.com/gin-gonic/gin"_61 "github.com/twilio/twilio-go/client"_61 "github.com/twilio/twilio-go/twiml"_61)_61_61// Custom Gin middleware that rejects non-Twilio requests_61func requireValidTwilioSignature(validator *client.RequestValidator) gin.HandlerFunc {_61 return func(context *gin.Context) {_61 // Your url will vary depending on your environment and how your application is deployed_61 // Modify this url declaration sample as necessary_61 url := "https://some-digits.ngrok.io" + context.Request.URL.Path_61 signatureHeader := context.Request.Header.Get("X-Twilio-Signature")_61 params := make(map[string]string)_61 context.Request.ParseForm()_61 for key, value := range context.Request.PostForm {_61 params[key] = value[0]_61 }_61_61 // Requests are validated based on the incoming url, parameters,_61 // and the X-Twilio-Signature header._61 // If the request is not valid, return a 403 error_61 if !validator.Validate(url, params, signatureHeader) {_61 fmt.Println("Request isn't from Twilio 🚫")_61 context.AbortWithStatus(http.StatusForbidden)_61 return_61 }_61 // If the request is valid, execute the next middleware (in this case, the route handler)_61 context.Next()_61 }_61}_61_61func main() {_61 router := gin.Default()_61 // Create a RequestValidator instance_61 requestValidator := client.NewRequestValidator(os.Getenv("TWILIO_AUTH_TOKEN"))_61_61 // Apply the requireValidTwilioSignature middleware to your route handler(s), before any_61 // code that you want to only apply to validated requests_61 router.POST("/sms", requireValidTwilioSignature(&requestValidator), func(context *gin.Context) {_61 message := &twiml.MessagingMessage{_61 Body: "Yay, valid requests!",_61 }_61_61 twimlResult, err := twiml.Messages([]twiml.Element{message})_61 if err != nil {_61 context.String(http.StatusInternalServerError, err.Error())_61 } else {_61 context.Header("Content-Type", "text/xml")_61 context.String(http.StatusOK, twimlResult)_61 }_61 })_61_61 router.Run(":3000")_61}
To validate an incoming request genuinely originated from Twilio, you first need to create an instance of the RequestValidator
struct using our Twilio auth token. After that you call its Validate
method, passing in the request's URL, payload, and the value of the request's X-TWILIO-SIGNATURE
header. Remember that the incoming request from a Twilio webhook is of type application/x-www-form-urlencoded
, so you will need to create a map
and populate it with all of the key/value pairs from the request in order for this to work.
The Validate
method will return the boolean true
if the request is valid or false
if it isn't. Based on this result, this middleware then either calls context.Next()
to pass the request onto the next middleware or handler code or returns a 403 HTTP response for inauthentic requests.
To apply this middleware, add it to the list of handlers for any route before any code that requires this validation.
Your request validator may fail locally when you use a ngrok tunnel, or in production if your app is behind a load balancer, proxy, etc. This is because the request URL that your Gin application sees does not match the URL Twilio used to reach your application.
To fix this for local development with ngrok, you may want to manually set the value of the URL based on your tunnel's scheme and host. To fix this in your production app, your middleware will need to reconstruct the request's original URL using deployment environment variables and request headers like X-Forwarded-Proto
, if available.
If you write tests for your Gin app, those tests may fail for routes where you use your Twilio request validation middleware. Any requests your test suite sends to those routes will fail the validation check.
To fix this problem, we recommend adding an extra check in your middleware, telling it to only reject invalid requests if your app is running outside of a test environment. Implementation details such as checking for Go Flags vs environment variables will very depending on your development stack, but the principle remains the same.
Disable webhook validation during testing.
_75import (_75 "flag"_75 "fmt"_75 "net/http"_75 "os"_75 "testing"_75_75 "github.com/gin-gonic/gin"_75 "github.com/twilio/twilio-go/client"_75 "github.com/twilio/twilio-go/twiml"_75)_75_75// The init function is a great place to prepare application state prior to execution_75// In this case, parsing input flags to your app_75func init() {_75 testing.Init()_75 flag.Parse()_75}_75_75// Helper method to determine if your Go code is being run in test mode_75func IsTestRun() bool {_75 return flag.Lookup("test.v").Value.(flag.Getter).Get().(bool)_75 // Some teams may prefer to use env vars to indicate testing instead, such as_75 // return os.Getenv("GO_ENV") == "testing"_75}_75_75// Custom Gin middleware that rejects non-Twilio requests_75func requireValidTwilioSignature(validator *client.RequestValidator) gin.HandlerFunc {_75 return func(context *gin.Context) {_75 // Your url will vary depending on your environment and how your application is deployed_75 // Modify this url declaration sample as necessary_75 url := "https://some-digits.ngrok.io" + context.Request.URL.Path_75 signatureHeader := context.Request.Header.Get("X-Twilio-Signature")_75 params := make(map[string]string)_75 context.Request.ParseForm()_75 for key, value := range context.Request.PostForm {_75 params[key] = value[0]_75 }_75_75 // Requests are validated based on the incoming url, parameters,_75 // and the X-Twilio-Signature header._75 // If the request is not valid AND this isn't being run in a test env, return a 403 error_75 if !validator.Validate(url, params, signatureHeader) && !IsTestRun() {_75 fmt.Println("Request isn't from Twilio 🚫")_75 context.AbortWithStatus(http.StatusForbidden)_75 return_75 }_75 // If the request is valid, execute the next middleware (in this case, the route handler)_75 context.Next()_75 }_75}_75_75func main() {_75 router := gin.Default()_75 // Create a RequestValidator instance_75 requestValidator := client.NewRequestValidator(os.Getenv("TWILIO_AUTH_TOKEN"))_75_75 // Apply the requireValidTwilioSignature middleware to your route handler(s), before any_75 // code that you want to only apply to validated requests_75 router.POST("/sms", requireValidTwilioSignature(&requestValidator), func(context *gin.Context) {_75 message := &twiml.MessagingMessage{_75 Body: "Yay, valid requests!",_75 }_75_75 twimlResult, err := twiml.Messages([]twiml.Element{message})_75 if err != nil {_75 context.String(http.StatusInternalServerError, err.Error())_75 } else {_75 context.Header("Content-Type", "text/xml")_75 context.String(http.StatusOK, twimlResult)_75 }_75 })_75_75 router.Run(":3000")_75}
Validating requests to your Twilio webhooks is a great first step for securing your Twilio application. We recommend reading over our full security documentation for more advice on protecting your app, and the Anti-Fraud Developer's Guide in particular.