diff --git a/controllers/auth.go b/controllers/auth.go index 0f3ceafe..1a78e54a 100644 --- a/controllers/auth.go +++ b/controllers/auth.go @@ -355,20 +355,27 @@ func isProxyProviderType(providerType string) bool { func checkMfaEnable(c *ApiController, user *object.User, organization *object.Organization, verificationType string) bool { if object.IsNeedPromptMfa(organization, user) { - // The prompt page needs the user to be srigned in + // The prompt page needs the user to be signed in c.SetSessionUsername(user.GetId()) c.ResponseOk(object.RequiredMfa) return true } if user.IsMfaEnabled() { + currentTime := util.String2Time(util.GetCurrentTime()) + mfaRememberDeadline := util.String2Time(user.MfaRememberDeadline) + if user.MfaRememberDeadline != "" && mfaRememberDeadline.After(currentTime) { + return false + } c.setMfaUserSession(user.GetId()) mfaList := object.GetAllMfaProps(user, true) mfaAllowList := []*object.MfaProps{} + mfaRememberInHours := organization.MfaRememberInHours for _, prop := range mfaList { if prop.MfaType == verificationType || !prop.Enabled { continue } + prop.MfaRememberInHours = mfaRememberInHours mfaAllowList = append(mfaAllowList, prop) } if len(mfaAllowList) >= 1 { @@ -973,6 +980,28 @@ func (c *ApiController) Login() { return } + var application *object.Application + if authForm.ClientId == "" { + application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application)) + } else { + application, err = object.GetApplicationByClientId(authForm.ClientId) + } + if err != nil { + c.ResponseError(err.Error()) + return + } + + if application == nil { + c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application)) + return + } + + var organization *object.Organization + organization, err = object.GetOrganization(util.GetId("admin", application.Organization)) + if err != nil { + c.ResponseError(c.T(err.Error())) + } + if authForm.Passcode != "" { if authForm.MfaType == c.GetSession("verificationCodeType") { c.ResponseError("Invalid multi-factor authentication type") @@ -999,6 +1028,17 @@ func (c *ApiController) Login() { } } + if authForm.EnableMfaRemember { + mfaRememberInSeconds := organization.MfaRememberInHours * 3600 + currentTime := util.String2Time(util.GetCurrentTime()) + duration := time.Duration(mfaRememberInSeconds) * time.Second + user.MfaRememberDeadline = util.Time2String(currentTime.Add(duration)) + _, err = object.UpdateUser(user.GetId(), user, []string{"mfa_remember_deadline"}, user.IsAdmin) + if err != nil { + c.ResponseError(err.Error()) + return + } + } c.SetSession("verificationCodeType", "") } else if authForm.RecoveryCode != "" { err = object.MfaRecover(user, authForm.RecoveryCode) @@ -1011,22 +1051,6 @@ func (c *ApiController) Login() { return } - var application *object.Application - if authForm.ClientId == "" { - application, err = object.GetApplication(fmt.Sprintf("admin/%s", authForm.Application)) - } else { - application, err = object.GetApplicationByClientId(authForm.ClientId) - } - if err != nil { - c.ResponseError(err.Error()) - return - } - - if application == nil { - c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), authForm.Application)) - return - } - resp = c.HandleLoggedIn(application, user, &authForm) c.setMfaUserSession("") diff --git a/controllers/mfa.go b/controllers/mfa.go index 4711b08b..358749a1 100644 --- a/controllers/mfa.go +++ b/controllers/mfa.go @@ -58,6 +58,12 @@ func (c *ApiController) MfaSetupInitiate() { return } + organization, err := object.GetOrganizationByUser(user) + if err != nil { + c.ResponseError(err.Error()) + return + } + mfaProps, err := MfaUtil.Initiate(user.GetId()) if err != nil { c.ResponseError(err.Error()) @@ -66,6 +72,7 @@ func (c *ApiController) MfaSetupInitiate() { recoveryCode := uuid.NewString() mfaProps.RecoveryCodes = []string{recoveryCode} + mfaProps.MfaRememberInHours = organization.MfaRememberInHours resp := mfaProps c.ResponseOk(resp) diff --git a/controllers/organization.go b/controllers/organization.go index 72dfd649..71589db1 100644 --- a/controllers/organization.go +++ b/controllers/organization.go @@ -98,6 +98,10 @@ func (c *ApiController) GetOrganization() { return } + if organization != nil && organization.MfaRememberInHours == 0 { + organization.MfaRememberInHours = 12 + } + c.ResponseOk(organization) } diff --git a/form/auth.go b/form/auth.go index ef72db43..9a430b2c 100644 --- a/form/auth.go +++ b/form/auth.go @@ -61,9 +61,10 @@ type AuthForm struct { CaptchaToken string `json:"captchaToken"` ClientSecret string `json:"clientSecret"` - MfaType string `json:"mfaType"` - Passcode string `json:"passcode"` - RecoveryCode string `json:"recoveryCode"` + MfaType string `json:"mfaType"` + Passcode string `json:"passcode"` + RecoveryCode string `json:"recoveryCode"` + EnableMfaRemember bool `json:"enableMfaRemember"` Plan string `json:"plan"` Pricing string `json:"pricing"` diff --git a/object/mfa.go b/object/mfa.go index fb721172..efccd044 100644 --- a/object/mfa.go +++ b/object/mfa.go @@ -21,13 +21,14 @@ import ( ) type MfaProps struct { - Enabled bool `json:"enabled"` - IsPreferred bool `json:"isPreferred"` - MfaType string `json:"mfaType" form:"mfaType"` - Secret string `json:"secret,omitempty"` - CountryCode string `json:"countryCode,omitempty"` - URL string `json:"url,omitempty"` - RecoveryCodes []string `json:"recoveryCodes,omitempty"` + Enabled bool `json:"enabled"` + IsPreferred bool `json:"isPreferred"` + MfaType string `json:"mfaType" form:"mfaType"` + Secret string `json:"secret,omitempty"` + CountryCode string `json:"countryCode,omitempty"` + URL string `json:"url,omitempty"` + RecoveryCodes []string `json:"recoveryCodes,omitempty"` + MfaRememberInHours int `json:"mfaRememberInHours"` } type MfaInterface interface { diff --git a/object/organization.go b/object/organization.go index 81c768f7..079b8fd1 100644 --- a/object/organization.go +++ b/object/organization.go @@ -84,8 +84,9 @@ type Organization struct { NavItems []string `xorm:"varchar(1000)" json:"navItems"` WidgetItems []string `xorm:"varchar(1000)" json:"widgetItems"` - MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` - AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` + MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` + MfaRememberInHours int `json:"mfaRememberInHours"` + AccountItems []*AccountItem `xorm:"varchar(5000)" json:"accountItems"` } func GetOrganizationCount(owner, name, field, value string) (int64, error) { diff --git a/object/user.go b/object/user.go index 91465c62..a6f7816e 100644 --- a/object/user.go +++ b/object/user.go @@ -210,11 +210,12 @@ type User struct { LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"` SigninWrongTimes int `json:"signinWrongTimes"` - ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` - MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` - MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` - NeedUpdatePassword bool `json:"needUpdatePassword"` - IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"` + ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"` + MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"` + MfaItems []*MfaItem `xorm:"varchar(300)" json:"mfaItems"` + MfaRememberDeadline string `xorm:"varchar(100)" json:"mfaRememberDeadline"` + NeedUpdatePassword bool `json:"needUpdatePassword"` + IpWhitelist string `xorm:"varchar(200)" json:"ipWhitelist"` } type Userinfo struct { @@ -792,7 +793,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er "eveonline", "fitbit", "gitea", "heroku", "influxcloud", "instagram", "intercom", "kakao", "lastfm", "mailru", "meetup", "microsoftonline", "naver", "nextcloud", "onedrive", "oura", "patreon", "paypal", "salesforce", "shopify", "soundcloud", "spotify", "strava", "stripe", "type", "tiktok", "tumblr", "twitch", "twitter", "typetalk", "uber", "vk", "wepay", "xero", "yahoo", - "yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", + "yammer", "yandex", "zoom", "custom", "need_update_password", "ip_whitelist", "mfa_items", "mfa_remember_deadline", } } if isAdmin { diff --git a/web/src/OrganizationEditPage.js b/web/src/OrganizationEditPage.js index 23f06f58..aa0cde05 100644 --- a/web/src/OrganizationEditPage.js +++ b/web/src/OrganizationEditPage.js @@ -603,6 +603,16 @@ class OrganizationEditPage extends React.Component { /> + + + {Setting.getLabel(i18next.t("application:MFA remember time"), i18next.t("application:MFA remember time - Tooltip"))} : + + + { + this.updateOrganizationField("mfaRememberInHours", value); + }} /> + + {Setting.getLabel(i18next.t("general:MFA items"), i18next.t("general:MFA items - Tooltip"))} : diff --git a/web/src/OrganizationListPage.js b/web/src/OrganizationListPage.js index 9b683870..1496fac2 100644 --- a/web/src/OrganizationListPage.js +++ b/web/src/OrganizationListPage.js @@ -25,6 +25,7 @@ import PopconfirmModal from "./common/modal/PopconfirmModal"; class OrganizationListPage extends BaseListPage { newOrganization() { const randomName = Setting.getRandomName(); + const DefaultMfaRememberInHours = 12; return { owner: "admin", // this.props.account.organizationname, name: `organization_${randomName}`, @@ -48,6 +49,7 @@ class OrganizationListPage extends BaseListPage { enableSoftDeletion: false, isProfilePublic: true, enableTour: true, + mfaRememberInHours: DefaultMfaRememberInHours, accountItems: [ {name: "Organization", visible: true, viewRule: "Public", modifyRule: "Admin"}, {name: "ID", visible: true, viewRule: "Public", modifyRule: "Immutable"}, diff --git a/web/src/auth/mfa/MfaAuthVerifyForm.js b/web/src/auth/mfa/MfaAuthVerifyForm.js index 494b3003..48ad7cee 100644 --- a/web/src/auth/mfa/MfaAuthVerifyForm.js +++ b/web/src/auth/mfa/MfaAuthVerifyForm.js @@ -31,9 +31,9 @@ export function MfaAuthVerifyForm({formValues, authParams, mfaProps, application const [mfaType, setMfaType] = useState(mfaProps.mfaType); const [recoveryCode, setRecoveryCode] = useState(""); - const verify = ({passcode}) => { + const verify = ({passcode, enableMfaRemember}) => { setLoading(true); - const values = {...formValues, passcode}; + const values = {...formValues, passcode, enableMfaRemember}; values["mfaType"] = mfaProps.mfaType; const loginFunction = formValues.type === "cas" ? AuthBackend.loginCas : AuthBackend.login; loginFunction(values, authParams).then((res) => { diff --git a/web/src/auth/mfa/MfaVerifySmsForm.js b/web/src/auth/mfa/MfaVerifySmsForm.js index d06e5bd0..474fad78 100644 --- a/web/src/auth/mfa/MfaVerifySmsForm.js +++ b/web/src/auth/mfa/MfaVerifySmsForm.js @@ -1,5 +1,5 @@ import {UserOutlined} from "@ant-design/icons"; -import {Button, Form, Input, Space} from "antd"; +import {Button, Checkbox, Form, Input, Space} from "antd"; import i18next from "i18next"; import React, {useEffect} from "react"; import {CountryCodeSelect} from "../../common/select/CountryCodeSelect"; @@ -12,6 +12,13 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user} const [dest, setDest] = React.useState(""); const [form] = Form.useForm(); + const handleFinish = (values) => { + onFinish({ + passcode: values.passcode, + enableMfaRemember: values.enableMfaRemember, + }); + }; + useEffect(() => { if (method === mfaAuth) { setDest(mfaProps.secret); @@ -51,9 +58,10 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user}
{isShowText() ? @@ -109,6 +117,14 @@ export const MfaVerifySmsForm = ({mfaProps, application, onFinish, method, user} application={application} /> + + + {i18next.t("mfa:Remember this account for {hour} hours").replace("{hour}", mfaProps?.mfaRememberInHours)} + +