Phone-Based Realtime Authentication With ColdFusion, Twilio, And Pusher
The other day, I was signing up for something online when I reached a step in which the vendor required me to verify my phone number. To my surprise this verification process took place in realtime with the web page automatically reacting to what I was doing on the phone. I thought this was wicked awesome and I wanted to see if I could do the same thing in ColdFusion. For the phone support, I naturally used Twilio as it is amazing; and for realtime notification support, I used Pusher.
For this proof of concept, I tried to keep it as simple as possible. My application has a single secure page and a login page. The login page asks you for your phone number and then provides you with a 4-digit verification code. Upon submitting your phone number, the server initiates a phone call - via Twilio - to the given phone number. The user then answers the phone and enters the 4-digit verification code. Twilio then submits the collected digits back to the ColdFusion server which will then compare them to the original 4-digit verification code; and, if they match, the ColdFusion server flags the user as authenticated and "pushes" a verification event back to the browser using Pusher. The browser then listens for this verification event and forwards the user on to the secure page.
That's kind of a lot to follow so hopefully this snazzy info-graphic will help clarify things:
Ok, so now that you have a better understanding of the workflow, let's start to look at some of the code. The first thing that we want to do is set up our ColdFusion application so that it has access to our Twilio and Pusher accounts.
Application.cfc
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application settings. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 ) />
<!---
Turn on session management because we need a way to keep
track of the user's authentication across page requests.
--->
<cfset this.sessionManagement = true />
<cfset this.sessionTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<cffunction
name="onApplicationStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the application.">
<!---
Set up the Twilio information for Telephony integration.
We will need this information to initialize phone calls.
--->
<cfset application.twilio = {
accountSID = "********************",
authToken = "********************",
phoneNumber = "********************"
} />
<!---
Set up the Pusher information for realtime push
notifications. We will need Pusher to let the
authentication page know about the phone-based
interactions.
--->
<cfset application.pusher = {
appID = "********************",
key = "********************",
secret = "********************"
} />
<!--- Return true so the page will continue loading. --->
<cfreturn true />
</cffunction>
<cffunction
name="onSessionStart"
access="public"
returntype="void"
output="false"
hint="I initialize the session.">
<!---
This is the code the user will need to enter on the
phone in order to prove phone number authentication.
--->
<cfset session.authenticationCode = "" />
<!---
THis is the user's authenticated status. They will
not be able to acecss the secure portion of this
site until they are authenticated via Twilio.
--->
<cfset session.isAuthenticated = false />
<!--- This is the user's phone number. --->
<cfset session.phoneNumber = "" />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
For Twilio, I had to purchase a phone number; but, I didn't have to set up any SMS or Voice endpoints. Since all Twilio interaction will be initiated by the ColdFusion server, we'll be able to supply all the necessary callback information from within the application itself.
Once the application has been defined, and the user's session is default to not being authenticated, the user can then proceed to the login page where they will have to submit their phone number. In addition to posting the phone number to the ColdFusion server, this login page also connects to the Pusher realtime notification service where it registers to listen for "verified" events:
Login.cfm
<!---
Now that the user has landed on the login page, reset their
authentication status.
--->
<cfset session.isAuthenticated = false />
<!---
Every time the user comes to the login page, we will generate a
new authenticate code for them to enter on the cell phone. This
will be a random four-digit number.
--->
<cfset session.authenticationCode = randRange( 1111, 9999 ) />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Reset the content buffer and set the mime-type. --->
<cfcontent type="text/html" />
<cfoutput>
<!DOCTYPE html>
<html>
<head>
<title>Phone Authentication With Twilio And Pusher</title>
<!--- jQuery library for DOM manipulation. --->
<script type="text/javascript" src="./jquery-1.5.1.js"></script>
<!--- Pusher library for realtime notification. --->
<script type="text/javascript" src="http://js.pusherapp.com/1.7/pusher.min.js"></script>
</head>
<body>
<h1>
Phone Authentication With Twilio And Pusher
</h1>
<form>
<p>
In order to verify your identity, please submit your
phone number:
</p>
<p>
<input type="password" name="number" />
<input type="submit" value="Submit" />
</p>
</form>
<!---
Once the phone number has been dialed, we will show
the user that they need to under an authentication
code over the phone.
--->
<div class="nextStep" style="display: none ;">
<p>
We are calling your phone now...
</p>
<p>
Using your touchtone phone, please enter the
following authentication code:
</p>
<p style="font-size: 300% ; margin-top: 10px ;">
<strong>#session.authenticationCode#</strong>
</p>
</div>
<!---
Now that the DOM is ready, let's initailize our
interaction scripts.
--->
<script type="text/javascript">
// Get a reference to our DOM elements.
var form = $( "form" );
var phoneNumber = form.find( "input[ name = 'number' ]" );
var nextStep = $( "div.nextStep" );
// Capture the form submission so that we can interact
// with the server using AJAX rather than a traditional
// form submission.
form.submit(
function( event ){
// Cancel the default form-submit behavior.
event.preventDefault();
// Strip out any non-number values, including
// the leading 1 in the phone number. This will
// leave us with only the digits that are valid
// **US** numbers.
var number = phoneNumber.val().replace(
new RegExp( "^1|[^\\d]+", "g" ),
""
);
// Check to see if the number is exactly 10
// digits. Again, we are only dealing with
// valid US numbers at this time.
if (number.length != 10){
// Tell the user that their input is not
// valid.
alert( "Please enter a valid 10-digit phone number." );
// Return out of our guard statement.
return;
}
// ASSERT: At this point, we have validated the
// phone number and are prepared to proceed with
// dialing the outgoing number.
// Dial outgoing call.
//
// NOTE: We are prepending the number with a space
// to make sure that we don't get some strange
// string-to-number conversion during the client-
// server interaction.
var startCall = $.ajax({
type: "post",
url: "./call.cfm",
data: {
number: (" " + number)
},
dataType: "text"
});
// When the call API comes back successfully,
// show the next step instructions.
startCall.done(
function(){
nextStep.show();
}
);
// If the call fails, then alert that something
// went wrong and have them resubmit their phone
// number.
startCall.fail(
function(){
alert( "Something went wrong - please double-check your phone number." );
}
);
}
);
// Connect to Pusher so we can start to listen for
// realtime notifications during the verification
// process.
var pusher = new Pusher( "#application.pusher.key#" );
// Listen to the Pusher channel for this user. Make the
// channel user-specific so we don't get miscommunication
// between users trying to login at the same time.
var channel = pusher.subscribe(
"login-#cookie.cfid#-#cookie.cftoken#"
);
// Bind the verified event - this is the event triggered
// when the user has confirmed their identity via the
// phone call - this is being "pushed" from the server to
// the client -- F-yeah!!
channel.bind(
"verified",
function( data ){
// The user has been verified - send them to the
// secure page for verified users.
location.href = "./secure.cfm";
}
);
</script>
</body>
</html>
</cfoutput>
Once the user has submitted their phone number, the login page reveals the already-generated 4-digit verification code. This is the code that the user will have to enter over the phone. To initiate that phone call, the login page posts the given phone number back to the call page on the ColdFusion server.
The Call page then contacts Twilio via its RESTful API and initiates a phone call. As part of this API call, the ColdFusion server needs to supply a callback URL that Twilio will use in order to process the execution logic of the outgoing phone call. In this case, we are telling it to use the "gather.cfm" page as the logic provider.
Call.cfm
<!--- Param the incoming phone number. --->
<cfparam name="form.number" type="string" />
<!---
Sanitize the incoming value; we passed it through with a
leading space in order to ensure no string-to-number comparison
messed up the formatting.
--->
<cfset form.number = reReplace(
trim( form.number ),
"^1|[^\d]+",
"",
"all"
) />
<!---
Even though we validated on the client-side, double check to make
sure that we are dealing with a valid 10-digit US phone number.
--->
<cfif (len( form.number) neq 10)>
<!---
The phone number is not valid. Return a 400 Bad Request
header so that the client can alert the user.
--->
<cfheader
statuscode="400"
statustext="Bad Request"
/>
<!---
Abort the request since there is nothing more we can do as
far as processing this number is concerned.
--->
<cfabort />
</cfif>
<!---
Since we have made it this far, we know that the phone number
is valid - store the phone number in the user session.
--->
<cfset session.phoneNumber = form.number />
<!---
When we initiate the call to Twilio, we are going to need to
provide it with a URL for the processing logic of the call.
When doing that, we want to provide it with security credentials
to make sure that no one else is trying to hit the verification
page without our authorization.
NOTE: We are using SSL so that our username + password is not
being sent in plain sight.
--->
<cfset secureUrl = (
"https://jill:hasDeliciouslySexyFeet@" &
cgi.server_name &
getDirectoryFromPath( cgi.script_name ) &
"gather.cfm"
) />
<!--- Initiate the outgoing call with Twilio. --->
<cfhttp
result="outboundCall"
method="post"
url="https://api.twilio.com/2010-04-01/Accounts/#application.twilio.accountSID#/Calls"
username="#application.twilio.accountSID#"
password="#application.twilio.authToken#">
<!---
This is the Twilio phone number that will be placing the
outbound call to the given touch-tone phone number (for
user authentication).
NOTE: It must start with "+" and include the country code.
This number happens to be a US number (+1).
--->
<cfhttpparam
type="formfield"
name="From"
value="+1#application.twilio.phoneNumber#"
/>
<!---
This is the user-submitted number that we are calling
for authentication.
NOTE: We are starting the number with "+1" for the country
code; but, Twilio will also accept unformatted US phone
numbers as well.
--->
<cfhttpparam
type="formfield"
name="To"
value="+1#form.number#"
/>
<!---
This is the URL that will define the TwiML (Twilio Markup)
that will provide the processing and routing logic of the
phone interaction.
NOTE: When Twilio calls the Verify callback, we need it to
pass the CFID and CFTOKEN session token values so that it
can mimc the user. Otherwise, we will not be able to have
Twilio-initiated HTTP calls to interact with the user's
session.
--->
<cfhttpparam
type="formfield"
name="Url"
value="#secureUrl#?cfid=#cookie.cfid#&cftoken=#cookie.cftoken#"
/>
<!---
NOTE: While you can submit Cookie values (cfid / cftoken)
using CFHTTPParam tags, it won't matter. Twilio will consider
this request (to initiate a call) a completely different
interaction that the actual call it makes; as such, it is
considered a completely different session and gets its own
set of cookies.
--->
</cfhttp>
<!---
Determine if the outbound call was successfully initiated. In
this case, we're just looking for any response code in the 200
block.
--->
<cfset success = !!reFind( "^20\d", outboundCall.statusCode ) />
<!---
Echo the status code of the outbound call. This will just give
us an easy way to handle the Javascript on the login page (using
the done/fail aspect of the jQuery deferred objects).
WARNING: jQuery deferred objects are sexier than they may at
first appear.
--->
<cfheader
statuscode="#listFirst( outboundCall.statusCode, ' ' )#"
statustext="#listRest( outboundCall.statusCode, ' ' )#"
/>
<!--- Return the plain text response. --->
<cfcontent
type="text/plain"
variable="#toBinary( toBase64( toString( success ) ) )#"
/>
Notice that we are including the CFID and CFTOKEN session tokens in the callback URL provided to Twilio. This will allow the Twilio web proxy to hook into our user's session during the phone-based interactions. Since verification is session-specific, we'll need to be able to access the user's session on-demand.
Notice also that the callback URL includes a username and password for use with Basic Authentication. Once the user has entered their 4-digit verification code, we will want to assert that the call is being made by Twilio and not by someone trying to circumvent our security system. Since the URL provided is using HTTPS, we don't have to worry about the basic authentication credentials being passed out in the open.
Once Twilio initiates the call, it passes the workflow off to the gather page which is responsible for recording the 4-digit code supplied by the user.
Gather.cfm
<!---
Param the session tokens that have been passed through.
We will use these to hook the Twilio Proxy into the
ColdFusion session created by the user that is being
validated via Twilio.
--->
<cfparam name="url.cfid" type="string" default="" />
<cfparam name="url.cftoken" type="string" default="" />
<!---
NOTE: We could check the given secure credentials on this
page; but, since this page isnt' really dealing with any critical
data, I won't bother asking Twilio for the Basic Authentication
credentials - we'll have that for the verification step.
--->
<!--- Build the TwiML XML response. --->
<cfsavecontent variable="twiml">
<cfoutput>
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<!---
Gather the 4-digit authentication code. We're
creating this as the primary node so that the user
can enter the digits at any point during the pause
and / or instructional voice.
NOTE: We do NOT have to pass session tokens (cfid and
cftoken) at this point (like we did in the previous
step) since they are being sent back to the Twilio
web proxy as set-cookies headers (see below).
--->
<Gather
action="./verify.cfm"
numDigits="4"
timeout="20">
<!---
Start off with a pause since the user might need
time to get adjusted to the incoming call.
--->
<Pause length="2" />
<!--- Say the instructions (with the sexy voice). --->
<Say voice="woman">In order to verify your identity, please enter the four digit code on your computer screen.</Say>
</Gather>
</Response>
</cfoutput>
</cfsavecontent>
<!---
Store the incoming session tokens into the Twilio proxy. These
cookies will be exchanged for each part of this single call
(which Twilio considers to be a single interaction session).
Once the cookies have been passed, every page request made by
the Twilio Proxy during the length of this call will hook into
the user's session (assuming CFID and CFTOKEN are the only values
used to determine session validation).
--->
<cfcookie
name="cfid"
value="#url.cfid#"
/>
<cfcookie
name="cftoken"
value="#url.cftoken#"
/>
<!--- Set the Twilio response as XML. --->
<cfcontent
type="text/xml"
variable="#toBinary( toBase64( trim( twiml ) ) )#"
/>
Since we're not dealing with any really secure data at this point, I am not validating the Basic Authentication credentials. But, you will notice that after I define my Twilio Markup Language response (TwiML), I am setting two cookies - CFID and CFTOKEN. The Twilio web proxy acts exactly like a web browser; as such, once we set the CFID and CFTOKEN cookies, Twilio will continue to post them back with each request made during the same "session." This means that all Twilio-ColdFusion interactions made subsequently on this call will be able to implicitly hook into the user's session.
Once the user has submitted a 4-digit code, Twilio that passes the processing off to the verification page where ColdFusion can validate the user-supplied code against the known verification code.
Verify.cfm
<!--- Param the selected digits. --->
<cfparam name="form.digits" type="string" />
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Now that the user has submitted their digits, it's time to
check to make sure that this call is being initiated from
the Twilio server and NOT by someone who is trying to
circumvent the secuirty system. As such, it's time to ask for
Basic Authentication credentials.
Since a few things could go wrong at this point, we'll wrap the
authorization process in a Try/Catch and if anything goes wrong,
we'll simply ask for the credentials again.
--->
<cftry>
<!---
Grab the encoded credentials from the header. Remember, the
credentials come back in a Base64-encoded Header value that
looks like:
Basic dHJpY2lhOnN1cGVyc2V4eQ==
--->
<cfset encodedCredentials = listLast(
getHttpRequestData().headers.authorization,
" "
) />
<!---
Convert the Base64 credentials to a colon-delimited string
that contains the username:password value.
--->
<cfset credentials = toString( toBinary( encodedCredentials ) ) />
<!--- Extract the username and password. --->
<cfset username = listFirst( credentials, ":" ) />
<cfset password = listRest( credentials, ":" ) />
<!---
Make sure that the given credentials match the ones used as
the login presented to the Twilio proxy (see the CFHTTP code
that triggered the Twilio-based call).
--->
<cfif (
(username neq "jill") ||
(password neq "hasDeliciouslySexyFeet")
)>
<!--- The credentials don't match. Throw an error. --->
<cfthrow type="InvalidCredentials" />
</cfif>
<!---
If anything goes wrong at this point then we know that
either Twilio has yet to provide the given Basic
Authentication credentials OR someone is messing with the
security system. In any case, return a 401 response code.
In the case of Twilio, this will prompt it for the
credentials that we supplied in the previous step.
--->
<cfcatch>
<!---
Since the user did not submit any authorization, return
a request for Basic Authentication along with a 401
Unauthorized response.
--->
<cfheader
statuscode="401"
statustext="Unauthorized"
/>
<!---
Tell the user that this service supports Basic
Authentication logins.
--->
<cfheader
name="WWW-Authenticate"
value="basic realm=""website"""
/>
<!---
End the request - let the user submit their credentials
on the subsequen request.
--->
<cfabort />
</cfcatch>
</cftry>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!---
Check to see if the digits entered by the user over the phone
match the one in the session (which has been linked with the
pasesd-through CFID and CFTOKEN values.
--->
<cfif (
(len( form.digits ) eq 4) &&
(form.digits eq session.authenticationCode)
)>
<!---
The user has positively authenticated themselves using
their telephone number. Flag the user as authenticated.
--->
<cfset session.isAuthenticated = true />
<!---
Create an instance of the Pusher component so that we
can let the login page know that the user has been
verified.
NOTE: We can use the Cookie scope here since the user's
CFID and CFTOKEN values have been injected in the Twilio
Proxy cookie collection in the previous step (gather).
--->
<cfset createObject( "component", "Pusher" )
.init(
application.pusher.appID,
application.pusher.key,
application.pusher.secret
)
.pushMessage(
channel = "login-#cookie.cfid#-#cookie.cftoken#",
event = "verified",
message = "true"
)
/>
<!--- Build the TwiML XML response. --->
<cfsavecontent variable="twiml">
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<!--- Say thanks (with the sexy voice). --->
<Say voice="woman">Thank you. Your identity has been authenticated. You will be redirected momentarily.</Say>
</Response>
</cfsavecontent>
<cfelse>
<!---
The user has failed to authenticate themselves.
Respond with a failure and ask them to resubmit
thier phone number.
--->
<!--- Build the TwiML XML response. --->
<cfsavecontent variable="twiml">
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
<!--- Say the instructions (with the sexy voice). --->
<Say voice="woman">Sorry, your authentication has failed. Please hang-up and resubmit your phone number.</Say>
</Response>
</cfsavecontent>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Set the Twilio response as XML. --->
<cfcontent
type="text/xml"
variable="#toBinary( toBase64( trim( twiml ) ) )#"
/>
At this point, we do care about Basic Authentication and therefore must supply a 401 Unauthorized status code in order to get the Twilio web proxy to submit the Basic Authentication credentials. If both the basic authentication credentials and the supplied 4-digit authorization code match then we can safely say that the user has successfully verified their identity over the phone. At this point, we can then use Pusher to push a realtime notification event back to the login page.
Once the login page receives this pushed event - "verified" - it forwards the user onto the secure content page.
Secure.cfm
<!---
Check to see if the user has been authenticated. If not, kick
them back to the login page where they will have the chance
to authenticate.
--->
<cfif !session.isAuthenticated>
<!---
Danger!! The user is not authenticated. Kick them back to
the login page where they will have a change to authenticate
via their phone.
--->
<cflocation
url="./login.cfm"
addtoken="false"
/>
</cfif>
<!--- ----------------------------------------------------- --->
<!--- ----------------------------------------------------- --->
<!--- Reset the content buffer and set the mime-type. --->
<cfcontent type="text/html" />
<cfoutput>
<!DOCTYPE html>
<html>
<head>
<title>You Have Been Authenticated Via Twilio And Pusher</title>
</head>
<body>
<h1>
You Have Been Authenticated Via Twilio And Pusher
</h1>
<p>
<img src="./muscle.jpg" width="500" />
</p>
<p>
Groovy!
</p>
</body>
</html>
</cfoutput>
This was a whole lot of fun to put together. The basic coding only took me a few hours. It took me a few days to pull it all together, however, since I was confused about the Basic Authentication work flow. While I have worked with basic authentication and Twilio before, I didn't realized that some clients required a 401 Unauthorized response before they would submit the Authorization header. All in all, though, it's amazing how easy some of these API services make creating really rich, complex, and engaging workflows.
Want to use code from this post? Check out the license.
Reader Comments
Very cool stuff, you have been doing some great Twilio work lately.
It does add an extra layer of security, I guess. Have cell phones become the new "Signet Ring" If someone steals my phone, have they stolen my identity?
Still it is better than "read" 500 pages of legal crapola, click here to accept said crapola.
Ben, thanks for posting this. I will have use for it in the near future and you just made my life easier.
Excellent job.
Regards,
Ricardo.
Very cool! I know I can ask, but I'm wondering if you'll answer: what site was it?
@Tim,
I guess it makes it much easier to guarantee (hopefully) that someone is not a bot? I suppose a computer could do this, but it would be much harder.
@Ricardo,
Awesome - I hope what ever you're gonna do with it works out!
@Randall,
Thanks - I *think* it was Amazon EC2. I don't remember S3 doing this; I suppose when you can start booting up instance of computers that bill hourly, they really really really want to make sure you are who you say you are :)
Wow, thanks for this! I just whipped up a demo, based on your work here, in a couple hours.
You are making me look smarter than I really am!
I'll just note that if part of this process is listening to an audio recording (when I ran the your screencast, I thought I heard a female voice near the end), this isn't going to be accessible for deaf/hard of hearing people.
@Mark,
Ha ha ha - hey, it's totally awesome that you were able to put something together based on this code; if nothing else, it's a confirmation that people can make heads-and-tails out of what I write. Glad to have given you a good starting point.
@Lola,
That's a really good point. Once of the nice things about Twilio is that you can put the Say verb inside of the Gather verb. So, while I waited for the voice to finish on the call before entering your number, there's nothing that requires you to do so (if your Say is nested inside your Gather as in my example).
As such, as long as there is an on-screen direction that One can simply enter the verification code upon answering the call, you should be ok.
Of course, the best option would be to provide either a Voice **or** SMS/Text messaging option. The only difference really, with that approach, is that you'd have to define an SMS end-point in the Twilio configuration so that the user's response Text message could be routed back to the ColdFusion server.
Ben great post! I don't know too much about Twilio but just wondering if its possible instead of showing the 4 digit random number on the screen, send that random number through Twilio and receive a text or voice message (whichever user prefers) with that number so that the user can type that number into the form instead of typing it on the cell phone.
Thanks.
@Marcin,
Yeah, you can definitely go the SMS route rather than the phone. You would just need to configure the SMS end point to handle the reply from the user. Perhaps I can fool around with a demo for this.
Ben, what I meant was whether Twilio can send you SMS or Voice with a generated number so the user can type it into the form rather show the number to the user and enter it into the cell phone? I believe Craigslist is using something similar, where you provide cell#, they send you SMS/Voice with a specific generated number and you enter it on the browser to verify if its really you.
@Ben from the time the user clicks the call me link, how long did the process take?
Google voice also does this when you add new phone numbers. They only require a 2 digit code.
Derek
@Marcin,
Ahh, yeah, that makes good sense. And, as @Lola brought up before, this would be a much better route for people who are hard of hearing.
@Derek,
So, there's actually two legs to consider:
1. User clicks (AJAX to server, server to Twilio).
2. Twilio calls (and passes control over to call-processing page).
Step #1 sometimes took a few seconds before the call rang on my phone. This could be a hold up on my server, the Twilio server, or the cellular network?
Step #2, going from picked-up phone to processing (Gather.cfm) was pretty instantaneous. Despite the fact that the Twilio web proxy has to make an additional HTTP request, any delay was nothing. In fact, I added a Pause verb to the Gather worflow because the voice would typically kick in before I got the phone to my ear (after having picked up).
The ability to telesign in to online accounts makes them a lot more secure but @Tim I think you are right. If someone steals your phone, they really have stolen your identity. It will be even more important with Google wallet becomes mainstream.