feat: support stateless MFA setup (#3382)

This commit is contained in:
DacongDA
2024-11-29 19:50:10 +08:00
committed by GitHub
parent 2d1736f13a
commit fe40910e3b
7 changed files with 82 additions and 94 deletions

View File

@ -22,13 +22,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const (
MfaRecoveryCodesSession = "mfa_recovery_codes"
MfaCountryCodeSession = "mfa_country_code"
MfaDestSession = "mfa_dest"
MfaTotpSecretSession = "mfa_totp_secret"
)
// MfaSetupInitiate // MfaSetupInitiate
// @Title MfaSetupInitiate // @Title MfaSetupInitiate
// @Tag MFA API // @Tag MFA API
@ -72,11 +65,6 @@ func (c *ApiController) MfaSetupInitiate() {
} }
recoveryCode := uuid.NewString() recoveryCode := uuid.NewString()
c.SetSession(MfaRecoveryCodesSession, recoveryCode)
if mfaType == object.TotpType {
c.SetSession(MfaTotpSecretSession, mfaProps.Secret)
}
mfaProps.RecoveryCodes = []string{recoveryCode} mfaProps.RecoveryCodes = []string{recoveryCode}
resp := mfaProps resp := mfaProps
@ -94,6 +82,9 @@ func (c *ApiController) MfaSetupInitiate() {
func (c *ApiController) MfaSetupVerify() { func (c *ApiController) MfaSetupVerify() {
mfaType := c.Ctx.Request.Form.Get("mfaType") mfaType := c.Ctx.Request.Form.Get("mfaType")
passcode := c.Ctx.Request.Form.Get("passcode") passcode := c.Ctx.Request.Form.Get("passcode")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
if mfaType == "" || passcode == "" { if mfaType == "" || passcode == "" {
c.ResponseError("missing auth type or passcode") c.ResponseError("missing auth type or passcode")
@ -104,32 +95,28 @@ func (c *ApiController) MfaSetupVerify() {
MfaType: mfaType, MfaType: mfaType,
} }
if mfaType == object.TotpType { if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession) if secret == "" {
if secret == nil {
c.ResponseError("totp secret is missing") c.ResponseError("totp secret is missing")
return return
} }
config.Secret = secret.(string) config.Secret = secret
} else if mfaType == object.SmsType { } else if mfaType == object.SmsType {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
config.Secret = dest.(string) config.Secret = dest
countryCode := c.GetSession(MfaCountryCodeSession) if countryCode == "" {
if countryCode == nil {
c.ResponseError("country code is missing") c.ResponseError("country code is missing")
return return
} }
config.CountryCode = countryCode.(string) config.CountryCode = countryCode
} else if mfaType == object.EmailType { } else if mfaType == object.EmailType {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
config.Secret = dest.(string) config.Secret = dest
} }
mfaUtil := object.GetMfaUtil(mfaType, config) mfaUtil := object.GetMfaUtil(mfaType, config)
@ -159,6 +146,10 @@ func (c *ApiController) MfaSetupEnable() {
owner := c.Ctx.Request.Form.Get("owner") owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name") name := c.Ctx.Request.Form.Get("name")
mfaType := c.Ctx.Request.Form.Get("mfaType") mfaType := c.Ctx.Request.Form.Get("mfaType")
secret := c.Ctx.Request.Form.Get("secret")
dest := c.Ctx.Request.Form.Get("dest")
countryCode := c.Ctx.Request.Form.Get("secret")
recoveryCodes := c.Ctx.Request.Form.Get("recoveryCodes")
user, err := object.GetUser(util.GetId(owner, name)) user, err := object.GetUser(util.GetId(owner, name))
if err != nil { if err != nil {
@ -176,43 +167,39 @@ func (c *ApiController) MfaSetupEnable() {
} }
if mfaType == object.TotpType { if mfaType == object.TotpType {
secret := c.GetSession(MfaTotpSecretSession) if secret == "" {
if secret == nil {
c.ResponseError("totp secret is missing") c.ResponseError("totp secret is missing")
return return
} }
config.Secret = secret.(string) config.Secret = secret
} else if mfaType == object.EmailType { } else if mfaType == object.EmailType {
if user.Email == "" { if user.Email == "" {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
user.Email = dest.(string) user.Email = dest
} }
} else if mfaType == object.SmsType { } else if mfaType == object.SmsType {
if user.Phone == "" { if user.Phone == "" {
dest := c.GetSession(MfaDestSession) if dest == "" {
if dest == nil {
c.ResponseError("destination is missing") c.ResponseError("destination is missing")
return return
} }
user.Phone = dest.(string) user.Phone = dest
countryCode := c.GetSession(MfaCountryCodeSession) if countryCode == "" {
if countryCode == nil {
c.ResponseError("country code is missing") c.ResponseError("country code is missing")
return return
} }
user.CountryCode = countryCode.(string) user.CountryCode = countryCode
} }
} }
recoveryCodes := c.GetSession(MfaRecoveryCodesSession)
if recoveryCodes == nil { if recoveryCodes == "" {
c.ResponseError("recovery codes is missing") c.ResponseError("recovery codes is missing")
return return
} }
config.RecoveryCodes = []string{recoveryCodes.(string)} config.RecoveryCodes = []string{recoveryCodes}
mfaUtil := object.GetMfaUtil(mfaType, config) mfaUtil := object.GetMfaUtil(mfaType, config)
if mfaUtil == nil { if mfaUtil == nil {
@ -226,14 +213,6 @@ func (c *ApiController) MfaSetupEnable() {
return return
} }
c.DelSession(MfaRecoveryCodesSession)
if mfaType == object.TotpType {
c.DelSession(MfaTotpSecretSession)
} else {
c.DelSession(MfaCountryCodeSession)
c.DelSession(MfaDestSession)
}
c.ResponseOk(http.StatusText(http.StatusOK)) c.ResponseOk(http.StatusText(http.StatusOK))
} }

View File

@ -246,8 +246,6 @@ func (c *ApiController) SendVerificationCode() {
if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedEmail(mfaProps.Secret) == vform.Dest {
vform.Dest = mfaProps.Secret vform.Dest = mfaProps.Secret
} }
} else if vform.Method == MfaSetupVerification {
c.SetSession(MfaDestSession, vform.Dest)
} }
provider, err = application.GetEmailProvider(vform.Method) provider, err = application.GetEmailProvider(vform.Method)
@ -282,11 +280,6 @@ func (c *ApiController) SendVerificationCode() {
vform.CountryCode = user.GetCountryCode(vform.CountryCode) vform.CountryCode = user.GetCountryCode(vform.CountryCode)
} }
} }
if vform.Method == MfaSetupVerification {
c.SetSession(MfaCountryCodeSession, vform.CountryCode)
c.SetSession(MfaDestSession, vform.Dest)
}
} else if vform.Method == MfaAuthVerification { } else if vform.Method == MfaAuthVerification {
mfaProps := user.GetPreferredMfaProps(false) mfaProps := user.GetPreferredMfaProps(false)
if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest { if user != nil && util.GetMaskedPhone(mfaProps.Secret) == vform.Dest {

View File

@ -179,8 +179,10 @@ class MfaSetupPage extends React.Component {
mfaProps={this.state.mfaProps} mfaProps={this.state.mfaProps}
application={this.state.application} application={this.state.application}
user={this.props.account} user={this.props.account}
onSuccess={() => { onSuccess={(res) => {
this.setState({ this.setState({
dest: res.dest,
countryCode: res.countryCode,
current: this.state.current + 1, current: this.state.current + 1,
}); });
}} }}
@ -195,7 +197,7 @@ class MfaSetupPage extends React.Component {
); );
case 2: case 2:
return ( return (
<MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes} <MfaEnableForm user={this.getUser()} mfaType={this.state.mfaType} secret={this.state.mfaProps.secret} recoveryCodes={this.state.mfaProps.recoveryCodes} dest={this.state.dest} countryCode={this.state.countryCode}
onSuccess={() => { onSuccess={() => {
Setting.showMessage("success", i18next.t("general:Enabled successfully")); Setting.showMessage("success", i18next.t("general:Enabled successfully"));
this.props.onfinish(); this.props.onfinish();

View File

@ -3,11 +3,15 @@ import i18next from "i18next";
import React, {useState} from "react"; import React, {useState} from "react";
import * as MfaBackend from "../../backend/MfaBackend"; import * as MfaBackend from "../../backend/MfaBackend";
export function MfaEnableForm({user, mfaType, recoveryCodes, onSuccess, onFail}) { export function MfaEnableForm({user, mfaType, secret, recoveryCodes, dest, countryCode, onSuccess, onFail}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const requestEnableMfa = () => { const requestEnableMfa = () => {
const data = { const data = {
mfaType, mfaType,
secret,
recoveryCodes,
dest,
countryCode,
...user, ...user,
}; };
setLoading(true); setLoading(true);

View File

@ -26,11 +26,13 @@ export const mfaSetup = "mfaSetup";
export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) { export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const onFinish = ({passcode}) => { const onFinish = ({passcode, countryCode, dest}) => {
const data = {passcode, mfaType: mfaProps.mfaType, ...user}; const data = {passcode, mfaType: mfaProps.mfaType, secret: mfaProps.secret, dest: dest, countryCode: countryCode, ...user};
MfaBackend.MfaSetupVerify(data) MfaBackend.MfaSetupVerify(data)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
res.dest = dest;
res.countryCode = countryCode;
onSuccess(res); onSuccess(res);
} else { } else {
onFail(res); onFail(res);

View File

@ -1,5 +1,5 @@
import {UserOutlined} from "@ant-design/icons"; import {UserOutlined} from "@ant-design/icons";
import {Button, Form, Input} from "antd"; import {Button, Form, Input, Space} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import React, {useEffect} from "react"; import React, {useEffect} from "react";
import {CountryCodeSelect} from "../../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../../common/select/CountryCodeSelect";
@ -19,11 +19,13 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
} }
if (mfaProps.mfaType === SmsMfaType) { if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone); setDest(user.phone);
form.setFieldValue("dest", user.phone);
return; return;
} }
if (mfaProps.mfaType === EmailMfaType) { if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email); setDest(user.email);
form.setFieldValue("dest", user.email);
} }
}, [mfaProps.mfaType]); }, [mfaProps.mfaType]);
@ -57,45 +59,44 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
<div style={{marginBottom: 20, textAlign: "left", gap: 8}}> <div style={{marginBottom: 20, textAlign: "left", gap: 8}}>
{isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest} {isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> : </div> :
(<React.Fragment> (
<p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") : <p>{isEmail() ? i18next.t("mfa:Please bind your email first, the system will automatically uses the mail for multi-factor authentication") :
i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")} i18next.t("mfa:Please bind your phone first, the system automatically uses the phone for multi-factor authentication")}
</p> </p>
<Input.Group compact style={{width: "300Px", marginBottom: "30px"}}>
{isEmail() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Input.Group>
</React.Fragment>
) )
} }
<Space.Compact style={{width: "300Px", marginBottom: "30px", display: isShowText() ? "none" : ""}}>
{isEmail() || isShowText() ? null :
<Form.Item
name="countryCode"
noStyle
rules={[
{
required: false,
message: i18next.t("signup:Please select your country code!"),
},
]}
>
<CountryCodeSelect
initValue={mfaProps.countryCode}
style={{width: "30%"}}
countryCodes={application.organizationObj.countryCodes}
/>
</Form.Item>
}
<Form.Item
name="dest"
noStyle
rules={[{required: true, message: i18next.t("login:Please input your Email or Phone!")}]}
>
<Input
style={{width: isEmail() ? "100% " : "70%"}}
onChange={(e) => {setDest(e.target.value);}}
prefix={<UserOutlined />}
placeholder={isEmail() ? i18next.t("general:Email") : i18next.t("general:Phone")}
/>
</Form.Item>
</Space.Compact>
<Form.Item <Form.Item
name="passcode" name="passcode"
rules={[{required: true, message: i18next.t("login:Please input your code!")}]} rules={[{required: true, message: i18next.t("login:Please input your code!")}]}

View File

@ -32,6 +32,9 @@ export function MfaSetupVerify(values) {
formData.append("name", values.name); formData.append("name", values.name);
formData.append("mfaType", values.mfaType); formData.append("mfaType", values.mfaType);
formData.append("passcode", values.passcode); formData.append("passcode", values.passcode);
formData.append("secret", values.secret);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/verify`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
@ -44,6 +47,10 @@ export function MfaSetupEnable(values) {
formData.append("mfaType", values.mfaType); formData.append("mfaType", values.mfaType);
formData.append("owner", values.owner); formData.append("owner", values.owner);
formData.append("name", values.name); formData.append("name", values.name);
formData.append("secret", values.secret);
formData.append("recoveryCodes", values.recoveryCodes);
formData.append("dest", values.dest);
formData.append("countryCode", values.countryCode);
return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, { return fetch(`${Setting.ServerUrl}/api/mfa/setup/enable`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",