Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Ryan Brown
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Ryan Brown

Authenticating Twilio Request Signatures Using ColdFusion And HMAC-SHA1 Hashing

By
Published in Comments (5)

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.

Want to use code from this post? Check out the license.

Reader Comments

111 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

15,902 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:

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

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!

15,902 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 ;)

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?

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel