Phone number verification via Twilio SMS in Meteor
lovetap.org, my current side-project, sends people pictures from their Google photo albums directly to their phone at random intervals.
I wanted to make sure that people who sign up for the service actually own the phone number they're signing up with. I found a lot of tutorials on how to send a random code to a user's phone that they then have to enter on the website in order to verify their phone, but I was looking for something even simpler. lovetap will send you a text and all you have to do is reply "Yes" to verify your phone number.
How do you implement something like this with Twilio in a Meteor application? Well, it's actually not that hard:
First, you'll need a twilio account (duh).
When a user enters a number that hasn't been verified, send a text message asking them to verify (this is server-side code):
HTTP.post(twilioBaseUrl + "/Accounts/" + twilioAccountSid + "/Messages", {
auth: twilioAccountSid + ":" + twilioAuthToken,
params: {
From: twilioFromNumber,
To: toNumber,
Body: body,
},
});Now, this is where it gets a little more complicated. The next step is to mark the number as verified when a user replies "Yes" to the message we just sent him. To achieve this, we'll tell twilio to hit a web hook in our application whenever someone responds to a text message.
In your twilio account, go to "Dev Tools", then "TwiML Apps". Enter a friendly name, and under "SMS & MMS", the URL of the webhook in your application. This is the URL that twilio will get whenever one of your users replies to a text message you sent them. I set it to "GET" as I couldn't get it to work with a POST. In the Meteor app, we'll have to create a new route in iron router for our web hook:
this.route('/webhooks/twilio/handleresponse', {
where: 'server'
})
.get(function() {
this.response.end(Twilio.handleResponse(this));
});Now, we just need to handle the data that twilio sends in the request:
// extract data from twilio-post
var date = new Date();
var messageSid = routeContext.params.query.MessageSid;
var accountSid = routeContext.params.query.AccountSid;
var from = routeContext.params.query.From;
var to = routeContext.params.query.To;
var body = routeContext.params.query.Body;
if (!from) {
return "No from-number";
}
// unfortunately, iron router loses the + because it's URL encoded.
// we'll clumsily add it by hand:
from = "+" + from.trim();
to = "+" + to.trim();
// validated or not?
var verified = false;
body = body.toLowerCase();
if (body.indexOf("yes") !== -1 || body === "y") {
verified = true;
}
// update validated numbers collection
var responseBody = "";
if (verified) {
// set number to verified:
NumberStatus.update(
{
clean_number: from,
},
{
$set: {
verified_on: date,
},
},
{
multi: true,
}
);
responseBody = "Thanks! You'll soon receive your first picture!";
} else {
// set number to not verified:
NumberStatus.update(
{
clean_number: from,
},
{
$unset: {
verified_on: date,
},
},
{
multi: true,
}
);
responseBody =
'OK, we won\'t send you pictures.\nIf you change your mind, simply reply "Yes" to this message.';
}
// Set the headers
routeContext.response.writeHead(200, {
"Content-Type": "application/xml",
});
// respond with twiml:
return '<?xml version="1.0" encoding="UTF-8"?><Response><Sms from="[TWILIOFROM]" to="[TO]">[BODY]</Sms></Response>'
.replace("[TWILIOFROM]", twilioFromNumber)
.replace("[TO]", from)
.replace("[BODY]", responseBody);
We now marked the number as verified in our database and can safely send to it.