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

12,793

Solution 1

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:

Solution 2

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

Solution 3

This is not supported by firebase unfortunately. Logging in and out after signInWithCredential can work, but is very confusing

Share:
12,793
Jim
Author by

Jim

Updated on July 28, 2022

Comments

  • Jim
    Jim almost 2 years

    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?

  • Jim
    Jim over 4 years
    as my question states, I'm looking to verify phone number ownership to continue in registration. im not looking to actually authenticate with a phone. I just want to send a OTP and compare that with what the user entered. How can your suggestion be modified to accomplish what ive said without using .signInWithCredential(), because as I said, im not actually using phone authentication. I just want to verify the phone number
  • Jim
    Jim over 4 years
    I don't actually want to sign in with the phone number though. I want to sign-in with email/password. I want to verify phone number ownership using firebase & verifyPhoneNumber(). So how do I do this without using signInWithCredential()
  • Christos Lytras
    Christos Lytras over 4 years
    @Jim it's quite simple. You don't have to rely on authentication to continue with your app; once you get user to sign in after firebase.auth().signInWithCredential(phoneCredentials) using phoneAuthSnapshot.verificationId then you will know that the code the user entered is correct. You can save that and continue with your app and then sign the user out and sign them again using email/password authentication.
  • samthecodingman
    samthecodingman over 4 years
    @Jim I have updated the answer to verify using a background instance of a FirebaseApp, which will leave the currently signed in user alone.
  • Jim
    Jim over 4 years
    ahh I see. seems a little clunky but ill try out both answers and get back
  • Jim
    Jim over 4 years
    I've tried your suggestion, and it resulted in the following: 1) react-native-firebase does not support the setPersistence() method so I commented out that line in my code, I dont know how important that part is but I obviously cant use it with this library. 2) Now im dealing with this error: [Firebase/Core][I-COR000004] App with name AUTH-WORKER does not exist. which executes the 'other internal error' part of your catch() statement, its happens during the execution on the compareCode() method
  • sjsam
    sjsam over 4 years
    @samthecodingman the verification code is not exposed to your application? Is not the verification code sent to the device? How come this is OOBA?
  • samthecodingman
    samthecodingman over 4 years
    @sjsam I'll try explaining using an example. Your application wants to verify a phone number, so you make a request to Firebase to send a code to that phone number (let's say the code is 1234). Your application won't be sent 1234 and is instead sent an ID (let's say it's ABCD). You then prompt the user to enter the code they got sent. Once they have input the code, you send it to Firebase asking "is <INPUT> the right code for request ABCD?", to which the server would respond yes or no. If the server just returned 1234, someone could just use that code to claim they own the phone number.
  • MBK
    MBK about 3 years
    How to sign in if we do so. Because everytime login in it ask for phone number and send messages. I also want only send sms on registration for verify ownership and all other like sign in with email/ password. @Jim do you find any way?