Search code examples
firebasefirebase-authenticationreact-native-firebase

How to use Firebase's 'verifyPhoneNumber()' to confirm phone # ownership without using # to sign-in?


Im using react-native-firebase v5.6 in a project.

Goal: In the registration flow, I have the user input their phone number, I then send a OTP to said phone number. I want to be able to compare the code entered by the user with the code sent from Firebase, to be able to grant entry to the next steps in registration.

Problem: the user gets the SMS OTP and everything , but the phoneAuthSnapshot object returned by firebase.auth().verifyPhoneNumber(number).on('state_changed', (phoneAuthSnapshot => {}), it doesn't give a value for the code that firebase sent, so there's nothing to compare the users entered code with. However, there's a value for the verificationId property. Here's the object return from the above method:

'Verification code sent', { 
  verificationId: 'AM5PThBmFvPRB6x_tySDSCBG-6tezCCm0Niwm2ohmtmYktNJALCkj11vpwyou3QGTg_lT4lkKme8UvMGhtDO5rfMM7U9SNq7duQ41T8TeJupuEkxWOelgUiKf_iGSjnodFv9Jee8gvHc50XeAJ3z7wj0_BRSg_gwlN6sumL1rXJQ6AdZwzvGetebXhZMb2gGVQ9J7_JZykCwREEPB-vC0lQcUVdSMBjtig',
  code: null,
  error: null,
  state: 'sent' 
}

Here is my on-screen implementation:

firebase
  .firestore()
  .collection('users')
  .where('phoneNumber', '==', this.state.phoneNumber)
  .get()
  .then((querySnapshot) => {
    if (querySnapshot.empty === true) {
      // change status
      this.setState({ status: 'Sending confirmation code...' });
      // send confirmation OTP
      firebase.auth().verifyPhoneNumber(this.state.phoneNumber).on(
        'state_changed',
        (phoneAuthSnapshot) => {
          switch (phoneAuthSnapshot.state) {
            case firebase.auth.PhoneAuthState.CODE_SENT:
              console.log('Verification code sent', phoneAuthSnapshot);
              this.setState({ status: 'Confirmation code sent.', confirmationCode: phoneAuthSnapshot.code });

              break;
            case firebase.auth.PhoneAuthState.ERROR:
              console.log('Verification error: ' + JSON.stringify(phoneAuthSnapshot));
              this.setState({ status: 'Error sending code.', processing: false });
              break;
          }
        },
        (error) => {
          console.log('Error verifying phone number: ' + error);
        }
      );
    }
  })
  .catch((error) => {
    // there was an error
    console.log('Error during firebase operation: ' + JSON.stringify(error));
  });

How do I get the code sent from Firebase to be able to compare?


Solution

  • As @christos-lytras had in their answer, the verification code is not exposed to your application.

    This is done for security reasons as providing the code used for the out of band authentication to the device itself would allow a knowledgeable user to just take the code out of memory and authenticate as if they had access to that phone number.

    The general flow of operations is:

    1. Get the phone number to be verified
    2. Use that number with verifyPhoneNumber() and cache the verification ID it returns
    3. Prompt the user to input the code (or automatically retrieve it)
    4. Bundle the ID and the user's input together as a credential using firebase.auth.PhoneAuthProvider.credential(id, code)
    5. Attempt to sign in with that credential using firebase.auth().signInWithCredential(credential)

    In your source code, you also use the on(event, observer, errorCb, successCb) listener of the verifyPhoneNumber(phoneNumber) method. However this method also supports listening to results using Promises, which allows you to chain to your Firebase query. This is shown below.

    Sending the verification code:

    firebase
      .firestore()
      .collection('users')
      .where('phoneNumber', '==', this.state.phoneNumber)
      .get()
      .then((querySnapshot) => {
        if (!querySnapshot.empty) {
          // User found with this phone number.
          throw new Error('already-exists');
        }
    
        // change status
        this.setState({ status: 'Sending confirmation code...' });
    
        // send confirmation OTP
        return firebase.auth().verifyPhoneNumber(this.state.phoneNumber)
      })
      .then((phoneAuthSnapshot) => {
        // verification sent
        this.setState({
          status: 'Confirmation code sent.',
          verificationId: phoneAuthSnapshot.verificationId,
          showCodeInput: true // shows input field such as react-native-confirmation-code-field
        });
      })
      .catch((error) => {
        // there was an error
        let newStatus;
        if (error.message === 'already-exists') {
          newStatus = 'Sorry, this phone number is already in use.';
        } else {
          // Other internal error
          // see https://firebase.google.com/docs/reference/js/firebase.firestore.html#firestore-error-code
          // see https://firebase.google.com/docs/reference/js/firebase.auth.PhoneAuthProvider#verify-phone-number
          // probably 'unavailable' or 'deadline-exceeded' for loss of connection while querying users
          newStatus = 'Failed to send verification code.';
          console.log('Unexpected error during firebase operation: ' + JSON.stringify(error));
        }
    
        this.setState({
          status: newStatus,
          processing: false
        });
      });
    

    Handling a user-sourced verification code:

    codeInputSubmitted(code) {
      const { verificationId } = this.state;
    
      const credential = firebase.auth.PhoneAuthProvider.credential(
        verificationId,
        code
      );
    
      // To verify phone number without interfering with the existing user
      // who is signed in, we offload the verification to a worker app.
      let fbWorkerApp = firebase.apps.find(app => app.name === 'auth-worker')
                     || firebase.initializeApp(firebase.app().options, 'auth-worker');
      fbWorkerAuth = fbWorkerApp.auth();  
      fbWorkerAuth.setPersistence(firebase.auth.Auth.Persistence.NONE); // disables caching of account credentials
    
      fbWorkerAuth.signInWithCredential(credential)
        .then((userCredential) => {
          // userCredential.additionalUserInfo.isNewUser may be present
          // userCredential.credential can be used to link to an existing user account
    
          // successful
          this.setState({
            status: 'Phone number verified!',
            verificationId: null,
            showCodeInput: false,
            user: userCredential.user;
          });
    
          return fbWorkerAuth.signOut().catch(err => console.error('Ignored sign out error: ', err);
        })
        .catch((err) => {
          // failed
          let userErrorMessage;
          if (error.code === 'auth/invalid-verification-code') {
            userErrorMessage = 'Sorry, that code was incorrect.'
          } else if (error.code === 'auth/user-disabled') {
            userErrorMessage = 'Sorry, this phone number has been blocked.';
          } else {
            // other internal error
            // see https://firebase.google.com/docs/reference/js/firebase.auth.Auth.html#sign-inwith-credential
            userErrorMessage = 'Sorry, we couldn\'t verify that phone number at the moment. '
              + 'Please try again later. '
              + '\n\nIf the issue persists, please contact support.'
          }
          this.setState({
            codeInputErrorMessage: userErrorMessage
          });
        })
    }
    

    API References:

    Suggested code input component: