MFA Firebase 和 React Flow

MFA Firebase & React Flow

我正尝试按照此设置指南通过 Firebase 的多因素身份验证注册用户:https://cloud.google.com/identity-platform/docs/web/mfa

我正在努力弄清楚如何让我的函数在代码发送到用户的 phone 后等待用户输入验证码(我认为这就是代码出错的原因。)我目前单击“发送验证码”按钮后,下面的代码片段将抛出此错误:错误:'auth/missing-verification-code',消息:'The phone auth credential was created with an empty SMS verification code.'

这是我第一次实施 MFA 流程,所以有人知道我应该如何做吗?谢谢!

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" />&nbsp; 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);

想通了!我错误地认为从 verifyPhoneNumber() 传回的 verificationId 是原始代码,我不想将其保存在客户端的本地状态中,因为我认为这是一个安全漏洞。幸运的是 verificationId 不是要输入的原始代码,而是 JWT 或抽象的东西,所以我只是将该值保存在 React 状态中,然后由单独的函数 getAttemptedCode(values) 引用它仅在用户点击尝试提交的代码后调用。

如果有人发现我发现的这个方法是一个安全漏洞,请告诉我!

I'll add my code block when I am done building the component fully!