Authenticating Twilio Requests Using Basic Authentication, SSL, And ColdFusion
Yesterday, I demonstrated how to authenticate Twilio responses by verifying the given Twilio signature with HMAC-SHA1 hashing. Then, last night, I got into a Twitter conversation with Rick Osborne and Jason Dean about the merits of using Basic Authentication over an SSL (HTTPS) connection. Basic Authentication is an easy way to pass login credentials along with a URL. The downside to using basic authentication is that it is not terribly secure - it obfuscates the credentials, but passes them out in the open. As Jason pointed out to me, however, if one were to pass basic authentication credentials over an HTTPS request, the credentials would be both obfuscated as well as encrypted. And, since using an SSL connection does not seem to carry the computational overhead that is once did, I thought it was time to explore Twilio authorization using Basic Authentication.
Twilio supports Basic (and Digest) authentication for both its Phone and SMS end points. In order to utilize this authentication, you need to pass your username and password as part of your end point url:
Notice here that I am simply passing my username and password credentials to my end point by inserting "username:password@" before the host name. Also notice that I am using an HTTPS (SSL) connection. Since basic authentication passes your credentials out in the open, it is highly recommended that you do this over an HTTPS connection.
As I have blogged about before, when you use basic authentication, your username and password get Base64-encoded and placed in an "Authorization" header along with the HTTP request:
Basic dHJpY2lhOnN1cGVyc2V4eQ==
The first part of this header value, "Basic," simply denotes that we are using basic authentication. The latter part of the string is the Base64-encoded value containing your username and password:
username:password
When the incoming Twilio request reaches our ColdFusion SMS end point, we have to verify several things:
- The authorization header exists.
- The authorization header has the credentials.
- The credentials are in the right format.
- The credentials are authorized to access our system.
Any one of these steps might fail validation or might simply cause an unforseen technical error (ex. decoding an invalid Base64 value). As such, in a situation like this, I prefer to use a CFTry/CFCatch/CFThrow approach to work flow management. As I outlined in my Twitter API presentation, using a CFTry/CFCatch/CFThrow work flow allows our code to become substantially more straightforward. By throwing errors at any point of failure, it affords the insight at each step that all prior steps executed without concern.
To demonstrate this work flow in a Twilio context, let's take a look at my sample ColdFusion SMS end point below. You'll notice that the first half of the code is dedicated to request authorization while the latter half of the code is dedicate to responding to the SMS request.
Twilio SMS End Point
<!---
When we check for authorization, there's a number of things
that can go wrong, all of which will indicate that the
incoming request is not authorized. As such, let's wrap this in
a try/catch such that if any part of it fails, we can respond
with a 401 Unauthorized response.
--->
<cftry>
<!--- Get the HTTP request headers. --->
<cfset headers = getHttpRequestData().headers />
<!---
Check to see if the authorization header exists. This is
the value that contains our obfuscated (base64-encoded)
login credentials.
--->
<cfif !structKeyExists( headers, "Authorization" )>
<!--- Throw an error. --->
<cfthrow
type="AuthorizationNotProvided"
message="You must provide authorization credentials to access this system."
/>
</cfif>
<!---
At this point, we know that the client (Twilio Proxy) has
provided authorization headers; now, let's unecode them to
compare them to our valid credentials. This will be in the
form of:
Basic dHJpY2lhOnN1cGVyc2V4eQ==
We need to get the latter part, which a Base64-encoded string
in the form of username:password.
--->
<cfset encodedCredentials = listLast( headers.authorization, " " ) />
<!--- Convert the encoded credentials into a plain string. --->
<cfset credentials = toString( toBinary( encodedCredentials ) ) />
<!---
Check to make sure that the credentials conform to a valid
authorization string, username:password.
--->
<cfif !reFind( "^[^:]+:.+$", credentials )>
<!---
The client (Twilio Proxy) did not provide a valid
credentials string. Throw an error.
--->
<cfthrow
type="MalformedCredentials"
message="You must provide your authorization credentials in the form of [username:password]."
/>
</cfif>
<!---
Now that we know the credentials are in the proper format,
we can parse out the username and password values and compare
them to our internal credentials.
--->
<cfset username = listFirst( credentials, ":" ) />
<cfset password = listLast( credentials, ":" ) />
<!---
Compare the username and password to our list of accepted
credentials.
--->
<cfif !(
(username eq "joanna") &&
(password eq "Slipp3ryWh3nW3t")
)>
<!---
The username and password are not authorized. Throw
an error.
--->
<cfthrow
type="Unauthorized"
message="The credentials that you have provided are not authorized to access this system."
/>
</cfif>
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
If we have made it this far, then the user has provided the
properly formed, authenticated, and authorized credentials.
Allow them to continue on with the page request.
--->
<!--- ------------------------------------------------- --->
<!--- ------------------------------------------------- --->
<!---
For the most part, we are going to generically catch any
errors. However, if the error is that the provided
credentials simply aren't valid, then return a standard
XML response WITHOUT the 401 Unauthorized response. Even if
we provide a TwiML response with our 401 status code, Twilio
will NOT pass the response back to the user (because it
thinks it's still trying to authenticate).
NOTE: I would NOT *really* do this in a live system, this is
just for demo and exploration purposes. After all, if the
SMS end point is NOT authorized, then something is clearly
NOT configured properly in your Twilio phone number.
--->
<cfcatch type="Unauthorized">
<!--- Create an access denied TwiML response. --->
<cfsavecontent variable="responseXML">
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Sms>Access Denied</Sms>
</Response>
</cfsavecontent>
<!--- Return the access denied SMS response. --->
<cfcontent
type="text/xml"
variable="#toBinary( toBase64( trim( responseXML ) ) )#"
/>
</cfcatch>
<!---
Catch any error that is NOT part of the specific username and
password authentication. Any error here will indicate that the
request is not authorized.
--->
<cfcatch>
<!--- Send back an unauthorized status code. --->
<cfheader
statuscode="401"
statustext="Unauthorized"
/>
<!---
Alert the client that we support basic authentication in
the realm of SMS end points. Without this, the client
will not know how to authenticate itself.
--->
<cfheader
name="WWW-Authenticate"
value="basic realm=""SMS"""
/>
<!--- Return the access denied body. --->
<cfcontent
type="text/plain"
variable="#toBinary( toBase64( 'Access Denied' ) )#"
/>
</cfcatch>
</cftry>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
If we have made it this far, then we know this user is
authorized. Let's param the form values and proceeed with
standard usage.
--->
<cfparam name="form.from" type="string" default="" />
<cfparam name="form.body" type="string" default="" />
<!---
Build the response. For our demo purposes, just echo back the
passed-in SMS text message.
--->
<cfset response = "Thanks for the message: #form.body#" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Convert the message into Twilio XML response. --->
<cfsavecontent variable="responseXml">
<cfoutput>
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Sms>#xmlFormat( response )#</Sms>
</Response>
</cfoutput>
</cfsavecontent>
<!---
Stream XML response to Twilio client. Make sure to TRIM
the XML response such that it is valid XML.
--->
<cfcontent
type="text/xml"
variable="#toBinary( toBase64( trim( responseXml ) ) )#"
/>
In the authentication and authorization portion of the code, you can see that I am making good use of the CFThrow tag; any time that I find a situation in which the request fails authorization, I raise the appropriate exception. And, while I am using different Type values for my various errors, ideally, I only need one CFCatch tag. After all, if you fail authorization, you fail authorization. For this demo, however, I did add a superfluous CFCatch tag to handle "Unauthorized" errors specifically. If you return a "401 Unauthorized" HTTP status code, Twilio will not pass the response back to the mobile device even if a valid TwiML (Twilio Markup Language) response is provided. As such, for experimentation only, I am providing a "200 OK" status response in the specific case that the provided username and password is not authorized. However, since the username and password values are hard-coded in our SMS end point URL, this is pretty much an invalid use case.
Sending an SMS text message to the above Twilio end point will result in basic authorization and an SMS text message response:
The code we need to handle basic authentication is not small. It is, however, much more straightforward than using HMAC-SHA1 hashing to authenticate a Twilio request. If you have an SSL certificate for your web application, I would strongly suggest using this approach over using the signature-based approach. The other benefit to using basic authentication is that it makes it much easier to test your SMS end point directly within a browser (without having to use the Twilio Proxy).
Want to use code from this post? Check out the license.
Reader Comments