feat: fix bugs in MFA (#2033)

* fix: prompt mfa binding

* fix: clean session when leave promptpage

* fix: css

* fix: force enable mfa

* fix: add prompt rule

* fix: refactor directory structure

* fix: prompt notification

* fix: fix some bug and clean code

* fix: rebase

* fix: improve notification

* fix: i18n

* fix: router

* fix: prompt

* fix: remove localStorage
This commit is contained in:
Yaodong Yu
2023-07-07 12:30:07 +08:00
committed by GitHub
parent 6edfc08b28
commit 347d3d2b53
34 changed files with 941 additions and 607 deletions

View File

@ -12,181 +12,55 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import React, {useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import React from "react";
import {Button, Col, Result, Row, Steps} from "antd";
import {withRouter} from "react-router-dom";
import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting";
import i18next from "i18next";
import * as MfaBackend from "../backend/MfaBackend";
import {CheckOutlined, KeyOutlined, LockOutlined, UserOutlined} from "@ant-design/icons";
import * as UserBackend from "../backend/UserBackend";
import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
import {CheckOutlined, KeyOutlined, UserOutlined} from "@ant-design/icons";
import CheckPasswordForm from "./mfa/CheckPasswordForm";
import MfaEnableForm from "./mfa/MfaEnableForm";
import {MfaVerifyForm} from "./mfa/MfaVerifyForm";
export const EmailMfaType = "email";
export const SmsMfaType = "sms";
export const TotpMfaType = "app";
export const RecoveryMfaType = "recovery";
function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({password}) => {
const data = {...user, password};
UserBackend.checkUserPassword(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.finally(() => {
form.setFieldsValue({password: ""});
});
};
return (
<Form
form={form}
style={{width: "300px", marginTop: "20px"}}
onFinish={onFinish}
>
<Form.Item
name="password"
rules={[{required: true, message: i18next.t("login:Please input your password!")}]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={i18next.t("general:Password")}
/>
</Form.Item>
<Form.Item>
<Button
style={{marginTop: 24}}
loading={false}
block
type="primary"
htmlType="submit"
>
{i18next.t("forget:Next Step")}
</Button>
</Form.Item>
</Form>
);
}
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm();
const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user};
MfaBackend.MfaSetupVerify(data)
.then((res) => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
})
.catch((error) => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
})
.finally(() => {
form.setFieldsValue({passcode: ""});
});
};
if (mfaProps === undefined || mfaProps === null) {
return <div></div>;
}
if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaSmsVerifyForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaProps.mfaType === TotpMfaType) {
return <MfaTotpVerifyForm mfaProps={mfaProps} onFinish={onFinish} />;
} else {
return <div></div>;
}
}
function EnableMfaForm({user, mfaType, recoveryCodes, onSuccess, onFail}) {
const [loading, setLoading] = useState(false);
const requestEnableTotp = () => {
const data = {
mfaType,
...user,
};
setLoading(true);
MfaBackend.MfaSetupEnable(data).then(res => {
if (res.status === "ok") {
onSuccess(res);
} else {
onFail(res);
}
}
).finally(() => {
setLoading(false);
});
};
return (
<div style={{width: "400px"}}>
<p>{i18next.t("mfa:Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code")}</p>
<br />
<code style={{fontStyle: "solid"}}>{recoveryCodes[0]}</code>
<Button style={{marginTop: 24}} loading={loading} onClick={() => {
requestEnableTotp();
}} block type="primary">
{i18next.t("general:Enable")}
</Button>
</div>
);
}
class MfaSetupPage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(props.location.search);
const {location} = this.props;
this.state = {
account: props.account,
application: this.props.application ?? null,
application: null,
applicationName: props.account.signupApplication ?? "",
isAuthenticated: props.isAuthenticated ?? false,
isPromptPage: props.isPromptPage,
redirectUri: props.redirectUri,
current: props.current ?? 0,
mfaType: props.mfaType ?? new URLSearchParams(props.location?.search)?.get("mfaType") ?? SmsMfaType,
current: location.state?.from !== undefined ? 1 : 0,
mfaProps: null,
mfaType: params.get("mfaType") ?? SmsMfaType,
isPromptPage: props.isPromptPage || location.state?.from !== undefined,
};
}
componentDidMount() {
this.getApplication();
if (this.state.current === 1) {
this.initMfaProps();
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && (this.state.mfaProps === null || this.state.mfaType !== prevState.mfaType)) {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
if (this.state.mfaType !== prevState.mfaType || this.state.current !== prevState.current) {
if (this.state.current === 1) {
this.initMfaProps();
}
}
}
getApplication() {
if (this.state.application !== null) {
return;
}
ApplicationBackend.getApplication("admin", this.state.applicationName)
.then((res) => {
if (res !== null) {
@ -203,11 +77,75 @@ class MfaSetupPage extends React.Component {
});
}
initMfaProps() {
MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType,
...this.getUser(),
}).then((res) => {
if (res.status === "ok") {
this.setState({
mfaProps: res.data,
});
} else {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
}
});
}
getUser() {
return {
name: this.state.account.name,
owner: this.state.account.owner,
return this.props.account;
}
renderMfaTypeSwitch() {
const renderSmsLink = () => {
if (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${SmsMfaType}`);
}
}>{i18next.t("mfa:Use SMS")}</Button>
);
};
const renderEmailLink = () => {
if (this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${EmailMfaType}`);
}
}>{i18next.t("mfa:Use Email")}</Button>
);
};
const renderTotpLink = () => {
if (this.state.mfaType === TotpMfaType || this.props.account.totpSecret !== "") {
return null;
}
return (<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
this.props.history.push(`/mfa/setup?mfaType=${TotpMfaType}`);
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
);
};
return !this.state.isPromptPage ? (
<React.Fragment>
{renderSmsLink()}
{renderEmailLink()}
{renderTotpLink()}
</React.Fragment>
) : null;
}
renderStep() {
@ -219,19 +157,14 @@ class MfaSetupPage extends React.Component {
onSuccess={() => {
this.setState({
current: this.state.current + 1,
isAuthenticated: true,
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA") + ": " + res.msg);
}}
/>
);
case 1:
if (!this.state.isAuthenticated) {
return null;
}
return (
<div>
<MfaVerifyForm
@ -244,52 +177,25 @@ class MfaSetupPage extends React.Component {
});
}}
onFail={(res) => {
Setting.showMessage("error", i18next.t("general:Failed to verify"));
Setting.showMessage("error", i18next.t("general:Failed to verify") + ": " + res.msg);
}}
/>
<Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: EmailMfaType,
});
}
}>{i18next.t("mfa:Use Email")}</Button>
}
{
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: SmsMfaType,
});
}
}>{i18next.t("mfa:Use SMS")}</Button>
}
{
(this.state.mfaType === TotpMfaType) ? null :
<Button type={"link"} onClick={() => {
this.setState({
mfaType: TotpMfaType,
});
}
}>{i18next.t("mfa:Use Authenticator App")}</Button>
}
{this.renderMfaTypeSwitch()}
</Col>
</div>
);
case 2:
if (!this.state.isAuthenticated) {
return null;
}
return (
<EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully"));
if (this.state.isPromptPage && this.state.redirectUri) {
Setting.goToLink(this.state.redirectUri);
this.props.onfinish(true);
if (localStorage.getItem("mfaRedirectUrl") !== null) {
Setting.goToLink(localStorage.getItem("mfaRedirectUrl"));
localStorage.removeItem("mfaRedirectUrl");
} else {
Setting.goToLink("/account");
this.props.history.push("/account");
}
}}
onFail={(res) => {
@ -308,7 +214,7 @@ class MfaSetupPage extends React.Component {
status="403"
title="403 Unauthorized"
subTitle={i18next.t("general:Sorry, you do not have permission to access this page or logged in status invalid.")}
extra={<a href="/"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
extra={<a href="/web/public"><Button type="primary">{i18next.t("general:Back Home")}</Button></a>}
/>
);
}
@ -343,4 +249,4 @@ class MfaSetupPage extends React.Component {
}
}
export default MfaSetupPage;
export default withRouter(MfaSetupPage);