I don’t have the bandwidth to do a complete blog post, but I will try to lay out the general methodology here.
Once a user enters a phone number, I send a request to a Google Function that requests a passcode with the following logic:
// Looks for user by phone number. If found (new log in), sends
// verfication. If not found (new sign up), creates new user and
// sends verification.
admin.auth().getUserByPhoneNumber(phone)
.then(userRecord => {
sendSMSVerification(res, userRecord.uid, phone);
})
.catch(getUserErr => {
if (getUserErr.code === 'auth/user-not-found') {
admin.auth().createUser({
phoneNumber: phone
})
.then(userRecord => {
sendSMSVerification(res, userRecord.uid, phone);
})
.catch(userCreateErr => {
return res.status(422).send(userCreateErr);
});
} else {
res.status(422).send({ error: getUserErr });
}
});
The function sendSMSVerification creates a random (sort of) passcode and sends it via the Twilio API. Upon success, it saves the passcode and an expiration time for the user in the Firebase database.
const sendSMSVerification = (res, uid, phone) => {
const code = Math.floor(((Math.random() * 899999) + 100000));
const expiration = Date.now() + (2 * 60000); // Expires in 2 minutes
const verification = { code, expiration, valid: true };
twilio.messages.create({
body: `Your code is ${code}`,
to: twilioConfig.phone_dev ? twilioConfig.phone_dev : phone,
from: twilioConfig.phone_from
})
.then(message => {
admin.database().ref(`/users/${uid}/verification`)
.set(verification)
.then(() => {
return res.send({ success: true });
})
.catch((err) => {
return res.status(422).send(err);
});
})
.catch((err) => {
return res.status(422).send(err);
});
};
Back on the phone, the user enters a the verification code. I then send a request with the phone number and verification code to another Google Function with the following logic:
// Validate code and expiration time, ensuring that code hasn't been used before.
// If matches, create custom auth token and then invalidate code
admin.auth().getUserByPhoneNumber(phone)
.then(userRecord => {
const ref = admin.database().ref(`users/${userRecord.uid}/verification`);
ref.on('value', snapshot => {
ref.off();
const verification = snapshot.val();
const timeNow = Date.now();
if (verification.code !== code || !verification.valid || timeNow > verification.expiration) {
return res.status(422).send({ error: 'Code not valid' });
}
ref.update({ valid: false });
admin.auth().createCustomToken(userRecord.uid)
.then(token => res.send({ token }));
});
})
.catch((err) => res.status(422).send({ error: err }));
Finally, on the phone, you can sign in with the custom token the Google Function returned.
firebase.auth().signInWithCustomToken(data.token);
A few pieces of caution. I played fast and loose with both security and potential errors. Also, the number of users in the Firebase auth and database may bloat over time from unverified users. For my purposes, that’s not (currently) a problem.
Would love any feedback. And if anyone wants to crib this, modify it, and/or write a post on it, feel free. Please just attribute at least the inspiration to me. (And throw up my Twitter handle @ppetrick.)
Hmmm, I should have just done the post…
Hope that helps!