implement account lockout after 3 failed login attempts with 5-minute cooldown period
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package auth
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
@@ -10,3 +13,11 @@ var (
|
||||
ErrCredentialsEmpty = errors.New("username and password cannot be empty")
|
||||
ErrPasswordTooShort = errors.New("password must be at least 6 characters long")
|
||||
)
|
||||
|
||||
type AccountLockedError struct {
|
||||
LockedUntil time.Time
|
||||
}
|
||||
|
||||
func (e AccountLockedError) Error() string {
|
||||
return "account temporarily locked"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -35,6 +36,16 @@ func (h *Handler) Login(c *gin.Context) {
|
||||
|
||||
token, err := h.authService.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
var lockErr AccountLockedError
|
||||
if errors.As(err, &lockErr) {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "Слишком много неверных попыток. Попробуйте через 5 минут",
|
||||
"code": "account_temporarily_locked",
|
||||
"locked_until": lockErr.LockedUntil.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
switch err {
|
||||
case ErrUserNotFound, ErrInvalidCredentials:
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||
|
||||
@@ -71,10 +71,33 @@ func (s *Service) Login(ctx context.Context, username, password string) (string,
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if user.LockedUntil.Valid {
|
||||
if user.LockedUntil.Time.After(now) {
|
||||
return "", AccountLockedError{LockedUntil: user.LockedUntil.Time}
|
||||
}
|
||||
if err := s.queries.ResetLoginFailures(ctx, username); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if !s.VerifyPassword(user.PasswordHash, password) {
|
||||
failedLogin, err := s.queries.RecordFailedLogin(ctx, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if failedLogin.LockedUntil.Valid && failedLogin.LockedUntil.Time.After(now) {
|
||||
return "", AccountLockedError{LockedUntil: failedLogin.LockedUntil.Time}
|
||||
}
|
||||
return "", ErrInvalidCredentials
|
||||
}
|
||||
|
||||
if user.FailedLoginAttempts > 0 || user.LockedUntil.Valid {
|
||||
if err := s.queries.ResetLoginFailures(ctx, username); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
token, err := GenerateToken(uuid.UUID(user.ID.Bytes), s.secret, s.expiry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
Reference in New Issue
Block a user