May 10, 2015

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.

Comments powered by Disqus