// 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" "time" "github.com/beego/beego/context" "github.com/google/uuid" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" ) const ( MfaTotpSecretSession = "mfa_totp_secret" MfaTotpPeriodInSeconds = 30 ) 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" //} 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) if secret == nil { return errors.New("totp secret is missing") } result, err := totp.ValidateCustom(passcode, secret.(string), time.Now().UTC(), totp.ValidateOpts{ Period: MfaTotpPeriodInSeconds, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) if err != nil { return err } 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 } ctx.Input.CruSession.Delete(MfaRecoveryCodesSession) ctx.Input.CruSession.Delete(MfaTotpSecretSession) return nil } func (mfa *TotpMfa) Verify(passcode string) error { result, err := totp.ValidateCustom(passcode, mfa.Config.Secret, time.Now().UTC(), totp.ValidateOpts{ Period: MfaTotpPeriodInSeconds, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) if err != nil { return err } 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: MfaTotpPeriodInSeconds, secretSize: 20, digits: otp.DigitsSix, } }