Twilio is a telephony service that POSTs to a callback URL on your server and asks you what to do when it receives phone calls or SMSes to the numbers you rent from Twilio. But securing your communications with Twilio can be complex if you're using Tornado behind Nginx. This shows you how to protect your Twilio callback URL with HTTP Authentication, request-signing, and (optionally) SSL.
I'm using HTTP Authentication code from Kevin Kelley, and I wrote the rest myself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | # A decorator that lets you require HTTP basic authentication from visitors.
# Kevin Kelley <kelleyk@kelleyk.net> 2011
# Use however makes you happy, but if it breaks, you get to keep both pieces.
# Post with explanation, commentary, etc.:
# http://kelleyk.com/post/7362319243/easy-basic-http-authentication-with-tornado
import base64, logging
import tornado.web
import twilio # From https://github.com/twilio/twilio-python
def require_basic_auth(handler_class):
def wrap_execute(handler_execute):
def require_basic_auth(handler, kwargs):
auth_header = handler.request.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic '):
handler.set_status(401)
handler.set_header('WWW-Authenticate', 'Basic realm=Restricted')
handler._transforms = []
handler.finish()
return False
auth_decoded = base64.decodestring(auth_header[6:])
kwargs['basicauth_user'], kwargs['basicauth_pass'] = auth_decoded.split(':', 2)
return True
def _execute(self, transforms, *args, **kwargs):
if not require_basic_auth(self, kwargs):
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
twilio_account_sid = 'INSERT YOUR ACCOUNT ID HERE'
twilio_account_token = 'INSERT YOUR ACCOUNT TOKEN HERE'
@require_basic_auth
class TwilioRequestHandler(tornado.web.RequestHandler):
def post(self, basicauth_user, basicauth_pass):
"""
Receive a Twilio request, return a TwiML response
"""
# We check in two ways that it's really Twilio POSTing to this URL:
# 1. Check that Twilio is sending the username and password we specified
# for it at https://www.twilio.com/user/account/phone-numbers/incoming
# 2. Check that Twilio has signed its request with our secret account token
username = 'CONFIGURE USERNAME AT TWILIO.COM AND ENTER IT HERE'
password = 'CONFIGURE PASSWORD AT TWILIO.COM AND ENTER IT HERE'
if basicauth_user != username or basicauth_pass != password:
raise tornado.web.HTTPError(401, "Invalid username and password for HTTP basic authentication")
# Construct the URL to this handler.
# self.request.full_url() doesn't work, because Twilio sort of has a bug:
# We tell it to POST to our URL with HTTP Authentication like this:
# http://username:password@b-date.me/api/twilio_request_handler
# ... and Twilio uses *that* URL, with username and password included, as
# part of its signature.
# Also, if we're proxied by Nginx, then Nginx handles the HTTPS protocol and
# connects to Tornado over HTTP
protocol = 'https' if self.request.headers.get('X-Twilio-Ssl') == 'Enabled' else self.request.protocol
url = '%s://%s:%s@%s%s' % (
protocol, username, password, self.request.host, self.request.path,
)
if not twilio.Utils(twilio_account_sid, twilio_account_token).validateRequest(
url,
# arguments has lists like { 'key': [ 'value', ... ] }, so flatten them
{
k: self.request.arguments[k][0]
for k in self.request.arguments
},
self.request.headers.get('X-Twilio-Signature'),
):
logging.error("Invalid Twilio signature to %s: %s" % (
self.request.full_url(), self.request
))
raise tornado.web.HTTPError(401, "Invalid Twilio signature")
# Do your actual processing of Twilio's POST here, using self.get_argument()
|