Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 326
Jim Avatar asked Jan 17 '20 01:01

Jim


People also ask

How do I authenticate a phone number?

Phone authentication isn't foolproof; just as emails can be fraudulent, so too can phone numbers. The most common method for confirming a working number belongs to the account holder is by sending a one-time code—usually a 4-to-6 digit token—via SMS and asking the recipient to enter that code back into the application.

How do I verify my phone number with OTP?

Go to https://web-otp-demo.glitch.me/ on desktop. Click the Verify button. Send the exact text message that was on the screen from a second phone to the Android device. When the SMS is delivered to the Android device, a dialog appears asking if you want to verify the phone number on the desktop.


2 Answers

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:

  • verifyPhoneNumber() - React Native or Firebase
  • PhoneAuthProvider.credential(id, code) - Firebase
  • signInWithCredential() - React Native or Firebase

Suggested code input component:

  • react-native-confirmation-code-field
like image 97
samthecodingman Avatar answered Nov 09 '22 05:11

samthecodingman


Firebase firebase.auth.PhoneAuthProvider won't give you the code for to compare, you'll have to use verificationId to verify the verificationCode that the user enters. There is a basic example in firebase documentation than uses firebase.auth.PhoneAuthProvider.credential and then tries to sign in using these credentials with firebase.auth().signInWithCredential(phoneCredential):

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 });

              // Prompt the user the enter the verification code they get and save it to state
              const userVerificationCodeInput = this.state.userVerificationCode;
              const phoneCredentials = firebase.auth.PhoneAuthProvider.credential(
                phoneAuthSnapshot.verificationId, 
                userVerificationCodeInput
              );

              // Try to sign in with the phone credentials
              firebase.auth().signInWithCredential(phoneCredentials)
                .then(userCredentials => {
                  // Sign in successfull
                  // Use userCredentials.user and userCredentials.additionalUserInfo 
                })
                .catch(error => {
                  // Check error code to see the reason
                  // Expect something like:
                  // auth/invalid-verification-code
                  // auth/invalid-verification-id
                });

              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));
  });
like image 45
Christos Lytras Avatar answered Nov 09 '22 06:11

Christos Lytras