feat: add login attempt rate limiting
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -15,6 +16,30 @@ import (
|
|||||||
|
|
||||||
const sessionCookie = "am_session"
|
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 {
|
func hashToken(token string) string {
|
||||||
sum := sha256.Sum256([]byte(token))
|
sum := sha256.Sum256([]byte(token))
|
||||||
return hex.EncodeToString(sum[:])
|
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"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username and password required"})
|
||||||
return
|
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
|
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 {
|
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"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
||||||
@@ -120,6 +150,7 @@ func (a *App) login(c *gin.Context) {
|
|||||||
return
|
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})
|
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})
|
c.JSON(http.StatusOK, gin.H{"user": user})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user