I am trying to enroll a user with Multifactor authentication with Firebase following this setup guide: https://cloud.google.com/identity-platform/docs/web/mfa
I am struggling to figure out how to have my function wait for the user inputted verification code after the code is sent to the user's phone (I think this is why the code is erroring.) My current code snippet below will throw this error after I click the Send Verification Code button: error: 'auth/missing-verification-code', message: 'The phone auth credential was created with an empty SMS verification code.'
This is the first time I have implemented a MFA flow , so anyone have ideas on how I should be doing this? Thnaks!
import React, { Component } from 'react'
import { store } from 'react-notifications-component';
import { Grid, Row, Col } from 'react-flexbox-grid';
import { withRouter } from 'react-router-dom';
import { Form, Formik } from 'formik';
import { NOTIFICATION } from '../../../utils/constants.js';
import { firestore, firebase } from "../../../Fire.js";
import { updateProfileSchema, updateProfilePhoneSchema, checkVCodeSchema } from "../../../utils/formSchemas"
import { Hr, Recaptcha, Wrapper } from '../../../utils/styles/misc.js';
import { FField } from '../../../utils/styles/forms.js';
import { H1, Label, RedText, H2, LLink, GreenHoverText, SmText } from '../../../utils/styles/text.js';
import { MdGreenToInvBtn, MdInvToPrimaryBtn } from '../../../utils/styles/buttons.js';
class AdminProfile extends Component {
constructor(props) {
super(props)
this.state = {
user: "",
codeSent: false,
editingPhone: false,
vCode: "",
loading: {
user: true
}
}
}
componentDidMount(){
this.unsubscribeUser = firestore.collection("users").doc(this.props.user.uid)
.onSnapshot((doc) => {
if(doc.exists){
let docWithMore = Object.assign({}, doc.data());
docWithMore.id = doc.id;
this.setState({
user: docWithMore,
loading: {
user: false
}
})
} else {
console.error("User doesn't exist.")
}
});
}
componentWillUnmount() {
if(this.unsubscribeUser){
this.unsubscribeUser();
}
}
sendVerificationCode = (values) => {
store.addNotification({
title: "reCAPTCHA",
message: `Please complete the reCAPTCHA below to continue.`,
type: "success",
...NOTIFICATION
})
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha', {
'callback': (response) => {
this.props.user.multiFactor.getSession().then((multiFactorSession) => {
// Specify the phone number and pass the MFA session.
let phoneInfoOptions = {
phoneNumber: values.phone,
session: multiFactorSession
};
let phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
// Send SMS verification code.
return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, window.recaptchaVerifier);
}).then(async (verificationId) => {
this.setState({
codeSent: true
})
// Ask user for the verification code.
// TODO: how to do this async? do I need to split up my requests?
// let code = await this.getAttemptedCode()
let cred = firebase.auth.PhoneAuthProvider.credential(verificationId, this.state.vCode);
let multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
// Complete enrollment.
this.props.user.multiFactor.multiFactor.enroll(multiFactorAssertion, this.props.user.userName);
}).catch((error) => {
console.error("Error adding multi-factor authentication: ", error);
store.addNotification({
title: "Error",
message: `Error adding multi-factor authentication: ${error}`,
type: "danger",
...NOTIFICATION
})
window.recaptchaVerifier.clear()
});;
},
'expired-callback': () => {
// Response expired. Ask user to solve reCAPTCHA again.
store.addNotification({
title: "Timeout",
message: `Please solve the reCAPTCHA again.`,
type: "danger",
...NOTIFICATION
})
window.recaptchaVerifier.clear()
}
});
window.recaptchaVerifier.render()
}
getAttemptedCode = async () => {
}
render() {
if(this.state.loading.user){
return (
<Wrapper>
<H2>Loading...</H2>
</Wrapper>
)
} else {
return (
<Wrapper>
<LLink to={`/admin/dashboard`}>
<MdInvToPrimaryBtn type="button">
<i className="fas fa-chevron-left" /> Return to admin dashboard
</MdInvToPrimaryBtn>
</LLink>
<H1>Admin Profile</H1>
<Formik
initialValues={{
firstName: this.state.user.firstName,
lastName: this.state.user.lastName,
email: this.state.user.email,
phone: this.state.user.phone
}}
enableReinitialize={true}
validationSchema={updateProfileSchema}
onSubmit={(values, actions) => {
//this.updateProfile(values);
actions.resetForm();
}}
>
{props => (
<Form>
<Grid fluid>
<Row>
<Col xs={12}>
<Label htmlFor="phone">Phone: </Label>
<SmText><RedText> <GreenHoverText onClick={() => this.setState({ editingPhone: true })}>update phone</GreenHoverText></RedText></SmText>
<FField
type="phone"
disabled={true}
onChange={props.handleChange}
name="phone"
value={props.values.phone}
placeholder="(123) 456-7890"
/>
</Col>
</Row>
<Row center="xs">
<Col xs={12}>
<MdGreenToInvBtn type="submit" disabled={!props.dirty && !props.isSubmitting}>
Update
</MdGreenToInvBtn>
</Col>
</Row>
</Grid>
</Form>
)}
</Formik>
{this.state.editingPhone && (
<>
<Hr/>
<Formik
initialValues={{
phone: this.state.user.phone
}}
enableReinitialize={true}
validationSchema={updateProfilePhoneSchema}
onSubmit={(values, actions) => {
this.sendVerificationCode(values);
}}
>
{props => (
<Form>
<Grid fluid>
<Row>
<Col xs={12} sm={6}>
<Label htmlFor="phone">Phone: </Label>
<FField
type="phone"
onChange={props.handleChange}
name="phone"
value={props.values.phone}
placeholder="(123) 456-7890"
/>
{props.errors.phone && props.touched.phone ? (
<RedText>{props.errors.phone}</RedText>
) : (
""
)}
</Col>
</Row>
<Row center="xs">
<Col xs={12}>
<MdGreenToInvBtn type="submit" disabled={!props.dirty && !props.isSubmitting}>
Send verification code
</MdGreenToInvBtn>
</Col>
</Row>
</Grid>
</Form>
)}
</Formik>
</>
)}
{this.state.codeSent && (
<>
<Formik
initialValues={{
vCode: ""
}}
enableReinitialize={true}
validationSchema={checkVCodeSchema}
onSubmit={(values, actions) => {
this.SetState({ vCode: values.vCode });
}}
>
{props => (
<Form>
<Grid fluid>
<Row>
<FField
type="text"
onChange={props.handleChange}
name="vCode"
value={props.values.vCode}
placeholder="abc123"
/>
{props.errors.vCode && props.touched.vCode ? (
<RedText>{props.errors.vCode}</RedText>
) : (
""
)}
</Row>
<Row center="xs">
<Col xs={12}>
{/* TODO: add send code again button? */}
<MdGreenToInvBtn type="submit" disabled={!props.dirty && !props.isSubmitting}>
Submit verification code
</MdGreenToInvBtn>
</Col>
</Row>
</Grid>
</Form>
)}
</Formik>
</>
)}
<Recaptcha id="recaptcha" />
</Wrapper>
)
}
}
}
export default withRouter(AdminProfile);