feat: add login attempt rate limiting
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -15,6 +16,30 @@ import (
|
||||
|
||||
const sessionCookie = "am_session"
|
||||
|
||||
var loginLimiter = struct {
|
||||
sync.Mutex
|
||||
attempts map[string][]time.Time
|
||||
}{attempts: map[string][]time.Time{}}
|
||||
|
||||
func loginAllowed(key string) bool {
|
||||
now := time.Now()
|
||||
loginLimiter.Lock()
|
||||
defer loginLimiter.Unlock()
|
||||
items := loginLimiter.attempts[key]
|
||||
cutoff := now.Add(-15 * time.Minute)
|
||||
kept := items[:0]
|
||||
for _, t := range items {
|
||||
if t.After(cutoff) {
|
||||
kept = append(kept, t)
|
||||
}
|
||||
}
|
||||
if len(kept) >= 10 {
|
||||
return false
|
||||
}
|
||||
loginLimiter.attempts[key] = append(kept, now)
|
||||
return true
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
@@ -104,6 +129,11 @@ func (a *App) login(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "username and password required"})
|
||||
return
|
||||
}
|
||||
key := c.ClientIP() + ":" + strings.ToLower(strings.TrimSpace(in.Username))
|
||||
if !loginAllowed(key) {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many login attempts; try again later"})
|
||||
return
|
||||
}
|
||||
var user User
|
||||
if a.db.Where("username = ?", strings.TrimSpace(in.Username)).First(&user).Error != nil || user.Status != UserActive || checkPassword(user.PasswordHash, in.Password) != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
||||
@@ -120,6 +150,7 @@ func (a *App) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
http.SetCookie(c.Writer, &http.Cookie{Name: sessionCookie, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: c.Request.TLS != nil, Expires: session.ExpiresAt})
|
||||
user.PasswordHash = ""
|
||||
c.JSON(http.StatusOK, gin.H{"user": user})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user