Authenticating Twilio Request Signatures Using ColdFusion And HMAC-SHA1 Hashing

Posted July 22, 2010 at 8:11 AM by Ben Nadel

Tags: ColdFusion

Twilio, as I've explained previously, is an online service that acts as a proxy between your web applications and mobile devices. This service is configured through the use of Phone and SMS end points. When phone calls and SMS text messages come into your Twilio phone number, Twilio captures that information, augments it, and posts to the web-accessible URL designated as your particular end point. The danger with using any web accessible URL is that there is no implicit security to stop random people from posting their own data to your URL. To help remedy this, Twilio signs all of their HTTP requests in such a way that you can verify an authentic Twilio request using your own secret key.

Once a Twilio request comes into your server, you need to create a normalized representation of the HTTP request data. This representation includes the full URL (including the query string) of your end point concatenated with all of the form data, alpha sorted, in nameValue format. So for example, if your end point is:

http://www.bennadel.com/sms.cfm

... and you've posted two form values, A=1 and B=2; then, your normalized representation would be:

http://www.bennadel.com/sms.cfmA=1B=2

Notice here that the form values are appended to the URL without any delimiters. Also notice that the form keys maintain their original case. When it comes to ColdFusion, getting the form keys to have the appropriate casing is almost as difficult as executing the hashing algorithm itself. Unfortunately, all form keys in ColdFusion 8 show up as upper-cased. As such, we'll have to manually parse the HTTP request content in order to gather the form keys in their original case.

Once we have our normalized request representation, we can hash it using our Auth Key (as located on the Twilio dash board). After dealing with the Pusher web service, I'm starting to feel more comfortable dipping down into the Java layer to execute advanced hashing algorithms. Unlike Pusher, however, which uses the Hmac-SHA256 algorithm, Twilio uses the Hmac-SHA1 algorithm. In the following code, I will show you how to normalize the request representation, hash it, and compare it to the signature posted by Twilio.

  • <!--- Param the form data. --->
  • <cfparam name="form.body" type="string" default="Hello!" />
  •  
  • <!---
  • Define the Twilio Auth Key (from your Twilio dashbaord).
  • This will be used to re-hash the incoming data.
  • --->
  • <cfset twilioAuthKey = "************************************" />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • We have to re-hash the requested resource in order to ensure
  • that the request is truly coming from Twilio. To do this, we
  • have create the resource representation and compare its hash
  • to the one posted in the headers.
  •  
  • The resource is the requested URL concatenated with all of
  • the alpha-sorted form values.
  • --->
  • <cfset resource = (
  • "http://" &
  • cgi.server_name &
  • cgi.script_name
  • ) />
  •  
  • <!--- Check to see if we have a query string. --->
  • <cfif len( cgi.query_string )>
  •  
  • <!--- Append the query string to the resournce. --->
  • <cfset resource &= ("?" & cgi.query_string) />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now, we have to append all of the form values to the resource
  • (without any delimiter). However, since ColdFusion alters the
  • case of both the FORM collection keys and the Header keys. As
  • such, we actually have to manually parse the HTTP request
  • content in order to get the keys in their original alpha case.
  •  
  • Let's break the request content in name=value pairs.
  • --->
  • <cfset formContentPairs = reMatch(
  • "[^=&]+=[^&]*",
  • getHttpRequestData().content
  • ) />
  •  
  • <!--- Now, let's create a collection of Twilio form keys. --->
  • <cfset twilioFormKeys = [] />
  •  
  • <!---
  • Loop over the name=value pairs and extract the form key
  • (in its original case) as everything before the equals.
  • --->
  • <cfloop
  • index="formContentPair"
  • array="#formContentPairs#">
  •  
  • <!--- Append the parsed form key to the Twilio collection. --->
  • <cfset arrayAppend(
  • twilioFormKeys,
  • listFirst( formContentPair, "=" )
  • ) />
  •  
  • </cfloop>
  •  
  • <!--- Now, alpha sort the form keys. --->
  • <cfset arraySort(
  • twilioFormKeys,
  • "text",
  • "asc"
  • ) />
  •  
  • <!---
  • Now that the keys are parsed in thier original case and sorted,
  • let's add them to the resource that we are going to encode.
  • --->
  • <cfloop
  • index="formKey"
  • array="#twilioFormKeys#">
  •  
  • <!---
  • Add this Twilio form key and its associated value to the
  • resource without any delimiters.
  • --->
  • <cfset resource &= (formKey & form[ formKey ]) />
  •  
  • </cfloop>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have our resource, we need to hash it using the
  • HMAC-SHA1 algorithm. To do this, we are going to dip down into
  • the Java layer. Let's create our secret key representation using
  • our Twilio AUTH KEY and the HMAC-SHA1 algorithm selection.
  • --->
  • <cfset secretKeySpec = createObject(
  • "java",
  • "javax.crypto.spec.SecretKeySpec"
  • ).init(
  • toBinary( toBase64( twilioAuthKey ) ),
  • "HmacSHA1"
  • )
  • />
  •  
  • <!---
  • Now, let's create our MAC (Message Authentication Code) generator
  • to encrypt the Twilio resource we created above.
  • --->
  • <cfset mac = createObject( "java", "javax.crypto.Mac" )
  • .getInstance( "HmacSHA1" )
  • />
  •  
  • <!--- Initialize the MAC instance using our secret key. --->
  • <cfset mac.init( secretKeySpec ) />
  •  
  • <!---
  • Complete the mac encryption operation, encrypting the Twilio
  • resource using the given secret key spec (that we created above).
  • --->
  • <cfset encryptedBytes = mac.doFinal(
  • toBinary( toBase64( resource ) )
  • ) />
  •  
  • <!---
  • At this point, we have encrypted the resource; now, we have to
  • base64 encode it so that it has only printable characters.
  • --->
  •  
  • <cfset secureSignature = createObject(
  • "java",
  • "org.apache.commons.codec.binary.Base64"
  • )
  • .encodeBase64( encryptedBytes )
  • />
  •  
  • <!---
  • The Base64 encoding returns a byte array. Let's convert the
  • byte array to a normal string.
  • --->
  • <cfset secureSignature = toString( secureSignature ) />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Get the headers reference so that we can easily access the
  • Twilio signature.
  • --->
  • <cfset headers = getHttpRequestData().headers />
  •  
  • <!---
  • Now that we have re-hashed and encoded the Twilio resource,
  • let's compare our version to the signature passed in the
  • Headers.
  • --->
  • <cfif (
  • structKeyExists( headers, "X-Twilio-Signature" ) &&
  • (secureSignature eq headers[ "X-Twilio-Signature" ])
  • )>
  •  
  • <!--- The signatures match! This request is from Twilio. --->
  • <cfset response = "Sweeet! They match!" />
  •  
  • <cfelse>
  •  
  • <!---
  • The signatures do NOT match. The request cannot be verified
  • as coming from Twilio.
  • --->
  • <cfset response = "Nice try, tough guy!" />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Convert the response 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 ) ) )#"
  • />

Anyway, I won't go into any more detail on this. Mostly, I just wanted to try this approach because it was another chance for me to play with the SecretKeySpec and Mac classes for hashing. I still don't fully understand how they work; but, I think I'm started to get the hang of it. Actually, now that I've got this one working, it would be interesting to revisit my Pusher.cfc ColdFusion component to see if there is anything in there that I'd want to change.




Reader Comments

Jul 23, 2010 at 11:18 AM // reply »
110 Comments

That URL string just looks very weird to me without ampersands. Is it also supposed to be missing the ? in your original URL
sms.cfmA=1B=2
or should it be
sms.cfm?A=1B=2


Jul 23, 2010 at 11:20 AM // reply »
11,238 Comments

@Gareth,

Yeah, agreed that it looks very odd. It's not really a URL - it's simply a normalized representation of the HTTP request. We need a way to ensure that both Twilio and our SMS end points are hashing the right raw value.

This way seems overly complex (I feel that way about most hashing approaches). I much prefer the HTTPS / Basic Authentication approach that I just looked into:

http://www.bennadel.com/blog/1973-Authenticating-Twilio-Requests-Using-Basic-Authentication-SSL-And-ColdFusion.htm


Feb 8, 2011 at 4:58 PM // reply »
5 Comments

You just saved my ass with this post, Ben. I spent DAYS struggling with getting a propper HMAC-SHA1 signature for signing requests to Adobe Content Server, using two different Java-based CF functions I found (one of which works perfectly fine for signing requests to S3), but your code had just the right hoodoo to get my stuff to work properly (with a little extra toBinary(toBase64)) action that my other samples didn't have).

Thanks a bajillion!


Feb 8, 2011 at 10:57 PM // reply »
11,238 Comments

@Dave,

Awesome! Glad I could help. I think you'll find that once you get this kind of thing working, it will make any future requirement for advanced hashing basically a matter of copy-paste-tweak. It seems that all the APIs these days (safe the Facebook API which seems super easy to integrate with) require normalized, hashed requests. Hope this helps out in the future as well ;)


May 9, 2011 at 8:27 PM // reply »
1 Comments

Hi Ben. This looks *so* close to what I'm trying to do. I'm trying to create an enveloped, digitally signed XML SOAP packet in CF. Do you have any examples of this? I've read a lot recently about digitally signed XML, but I'm not entirely clear on how to use the X509 digital cert to generate digest, how to properly canonicalize, etc. In short, I'm not quite getting the pieces all together and from what I can see many other people are having the trouble too. Can you shed any light?


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 20, 2013 at 10:21 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
Is there any error logging and handling framework in angularjs, if not then in what way I can do this. ... read »
May 19, 2013 at 2:31 PM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
It's funny really just how well that image describes the way I would imagine most people that go with angular for some project is. I have had a similar roller-coaster ride with it as well, but not qu ... read »
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools