How to use Firebase's 'verifyPhoneNumber()' to confirm phone # ownership without using # to sign-in?
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:
- Get the phone number to be verified
- Use that number with
verifyPhoneNumber()
and cache the verification ID it returns - Prompt the user to input the code (or automatically retrieve it)
- Bundle the ID and the user's input together as a credential using
firebase.auth.PhoneAuthProvider.credential(id, code)
- 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:
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
Jim
Updated on July 28, 2022Comments
-
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 byfirebase.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 theverificationId
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 over 4 yearsas 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 over 4 yearsI 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 usingsignInWithCredential()
-
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)
usingphoneAuthSnapshot.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 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 over 4 yearsahh I see. seems a little clunky but ill try out both answers and get back
-
Jim over 4 yearsI'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 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 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 about 3 yearsHow 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?