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" /> 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!
我正尝试按照此设置指南通过 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" /> 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!