feat: add TOTP multi-factor authentication (#2014)

* feat: add totp multi-factor authentication

* feat: add license

* feat:i18n and update yarn.lock

* feat:i18n

* fix: i18n
This commit is contained in:
Yaodong Yu
2023-06-24 18:39:54 +08:00
committed by GitHub
parent d1e734e4ce
commit 0a8c2a35fe
23 changed files with 1961 additions and 1617 deletions

View File

@ -17,7 +17,6 @@ package controllers
import ( import (
"net/http" "net/http"
"github.com/beego/beego"
"github.com/casdoor/casdoor/object" "github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
) )
@ -58,10 +57,7 @@ func (c *ApiController) MfaSetupInitiate() {
return return
} }
issuer := beego.AppConfig.String("appname") mfaProps, err := MfaUtil.Initiate(c.Ctx, user.GetId())
accountName := user.GetId()
mfaProps, err := MfaUtil.Initiate(c.Ctx, issuer, accountName)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

4
go.mod
View File

@ -42,7 +42,7 @@ require (
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/nyaruka/phonenumbers v1.1.5 github.com/nyaruka/phonenumbers v1.1.5
github.com/pkoukk/tiktoken-go v0.1.1 github.com/pkoukk/tiktoken-go v0.1.1
github.com/plutov/paypal/v4 v4.7.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.11.1 github.com/prometheus/client_golang v1.11.1
github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model v0.2.0
github.com/qiangmzsx/string-adapter/v2 v2.1.0 github.com/qiangmzsx/string-adapter/v2 v2.1.0
@ -59,7 +59,7 @@ require (
github.com/tealeg/xlsx v1.0.5 github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13 // indirect github.com/xorm-io/builder v0.3.13
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6 github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect

7
go.sum
View File

@ -105,6 +105,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
@ -495,10 +497,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo=
github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw=
github.com/plutov/paypal/v4 v4.7.0 h1:6TRvYD4ny6yQfHaABeStNf43GFM1wpW5jU/XEDGQmq0=
github.com/plutov/paypal/v4 v4.7.0/go.mod h1:D56boafCRGcF/fEM0w282kj0fCDKIyrwOPX/Te1jCmw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@ -595,7 +597,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -22,6 +22,8 @@ import (
"github.com/beego/beego/context" "github.com/beego/beego/context"
) )
const MfaRecoveryCodesSession = "mfa_recovery_codes"
type MfaSessionData struct { type MfaSessionData struct {
UserId string UserId string
} }
@ -37,10 +39,10 @@ type MfaProps struct {
} }
type MfaInterface interface { type MfaInterface interface {
SetupVerify(ctx *context.Context, passCode string) error Initiate(ctx *context.Context, userId string) (*MfaProps, error)
Verify(passCode string) error SetupVerify(ctx *context.Context, passcode string) error
Initiate(ctx *context.Context, name1 string, name2 string) (*MfaProps, error)
Enable(ctx *context.Context, user *User) error Enable(ctx *context.Context, user *User) error
Verify(passcode string) error
} }
const ( const (
@ -58,11 +60,11 @@ const (
func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface { func GetMfaUtil(mfaType string, config *MfaProps) MfaInterface {
switch mfaType { switch mfaType {
case SmsType: case SmsType:
return NewSmsTwoFactor(config) return NewSmsMfaUtil(config)
case EmailType: case EmailType:
return NewEmailTwoFactor(config) return NewEmailMfaUtil(config)
case TotpType: case TotpType:
return nil return NewTotpMfaUtil(config)
} }
return nil return nil
@ -97,23 +99,9 @@ func MfaRecover(user *User, recoveryCode string) error {
func GetAllMfaProps(user *User, masked bool) []*MfaProps { func GetAllMfaProps(user *User, masked bool) []*MfaProps {
mfaProps := []*MfaProps{} mfaProps := []*MfaProps{}
if user.MfaPhoneEnabled { for _, mfaType := range []string{SmsType, EmailType, TotpType} {
mfaProps = append(mfaProps, user.GetMfaProps(SmsType, masked)) mfaProps = append(mfaProps, user.GetMfaProps(mfaType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: SmsType,
})
} }
if user.MfaEmailEnabled {
mfaProps = append(mfaProps, user.GetMfaProps(EmailType, masked))
} else {
mfaProps = append(mfaProps, &MfaProps{
Enabled: false,
MfaType: EmailType,
})
}
return mfaProps return mfaProps
} }
@ -121,6 +109,13 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps := &MfaProps{} mfaProps := &MfaProps{}
if mfaType == SmsType { if mfaType == SmsType {
if !user.MfaPhoneEnabled {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{ mfaProps = &MfaProps{
Enabled: user.MfaPhoneEnabled, Enabled: user.MfaPhoneEnabled,
MfaType: mfaType, MfaType: mfaType,
@ -132,6 +127,13 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps.Secret = user.Phone mfaProps.Secret = user.Phone
} }
} else if mfaType == EmailType { } else if mfaType == EmailType {
if !user.MfaEmailEnabled {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{ mfaProps = &MfaProps{
Enabled: user.MfaEmailEnabled, Enabled: user.MfaEmailEnabled,
MfaType: mfaType, MfaType: mfaType,
@ -142,9 +144,22 @@ func (user *User) GetMfaProps(mfaType string, masked bool) *MfaProps {
mfaProps.Secret = user.Email mfaProps.Secret = user.Email
} }
} else if mfaType == TotpType { } else if mfaType == TotpType {
if user.TotpSecret == "" {
return &MfaProps{
Enabled: false,
MfaType: mfaType,
}
}
mfaProps = &MfaProps{ mfaProps = &MfaProps{
Enabled: true,
MfaType: mfaType, MfaType: mfaType,
} }
if masked {
mfaProps.Secret = ""
} else {
mfaProps.Secret = user.TotpSecret
}
} }
if user.PreferredMfaType == mfaType { if user.PreferredMfaType == mfaType {
@ -158,8 +173,9 @@ func DisabledMultiFactorAuth(user *User) error {
user.RecoveryCodes = []string{} user.RecoveryCodes = []string{}
user.MfaPhoneEnabled = false user.MfaPhoneEnabled = false
user.MfaEmailEnabled = false user.MfaEmailEnabled = false
user.TotpSecret = ""
_, err := UpdateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled"}, user.IsAdminUser()) _, err := updateUser(user.GetId(), user, []string{"preferred_mfa_type", "recovery_codes", "mfa_phone_enabled", "mfa_email_enabled", "totp_secret"})
if err != nil { if err != nil {
return err return err
} }

View File

@ -18,26 +18,24 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/casdoor/casdoor/util"
"github.com/beego/beego/context" "github.com/beego/beego/context"
"github.com/casdoor/casdoor/util"
"github.com/google/uuid" "github.com/google/uuid"
) )
const ( const (
MfaSmsCountryCodeSession = "mfa_country_code" MfaSmsCountryCodeSession = "mfa_country_code"
MfaSmsDestSession = "mfa_dest" MfaSmsDestSession = "mfa_dest"
MfaSmsRecoveryCodesSession = "mfa_recovery_codes"
) )
type SmsMfa struct { type SmsMfa struct {
Config *MfaProps Config *MfaProps
} }
func (mfa *SmsMfa) Initiate(ctx *context.Context, name string, secret string) (*MfaProps, error) { func (mfa *SmsMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
recoveryCode := uuid.NewString() recoveryCode := uuid.NewString()
err := ctx.Input.CruSession.Set(MfaSmsRecoveryCodesSession, []string{recoveryCode}) err := ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -63,9 +61,9 @@ func (mfa *SmsMfa) SetupVerify(ctx *context.Context, passCode string) error {
} }
func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error { func (mfa *SmsMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaSmsRecoveryCodesSession).([]string) recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 { if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is empty") return fmt.Errorf("recovery codes is missing")
} }
columns := []string{"recovery_codes", "preferred_mfa_type"} columns := []string{"recovery_codes", "preferred_mfa_type"}
@ -111,7 +109,7 @@ func (mfa *SmsMfa) Verify(passCode string) error {
return nil return nil
} }
func NewSmsTwoFactor(config *MfaProps) *SmsMfa { func NewSmsMfaUtil(config *MfaProps) *SmsMfa {
if config == nil { if config == nil {
config = &MfaProps{ config = &MfaProps{
MfaType: SmsType, MfaType: SmsType,
@ -122,7 +120,7 @@ func NewSmsTwoFactor(config *MfaProps) *SmsMfa {
} }
} }
func NewEmailTwoFactor(config *MfaProps) *SmsMfa { func NewEmailMfaUtil(config *MfaProps) *SmsMfa {
if config == nil { if config == nil {
config = &MfaProps{ config = &MfaProps{
MfaType: EmailType, MfaType: EmailType,

133
object/mfa_totp.go Normal file
View File

@ -0,0 +1,133 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"errors"
"fmt"
"github.com/beego/beego"
"github.com/beego/beego/context"
"github.com/google/uuid"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
)
const MfaTotpSecretSession = "mfa_totp_secret"
type TotpMfa struct {
Config *MfaProps
period uint
secretSize uint
digits otp.Digits
}
func (mfa *TotpMfa) Initiate(ctx *context.Context, userId string) (*MfaProps, error) {
issuer := beego.AppConfig.String("appname")
if issuer == "" {
issuer = "casdoor"
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: issuer,
AccountName: userId,
Period: mfa.period,
SecretSize: mfa.secretSize,
Digits: mfa.digits,
})
if err != nil {
return nil, err
}
err = ctx.Input.CruSession.Set(MfaTotpSecretSession, key.Secret())
if err != nil {
return nil, err
}
recoveryCode := uuid.NewString()
err = ctx.Input.CruSession.Set(MfaRecoveryCodesSession, []string{recoveryCode})
if err != nil {
return nil, err
}
mfaProps := MfaProps{
MfaType: mfa.Config.MfaType,
RecoveryCodes: []string{recoveryCode},
Secret: key.Secret(),
URL: key.URL(),
}
return &mfaProps, nil
}
func (mfa *TotpMfa) SetupVerify(ctx *context.Context, passcode string) error {
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
result := totp.Validate(passcode, secret)
if result {
return nil
} else {
return errors.New("totp passcode error")
}
}
func (mfa *TotpMfa) Enable(ctx *context.Context, user *User) error {
recoveryCodes := ctx.Input.CruSession.Get(MfaRecoveryCodesSession).([]string)
if len(recoveryCodes) == 0 {
return fmt.Errorf("recovery codes is missing")
}
secret := ctx.Input.CruSession.Get(MfaTotpSecretSession).(string)
if secret == "" {
return fmt.Errorf("totp secret is missing")
}
columns := []string{"recovery_codes", "preferred_mfa_type", "totp_secret"}
user.RecoveryCodes = append(user.RecoveryCodes, recoveryCodes...)
user.TotpSecret = secret
if user.PreferredMfaType == "" {
user.PreferredMfaType = mfa.Config.MfaType
}
_, err := updateUser(user.GetId(), user, columns)
if err != nil {
return err
}
return nil
}
func (mfa *TotpMfa) Verify(passcode string) error {
result := totp.Validate(passcode, mfa.Config.Secret)
if result {
return nil
} else {
return errors.New("totp passcode error")
}
}
func NewTotpMfaUtil(config *MfaProps) *TotpMfa {
if config == nil {
config = &MfaProps{
MfaType: TotpType,
}
}
return &TotpMfa{
Config: config,
period: 30,
secretSize: 20,
digits: otp.DigitsSix,
}
}

View File

@ -161,6 +161,7 @@ type User struct {
WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"` WebauthnCredentials []webauthn.Credential `xorm:"webauthnCredentials blob" json:"webauthnCredentials"`
PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"` PreferredMfaType string `xorm:"varchar(100)" json:"preferredMfaType"`
RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"` RecoveryCodes []string `xorm:"varchar(1000)" json:"recoveryCodes,omitempty"`
TotpSecret string `xorm:"varchar(100)" json:"totpSecret,omitempty"`
MfaPhoneEnabled bool `json:"mfaPhoneEnabled"` MfaPhoneEnabled bool `json:"mfaPhoneEnabled"`
MfaEmailEnabled bool `json:"mfaEmailEnabled"` MfaEmailEnabled bool `json:"mfaEmailEnabled"`
MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"` MultiFactorAuths []*MfaProps `xorm:"-" json:"multiFactorAuths,omitempty"`
@ -432,16 +433,19 @@ func GetMaskedUser(user *User, errs ...error) (*User, error) {
if user.AccessSecret != "" { if user.AccessSecret != "" {
user.AccessSecret = "***" user.AccessSecret = "***"
} }
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
if user.ManagedAccounts != nil { if user.ManagedAccounts != nil {
for _, manageAccount := range user.ManagedAccounts { for _, manageAccount := range user.ManagedAccounts {
manageAccount.Password = "***" manageAccount.Password = "***"
} }
} }
if user.TotpSecret != "" {
user.TotpSecret = ""
}
if user.RecoveryCodes != nil {
user.RecoveryCodes = nil
}
return user, nil return user, nil
} }

View File

@ -24,8 +24,6 @@
"i18next": "^19.8.9", "i18next": "^19.8.9",
"libphonenumber-js": "^1.10.19", "libphonenumber-js": "^1.10.19",
"moment": "^2.29.1", "moment": "^2.29.1",
"qrcode.react": "^3.1.0",
"qs": "^6.10.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-app-polyfill": "^3.0.0", "react-app-polyfill": "^3.0.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",

View File

@ -16,8 +16,8 @@ import React, {useState} from "react";
import i18next from "i18next"; import i18next from "i18next";
import {Button, Input} from "antd"; import {Button, Input} from "antd";
import * as AuthBackend from "./AuthBackend"; import * as AuthBackend from "./AuthBackend";
import {EmailMfaType, SmsMfaType} from "./MfaSetupPage"; import {EmailMfaType, RecoveryMfaType, SmsMfaType} from "./MfaSetupPage";
import {MfaSmsVerifyForm, mfaAuth} from "./MfaVerifyForm"; import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaAuth} from "./MfaVerifyForm";
export const NextMfa = "NextMfa"; export const NextMfa = "NextMfa";
export const RequiredMfa = "RequiredMfa"; export const RequiredMfa = "RequiredMfa";
@ -60,7 +60,7 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
}); });
}; };
if (mfaType === SmsMfaType || mfaType === EmailMfaType) { if (mfaType !== RecoveryMfaType) {
return ( return (
<div style={{width: 300, height: 350}}> <div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>
@ -69,12 +69,18 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
<div style={{marginBottom: 24}}> <div style={{marginBottom: 24}}>
{i18next.t("mfa:Multi-factor authentication description")} {i18next.t("mfa:Multi-factor authentication description")}
</div> </div>
<MfaSmsVerifyForm {mfaType === SmsMfaType || mfaType === EmailMfaType ? (
mfaProps={mfaProps} <MfaSmsVerifyForm
method={mfaAuth} mfaProps={mfaProps}
onFinish={verify} method={mfaAuth}
application={application} onFinish={verify}
/> application={application}
/>) : (
<MfaTotpVerifyForm
mfaProps={mfaProps}
onFinish={verify}
/>
)}
<span style={{float: "right"}}> <span style={{float: "right"}}>
{i18next.t("mfa:Have problems?")} {i18next.t("mfa:Have problems?")}
<a onClick={() => { <a onClick={() => {
@ -85,7 +91,7 @@ export function MfaAuthVerifyForm({formValues, oAuthParams, mfaProps, applicatio
</span> </span>
</div> </div>
); );
} else if (mfaType === "recovery") { } else {
return ( return (
<div style={{width: 300, height: 350}}> <div style={{width: 300, height: 350}}>
<div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}> <div style={{marginBottom: 24, textAlign: "center", fontSize: "24px"}}>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React, {useEffect, useState} from "react"; import React, {useState} from "react";
import {Button, Col, Form, Input, Result, Row, Steps} from "antd"; import {Button, Col, Form, Input, Result, Row, Steps} from "antd";
import * as ApplicationBackend from "../backend/ApplicationBackend"; import * as ApplicationBackend from "../backend/ApplicationBackend";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
@ -26,6 +26,7 @@ import {MfaSmsVerifyForm, MfaTotpVerifyForm, mfaSetup} from "./MfaVerifyForm";
export const EmailMfaType = "email"; export const EmailMfaType = "email";
export const SmsMfaType = "sms"; export const SmsMfaType = "sms";
export const TotpMfaType = "app"; export const TotpMfaType = "app";
export const RecoveryMfaType = "recovery";
function CheckPasswordForm({user, onSuccess, onFail}) { function CheckPasswordForm({user, onSuccess, onFail}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -76,29 +77,11 @@ function CheckPasswordForm({user, onSuccess, onFail}) {
); );
} }
export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) { export function MfaVerifyForm({mfaProps, application, user, onSuccess, onFail}) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [mfaProps, setMfaProps] = useState({mfaType: mfaType});
useEffect(() => {
if (mfaType === SmsMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.phone,
countryCode: user.countryCode,
});
}
if (mfaType === EmailMfaType) {
setMfaProps({
mfaType: mfaType,
secret: user.email,
});
}
}, [mfaType]);
const onFinish = ({passcode}) => { const onFinish = ({passcode}) => {
const data = {passcode, mfaType: mfaType, ...user}; const data = {passcode, mfaType: mfaProps.mfaType, ...user};
MfaBackend.MfaSetupVerify(data) MfaBackend.MfaSetupVerify(data)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
@ -115,14 +98,14 @@ export function MfaVerifyForm({mfaType, application, user, onSuccess, onFail}) {
}); });
}; };
if (mfaType === null || mfaType === undefined || mfaProps.secret === undefined) { if (mfaProps === undefined || mfaProps === null) {
return <div></div>; return <div></div>;
} }
if (mfaType === SmsMfaType || mfaType === EmailMfaType) { if (mfaProps.mfaType === SmsMfaType || mfaProps.mfaType === EmailMfaType) {
return <MfaSmsVerifyForm onFinish={onFinish} application={application} method={mfaSetup} mfaProps={mfaProps} />; return <MfaSmsVerifyForm mfaProps={mfaProps} onFinish={onFinish} application={application} method={mfaSetup} user={user} />;
} else if (mfaType === TotpMfaType) { } else if (mfaProps.mfaType === TotpMfaType) {
return <MfaTotpVerifyForm onFinish={onFinish} />; return <MfaTotpVerifyForm mfaProps={mfaProps} onFinish={onFinish} />;
} else { } else {
return <div></div>; return <div></div>;
} }
@ -183,7 +166,7 @@ class MfaSetupPage extends React.Component {
} }
componentDidUpdate(prevProps, prevState, snapshot) { componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.isAuthenticated === true && this.state.mfaProps === null) { if (this.state.isAuthenticated === true && (this.state.mfaProps === null || this.state.mfaType !== prevState.mfaType)) {
MfaBackend.MfaSetupInitiate({ MfaBackend.MfaSetupInitiate({
mfaType: this.state.mfaType, mfaType: this.state.mfaType,
...this.getUser(), ...this.getUser(),
@ -226,18 +209,20 @@ class MfaSetupPage extends React.Component {
renderStep() { renderStep() {
switch (this.state.current) { switch (this.state.current) {
case 0: case 0:
return <CheckPasswordForm return (
user={this.getUser()} <CheckPasswordForm
onSuccess={() => { user={this.getUser()}
this.setState({ onSuccess={() => {
current: this.state.current + 1, this.setState({
isAuthenticated: true, current: this.state.current + 1,
}); isAuthenticated: true,
}} });
onFail={(res) => { }}
Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA")); onFail={(res) => {
}} Setting.showMessage("error", i18next.t("mfa:Failed to initiate MFA"));
/>; }}
/>
);
case 1: case 1:
if (!this.state.isAuthenticated) { if (!this.state.isAuthenticated) {
return null; return null;
@ -246,7 +231,7 @@ class MfaSetupPage extends React.Component {
return ( return (
<div> <div>
<MfaVerifyForm <MfaVerifyForm
mfaType={this.state.mfaType} mfaProps={this.state.mfaProps}
application={this.state.application} application={this.state.application}
user={this.props.account} user={this.props.account}
onSuccess={() => { onSuccess={() => {
@ -261,11 +246,6 @@ class MfaSetupPage extends React.Component {
<Col span={24} style={{display: "flex", justifyContent: "left"}}> <Col span={24} style={{display: "flex", justifyContent: "left"}}>
{(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null : {(this.state.mfaType === EmailMfaType || this.props.account.mfaEmailEnabled) ? null :
<Button type={"link"} onClick={() => { <Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${EmailMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${EmailMfaType}`);
}
this.setState({ this.setState({
mfaType: EmailMfaType, mfaType: EmailMfaType,
}); });
@ -275,17 +255,21 @@ class MfaSetupPage extends React.Component {
{ {
(this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null : (this.state.mfaType === SmsMfaType || this.props.account.mfaPhoneEnabled) ? null :
<Button type={"link"} onClick={() => { <Button type={"link"} onClick={() => {
if (this.state.isPromptPage) {
this.props.history.push(`/prompt/${this.state.application.name}?promptType=mfa&mfaType=${SmsMfaType}`);
} else {
this.props.history.push(`/mfa-authentication/setup?mfaType=${SmsMfaType}`);
}
this.setState({ this.setState({
mfaType: SmsMfaType, mfaType: SmsMfaType,
}); });
} }
}>{i18next.t("mfa:Use SMS")}</Button> }>{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>
}
</Col> </Col>
</div> </div>
); );
@ -294,18 +278,20 @@ class MfaSetupPage extends React.Component {
return null; return null;
} }
return <EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes} return (
onSuccess={() => { <EnableMfaForm user={this.getUser()} mfaType={this.state.mfaType} recoveryCodes={this.state.mfaProps.recoveryCodes}
Setting.showMessage("success", i18next.t("general:Enabled successfully")); onSuccess={() => {
if (this.state.isPromptPage && this.state.redirectUri) { Setting.showMessage("success", i18next.t("general:Enabled successfully"));
Setting.goToLink(this.state.redirectUri); if (this.state.isPromptPage && this.state.redirectUri) {
} else { Setting.goToLink(this.state.redirectUri);
Setting.goToLink("/account"); } else {
} Setting.goToLink("/account");
}} }
onFail={(res) => { }}
Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`); onFail={(res) => {
}} />; Setting.showMessage("error", `${i18next.t("general:Failed to enable")}: ${res.msg}`);
}} />
);
default: default:
return null; return null;
} }
@ -328,24 +314,20 @@ class MfaSetupPage extends React.Component {
<Col span={24} style={{justifyContent: "center"}}> <Col span={24} style={{justifyContent: "center"}}>
<Row> <Row>
<Col span={24}> <Col span={24}>
<div style={{textAlign: "center", fontSize: "28px"}}> <p style={{textAlign: "center", fontSize: "28px"}}>
{i18next.t("mfa:Protect your account with Multi-factor authentication")}</div> {i18next.t("mfa:Protect your account with Multi-factor authentication")}</p>
<div style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</div> <p style={{textAlign: "center", fontSize: "16px", marginTop: "10px"}}>{i18next.t("mfa:Each time you sign in to your Account, you'll need your password and a authentication code")}</p>
</Col>
</Row>
<Row>
<Col span={24}>
<Steps current={this.state.current}
items={[
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "80px",
}} >
</Steps>
</Col> </Col>
</Row> </Row>
<Steps current={this.state.current}
items={[
{title: i18next.t("mfa:Verify Password"), icon: <UserOutlined />},
{title: i18next.t("mfa:Verify Code"), icon: <KeyOutlined />},
{title: i18next.t("general:Enable"), icon: <CheckOutlined />},
]}
style={{width: "90%", maxWidth: "500px", margin: "auto", marginTop: "50px",
}} >
</Steps>
</Col> </Col>
<Col span={24} style={{display: "flex", justifyContent: "center"}}> <Col span={24} style={{display: "flex", justifyContent: "center"}}>
<div style={{marginTop: "10px", textAlign: "center"}}> <div style={{marginTop: "10px", textAlign: "center"}}>

View File

@ -12,23 +12,33 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {Button, Col, Form, Input, Row} from "antd"; import {Button, Col, Form, Input, QRCode, Space} from "antd";
import i18next from "i18next"; import i18next from "i18next";
import {CopyOutlined, UserOutlined} from "@ant-design/icons"; import {CopyOutlined, UserOutlined} from "@ant-design/icons";
import {SendCodeInput} from "../common/SendCodeInput"; import {SendCodeInput} from "../common/SendCodeInput";
import * as Setting from "../Setting"; import * as Setting from "../Setting";
import React from "react"; import React, {useEffect} from "react";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import {EmailMfaType} from "./MfaSetupPage"; import {EmailMfaType, SmsMfaType} from "./MfaSetupPage";
export const mfaAuth = "mfaAuth"; export const mfaAuth = "mfaAuth";
export const mfaSetup = "mfaSetup"; export const mfaSetup = "mfaSetup";
export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => { export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method, user}) => {
const [dest, setDest] = React.useState(mfaProps.secret ?? ""); const [dest, setDest] = React.useState(mfaProps.secret ?? "");
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => {
if (mfaProps.mfaType === SmsMfaType) {
setDest(user.phone);
}
if (mfaProps.mfaType === EmailMfaType) {
setDest(user.email);
}
}, [mfaProps.mfaType]);
const isEmail = () => { const isEmail = () => {
return mfaProps.mfaType === EmailMfaType; return mfaProps.mfaType === EmailMfaType;
}; };
@ -42,9 +52,9 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
countryCode: mfaProps.countryCode, countryCode: mfaProps.countryCode,
}} }}
> >
{mfaProps.secret !== "" ? {dest !== "" ?
<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")} {mfaProps.secret} {isEmail() ? i18next.t("mfa:Your email is") : i18next.t("mfa:Your phone is")} {dest}
</div> : </div> :
(<React.Fragment> (<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") :
@ -114,44 +124,49 @@ export const MfaSmsVerifyForm = ({mfaProps, application, onFinish, method}) => {
export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => { export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const renderSecret = () => {
if (!mfaProps.secret) {
return null;
}
return (
<React.Fragment>
<Col span={24} style={{display: "flex", justifyContent: "center"}}>
<QRCode
errorLevel="H"
value={mfaProps.url}
icon={"https://cdn.casdoor.com/static/favicon.png"}
/>
</Col>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Scan the QR code with your Authenticator App")}</p>
<p style={{textAlign: "center"}}>{i18next.t("mfa:Or copy the secret to your Authenticator App")}</p>
<Col span={24}>
<Space>
<Input value={mfaProps.secret} />
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Space>
</Col>
</React.Fragment>
);
};
return ( return (
<Form <Form
form={form} form={form}
style={{width: "300px"}} style={{width: "300px"}}
onFinish={onFinish} onFinish={onFinish}
> >
<Row type="flex" justify="center" align="middle"> {renderSecret()}
<Col>
</Col>
</Row>
<Row type="flex" justify="center" align="middle">
<Col>
{Setting.getLabel(
i18next.t("mfa:Multi-factor secret"),
i18next.t("mfa:Multi-factor secret - Tooltip")
)}
:
</Col>
<Col>
<Input value={mfaProps.secret} />
</Col>
<Col>
<Button
type="primary"
shape="round"
icon={<CopyOutlined />}
onClick={() => {
copy(`${mfaProps.secret}`);
Setting.showMessage(
"success",
i18next.t("mfa:Multi-factor secret to clipboard successfully")
);
}}
/>
</Col>
</Row>
<Form.Item <Form.Item
name="passcode" name="passcode"
rules={[{required: true, message: "Please input your passcode"}]} rules={[{required: true, message: "Please input your passcode"}]}
@ -162,7 +177,6 @@ export const MfaTotpVerifyForm = ({mfaProps, onFinish}) => {
placeholder={i18next.t("mfa:Passcode")} placeholder={i18next.t("mfa:Passcode")}
/> />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Button <Button
style={{marginTop: 24}} style={{marginTop: 24}}

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "If you are unable to access your device, enter your recovery code to verify your identity", "Multi-factor recover description": "If you are unable to access your device, enter your recovery code to verify your identity",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Métodos de vários fatores", "Multi-factor methods": "Métodos de vários fatores",
"Multi-factor recover": "Recuperação de vários fatores", "Multi-factor recover": "Recuperação de vários fatores",
"Multi-factor recover description": "Se você não conseguir acessar seu dispositivo, insira seu código de recuperação para verificar sua identidade", "Multi-factor recover description": "Se você não conseguir acessar seu dispositivo, insira seu código de recuperação para verificar sua identidade",
"Multi-factor secret": "Segredo de vários fatores",
"Multi-factor secret - Tooltip": "Segredo de vários fatores - Dica de ferramenta",
"Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso", "Multi-factor secret to clipboard successfully": "Segredo de vários fatores copiado para a área de transferência com sucesso",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Código de acesso", "Passcode": "Código de acesso",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Guarde este código de recuperação. Quando o seu dispositivo não puder fornecer um código de autenticação, você poderá redefinir a autenticação mfa usando este código de recuperação",
"Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores", "Protect your account with Multi-factor authentication": "Proteja sua conta com autenticação de vários fatores",
"Recovery code": "Código de recuperação", "Recovery code": "Código de recuperação",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Definir preferido", "Set preferred": "Definir preferido",
"Setup": "Configuração", "Setup": "Configuração",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Usar código de verificação SMS", "Use SMS verification code": "Usar código de verificação SMS",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "Multi-factor methods", "Multi-factor methods": "Multi-factor methods",
"Multi-factor recover": "Multi-factor recover", "Multi-factor recover": "Multi-factor recover",
"Multi-factor recover description": "Multi-factor recover description", "Multi-factor recover description": "Multi-factor recover description",
"Multi-factor secret": "Multi-factor secret",
"Multi-factor secret - Tooltip": "Multi-factor secret - Tooltip",
"Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully", "Multi-factor secret to clipboard successfully": "Multi-factor secret to clipboard successfully",
"Or copy the secret to your Authenticator App": "Or copy the secret to your Authenticator App",
"Passcode": "Passcode", "Passcode": "Passcode",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "Please bind your email first, the system will automatically uses the mail for multi-factor authentication",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "Please bind your phone first, the system automatically uses the phone for multi-factor authentication",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code",
"Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication", "Protect your account with Multi-factor authentication": "Protect your account with Multi-factor authentication",
"Recovery code": "Recovery code", "Recovery code": "Recovery code",
"Scan the QR code with your Authenticator App": "Scan the QR code with your Authenticator App",
"Set preferred": "Set preferred", "Set preferred": "Set preferred",
"Setup": "Setup", "Setup": "Setup",
"Use Authenticator App": "Use Authenticator App",
"Use Email": "Use Email", "Use Email": "Use Email",
"Use SMS": "Use SMS", "Use SMS": "Use SMS",
"Use SMS verification code": "Use SMS verification code", "Use SMS verification code": "Use SMS verification code",

View File

@ -434,17 +434,18 @@
"Multi-factor methods": "多因素认证方式", "Multi-factor methods": "多因素认证方式",
"Multi-factor recover": "重置多因素认证", "Multi-factor recover": "重置多因素认证",
"Multi-factor recover description": "如果您无法访问您的设备,输入您的多因素认证恢复代码来确认您的身份", "Multi-factor recover description": "如果您无法访问您的设备,输入您的多因素认证恢复代码来确认您的身份",
"Multi-factor secret": "多因素密钥",
"Multi-factor secret - Tooltip": "多因素密钥 - Tooltip",
"Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板", "Multi-factor secret to clipboard successfully": "多因素密钥已复制到剪贴板",
"Or copy the secret to your Authenticator App": "或者将这个密钥复制到你的身份验证应用中",
"Passcode": "认证码", "Passcode": "认证码",
"Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "请先绑定邮箱,之后会自动使用该邮箱作为多因素认证的方式", "Please bind your email first, the system will automatically uses the mail for multi-factor authentication": "请先绑定邮箱,之后会自动使用该邮箱作为多因素认证的方式",
"Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "请先绑定手机号,之后会自动使用该手机号作为多因素认证的方式", "Please bind your phone first, the system automatically uses the phone for multi-factor authentication": "请先绑定手机号,之后会自动使用该手机号作为多因素认证的方式",
"Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证", "Please save this recovery code. Once your device cannot provide an authentication code, you can reset mfa authentication by this recovery code": "请保存此恢复代码。一旦您的设备无法提供身份验证码,您可以通过此恢复码重置多因素认证",
"Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户", "Protect your account with Multi-factor authentication": "通过多因素认证保护您的帐户",
"Recovery code": "恢复码", "Recovery code": "恢复码",
"Scan the QR code with your Authenticator App": "用你的身份验证应用扫描二维码",
"Set preferred": "设为首选", "Set preferred": "设为首选",
"Setup": "设置", "Setup": "设置",
"Use Authenticator App": "使用身份验证应用",
"Use Email": "使用电子邮件", "Use Email": "使用电子邮件",
"Use SMS": "使用短信", "Use SMS": "使用短信",
"Use SMS verification code": "使用手机或电子邮件发送验证码认证", "Use SMS verification code": "使用手机或电子邮件发送验证码认证",

File diff suppressed because it is too large Load Diff