mirror of
https://github.com/casdoor/casdoor.git
synced 2025-07-02 11:20:18 +08:00
feat: support stateless MFA setup (#3382)
This commit is contained in:
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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!")}]}
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user