Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with:

Authenticating Twilio Request Signatures Using ColdFusion And HMAC-SHA1 Hashing

By Ben Nadel on
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.



Looking For A New Job?

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader 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

Reply to this Comment

@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

Reply to this Comment

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!

Reply to this Comment

@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 ;)

Reply to this Comment

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?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.