Firebase phone auth


#1

Is this currently possible in Expo (without detaching)? I noticed that the react-native-firebase team implemented it, but since they build off the native functionality, I’d need to detach, which I’d prefer not to do.

If this isn’t possible, is there a feature request for this that I can upvote (and where)?


#2

What about this one (https://docs.expo.io/versions/latest/guides/using-firebase.html)?


#3

Unfortunately, those docs don’t address the issues associated with Phone auth. Specifically issues around the reCAPTCHA verifier, which you currently can’t get to work from within an Expo project (unless, I gather, you’re willing to detach).


#4

in what kind of phone authentication? You should be able to build a Firebase (username + password) authentication with EXPO. EXPO does even provide authentication with Facebook as well.


#5

Sorry, I suppose I’m not being articulate enough. I’m quite familiar with Firebase and how to use it from within React Native. I’ve set up user/password, as well as third party provider (FB, Google, etc.), auth using Firebase previously.

Last spring, Firebase released phone number auth, i.e., enter your phone number, then receive an SMS and verify. They have a Web API for that as well - https://firebase.google.com/docs/auth/web/phone-auth

Here is the problem. The web API assumes your web app sits on a domain that Google can verify. But a React Native app, and by extension those made with Expo, don’t have a domain the Google verifier can use. Now as far as I can tell, there are 2 options here.

First, install the verifier on a domain I own, then have the app punch out to the web to get the token. That method is described here: https://stackoverflow.com/questions/44040495/using-firebase-phone-number-auth-with-react-native It’s a horrendous user experience.

Alternatively, I could detach and use one of the native Firebase libraries, or a binding that someone else has already built like react-native-firebase.

But I really, really, really don’t want to detach, so I was hoping there was another alternative that I wasn’t seeing.


#6

@ppetrick did you find any solution?
please help


#7

I had to roll my own solution using custom tokens and Twilio to send auth messages.


#8

Hey @ppetrick, I’m running into the same problem - would you be willing to share more details about your custom implementation?


#9

hi! nobody at expo has tried to implement firebase phone auth before, this seems really neat though, please write a blog post if you figure out how to get it working without detaching! an alternative you could try is amazon cognito, which works nicely with expo and has phone-based authentication as well (see: https://github.com/aws/aws-amplify#react-native-development).


#10

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!


#11

@ppetrick. I a little bit confused with the solution… How do you pass the phone number to the google function
As far as i know , we get the phone number in client side , And the google function is not there.


#12

Use Axios or some other http library to send post requests from the client to Google functions. You’ll make 2 requests. The first is request includes just the phone number. The second request includes the phone number and verification code. (Obviously, not the most secure solution, per my original caveats.)


#13

Trying something same , Does expo supports firebase phone auth now , or i have to detach the project ?


#14

I have to tried to implement the missing features for authentication using expo and firebase (Phone Number Authentication + Recaptcha, Twitter authentication and Github Authentication ). You can check out here - http://arivaa-firebase.laxaar.com/


#15

We have implemented Recaptcha verifier in our app - http://arivaa-firebase.laxaar.com/


#16

Hey, can you post of an example here? That is just a link to a product you’re selling.


#17

Hi,

I can explain the approach here since the code is copyrighted. Basically we have implemented RecaptchaVerifier Interface for expo which enables us to solve the first piece of puzzle in phone auth which is getting the captcha from user.Then We take the captcha from user via a webview and then send that captcha (We encapsulate it in RecaptchaVerifier we implemented) to the firebase while using phone authentication.


#18

Here is a more complete answer building off of @ppetrick’s answer. This shows the steps necessary to get your firebase functions working with your Expo application.

Updated to use firestore instead of the realtime database.

// <project_root>/functions/index.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

/*
  set environment variables via terminal
  $ firebase functions:config:set twilio.account=<accountSid> twilio.token=<authToken> twilio.from=<your from number>
*/
const config = functions.config();
const twilio = require('twilio')(config.twilio.account, config.twilio.token);

/*
  Now you need to download a private cert (service-account.json)
  https://firebase.google.com/docs/admin/setup#add_firebase_to_your_app
  and add add it to your /functions directory (don't forget to .gitignore it)
*/
const serviceAccount = require('./service-account.json');
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://<your-project-id>.firebaseio.com',
});

// 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
exports.logInWithPhoneNumber = functions.https.onRequest((req, res) => {
  const { phone } = req.body;
  admin
    .auth()
    .getUserByPhoneNumber(phone)
    .then(userRecord => sendSMSVerification(res, userRecord.uid, phone))
    .catch(error => {
      switch (error.code) {
        case 'auth/user-not-found':
          return createUser();
        default:
          return sendError(error);
      }
    });

  const createUser = () => {
    admin
      .auth()
      .createUser({
        phoneNumber: phone,
      })
      .then(({ uid }) => sendSMSVerification(res, uid, phone))
      .catch(sendError);
  };

  const sendError = error => res.status(422).send(error);

  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 };
    const setUser = message => {
      admin
        .firestore()
        .collection('users')
        .doc(uid)
        .set({ verification })
        .then(() => res.send({ success: true }))
        .catch(sendError);
    };
    twilio.messages
      .create({
        body: `Your family code is ${code}`,
        to: phone,
        from: config.twilio.from,
      })
      .then(setUser)
      .catch(sendError);
  };
});

exports.verifyToken = functions.https.onRequest((req, res) => {
  // Validate code and expiration time, ensuring that code hasn't been used before.
  // If matches, create custom auth token and then invalidate code
  const { code, phone } = req.body;
  const sendError = error => res.status(422).send({ error: error.message });

  admin
    .auth()
    .getUserByPhoneNumber(phone)
    .then(userRecord => {
      return admin
        .firestore()
        .collection('users')
        .doc(userRecord.uid)
        .get();
    })
    .then(doc => {
      if (!doc.exists) {
        return Promise.reject(new Error('custom/uid-not-found'));
      }

      const timeNow = Date.now();
      const { verification } = doc.data();
      let error = null;

      if (verification.code !== parseInt(code, 10)) {
        error = 'custom/code-does-not-match';
      } else if (!verification.valid) {
        error = 'custom/code-already-used';
      } else if (timeNow > verification.expiration) {
        error = 'custom/code-expired';
      }

      if (error) {
        return Promise.reject(new Error(error));
      }

      doc.ref.update({ 'verification.valid': false });
      return Promise.resolve(doc.id);
    })
    .then(uid => admin.auth().createCustomToken(uid))
    .then(token => res.send({ token }))
    .catch(sendError);
});

#19

Solution is here: https://stackoverflow.com/a/52350212/5245349
Hope it helps


#20

Awesome solution, thanks @evilandfox!
Also works when showing captcha in WebView inside the app UI.