We’re building a phone-intake AI voice bot that is connected for Virginia Legal Aid Society (VLAS) with Twilio, Pipecat, and FastAPI (Github). Without proper safeguards, this system could be exploited by attackers who go after our backend directly. That opens the door to unauthorized traffic, inflated usage costs, and degraded service quality for our users.

To prevent this, we need to ensure that every request truly originates from Twilio’s phone infrastructure and that each WebSocket connection is approved by our servers using cryptographically secure tokens (authentication codes). This not only protects against abuse but also supports scalable architecture; allowing webhook and WebSocket handlers to live on separate servers as long as they share a secret key.

In this post, we’ll walk through a three-step strategy we’re using to secure our Twilio integration for VLAS:

  1. Validating Twilio webhooks.
  2. Generating a WebSocket authentication code and sending it to Twilio.
  3. Verifying the WebSocket authentication code before proceeding with the WebSocket connection.

1) Validating Twilio webhooks

When Twilio receives a call to our registered phone number, Twilio sends a request to a specified webhook endpoint. Our goal is to connect the phone call to our AI voice bot using WebSockets, but before we do that, we need to ensure that the webhook is a legitimate request from Twilio. The webhook endpoint also needs to return a response in Twilio’s TwiML (Twilio Markup Language) format which tells Twilio that we want it to connect the call to our WebSocket endpoint.

This is the webhook endpoint:

Once we receive a Twilio webhook request we validate the it using the Twilio library’s RequestValidator. This uses our TWILIO_AUTH_TOKEN (stored as a secret/environment variable), the webhook endpoint’s url (matching the one in our Twilio dashboard), and all of the form_data to validate the request and confirm to us that the webhook request is authentic and came from Twilio.

2) Generating a WebSocket authentication code and sending it to Twilio

To ensure the security and integrity of our AI voice bot, it’s essential that incoming WebSocket connections are both legitimately initiated by Twilio and tied to a validated webhook request. This verification step allows us to maintain strict control over access to the AI voice bot from the Twilio and phone system side. Without proper protection, the WebSocket endpoint could be exploited by malicious actors who bypass our safeguards and connect directly to the bot. This could result in a surge of unauthorized traffic, driving up operational costs and overwhelming system resources far beyond what our infrastructure is designed to handle.

TwiML allows us to include custom parameters in the response we send back from our webhook endpoint. These parameters are then passed back to us with the initial WebSocket connection. By generating unique authentication codes and including them in the TwiML response, we can ensure that each WebSocket connection came from a valid Twilio webhook request. Then, when the connection is established, our server can validate the authentication code before allowing access to the AI voice bot.

The Authentication Code

A Hash-based Message Authentication Code (HMAC) combines a secret key with the message data and then hashes the result using the SHA-256 algorithm. This process ensures both the integrity and authenticity of the message: only parties with the shared secret key can generate or verify the correct HMAC, and any modification to the message or key will result in a different hash output. Thus, HMAC-SHA256 provides strong cryptographic guarantees for secure authentication over WebSocket connections.

Our WebSocket authentication code is created by using hashlib to create an HMAC-SHA256 from the call identifier (CallSid) and our 64-character hexadecimal SECRET_KEY.

3) Verifying the WebSocket authentication code before proceeding with the WebSocket connection

Upon receiving a WebSocket request, the websocket_auth_code is taken from the request and compared to a newly created authentication code using the same parameters (CallSid and SECRET_KEY). If the codes match, then it is safe to assume that the WebSocket request came from our server with our SECRET_KEY. If the codes don’t match, then the request can be safely rejected.

It is important that we use hmac.compare_digest(), rather than just a == b, because it uses an approach designed to prevent timing analysis, making it appropriate for cryptography.

Conclusion

By combining Twilio’s webhook validation with HMAC-based authentication for WebSocket connections, we’ve built a secure, verifiable communication pipeline for the VLAS phone system. Validating incoming requests with Twilio’s RequestValidator ensures that only trusted sources trigger our endpoints. Embedding an HMAC-SHA256 authentication code into the TwiML stream, derived from a shared SECRET_KEY and the CallSid, gives cryptographic assurance that the WebSocket initiation is legitimate. And verifying the authentication code before accepting the connection prevents unauthorized access.

Want help with your AI voice bot, implementing WebSocket security, or setting up your Twilio integration? Set up a meeting with us and let’s talk!

Book a free 15-minute consultation

Or email us at info@lemmalegal.com