Files
casdoor/object/verification_ip.go
2026-03-26 23:02:11 +08:00

153 lines
4.3 KiB
Go

// Copyright 2026 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"
"strconv"
"sync"
"time"
"github.com/casdoor/casdoor/i18n"
)
// Hard-coded thresholds for OTP / verification-code brute force protection (per IP + dest).
// These can be made configurable later if needed.
const (
defaultVerifyCodeIpLimit = 5
defaultVerifyCodeIpFrozenMinute = 10
)
var (
verifyCodeIpErrorMap = map[string]*verifyCodeErrorInfo{}
verifyCodeIpErrorMapLock sync.Mutex
)
func getVerifyCodeIpErrorKey(remoteAddr, dest string) string {
return fmt.Sprintf("%s:%s", remoteAddr, dest)
}
func checkVerifyCodeIpErrorTimes(remoteAddr, dest, lang string) error {
if remoteAddr == "" {
return nil
}
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
verifyCodeIpErrorMapLock.Lock()
defer verifyCodeIpErrorMapLock.Unlock()
errorInfo, ok := verifyCodeIpErrorMap[key]
if !ok || errorInfo == nil {
return nil
}
if errorInfo.wrongTimes < defaultVerifyCodeIpLimit {
return nil
}
minutesLeft := int64(defaultVerifyCodeIpFrozenMinute) - int64(time.Now().UTC().Sub(errorInfo.lastWrongTime).Minutes())
if minutesLeft > 0 {
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), minutesLeft)
}
delete(verifyCodeIpErrorMap, key)
return nil
}
func recordVerifyCodeIpErrorInfo(remoteAddr, dest, lang string) error {
// If remoteAddr is missing, still return a normal "wrong code" error.
if remoteAddr == "" {
return errors.New(i18n.Translate(lang, "verification:Wrong verification code!"))
}
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
verifyCodeIpErrorMapLock.Lock()
defer verifyCodeIpErrorMapLock.Unlock()
errorInfo, ok := verifyCodeIpErrorMap[key]
if !ok || errorInfo == nil {
errorInfo = &verifyCodeErrorInfo{}
verifyCodeIpErrorMap[key] = errorInfo
}
if errorInfo.wrongTimes < defaultVerifyCodeIpLimit {
errorInfo.wrongTimes++
}
if errorInfo.wrongTimes >= defaultVerifyCodeIpLimit {
errorInfo.lastWrongTime = time.Now().UTC()
}
leftChances := defaultVerifyCodeIpLimit - errorInfo.wrongTimes
if leftChances >= 0 {
return fmt.Errorf(i18n.Translate(lang, "check:password or code is incorrect, you have %s remaining chances"), strconv.Itoa(leftChances))
}
return fmt.Errorf(i18n.Translate(lang, "check:You have entered the wrong password or code too many times, please wait for %d minutes and try again"), defaultVerifyCodeIpFrozenMinute)
}
func resetVerifyCodeIpErrorTimes(remoteAddr, dest string) {
if remoteAddr == "" {
return
}
key := getVerifyCodeIpErrorKey(remoteAddr, dest)
verifyCodeIpErrorMapLock.Lock()
defer verifyCodeIpErrorMapLock.Unlock()
delete(verifyCodeIpErrorMap, key)
}
// CheckVerifyCodeWithLimitAndIp enforces both per-user and per-IP attempt limits for verification codes.
// It is intended for security-sensitive flows like password reset.
func CheckVerifyCodeWithLimitAndIp(user *User, remoteAddr, dest, code, lang string) error {
if err := checkVerifyCodeIpErrorTimes(remoteAddr, dest, lang); err != nil {
return err
}
if user != nil {
if err := checkVerifyCodeErrorTimes(user, dest, lang); err != nil {
return err
}
}
result, err := CheckVerificationCode(dest, code, lang)
if err != nil {
return err
}
switch result.Code {
case VerificationSuccess:
resetVerifyCodeIpErrorTimes(remoteAddr, dest)
if user != nil {
resetVerifyCodeErrorTimes(user, dest)
}
return nil
case wrongCodeError:
ipErr := recordVerifyCodeIpErrorInfo(remoteAddr, dest, lang)
if user != nil {
// Keep existing user-level error semantics when user is known.
return recordVerifyCodeErrorInfo(user, dest, lang)
}
return ipErr
default:
return errors.New(result.Msg)
}
}