- Implement strict password requirements: - Minimum 8 characters - At least one uppercase letter - At least one lowercase letter - At least one number - At least one special character - Add custom validator for Echo integration - Add GetPasswordRequirements helper function - Add ValidatePassword function for manual validation
122 lines
2.8 KiB
Go
122 lines
2.8 KiB
Go
package middleware
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// LoginAttemptTracker tracks failed login attempts per IP and username
|
|
type LoginAttemptTracker struct {
|
|
mu sync.RWMutex
|
|
attempts map[string]*AttemptInfo
|
|
maxAttempts int
|
|
lockoutDuration time.Duration
|
|
cleanupInterval time.Duration
|
|
}
|
|
|
|
// AttemptInfo stores information about login attempts
|
|
type AttemptInfo struct {
|
|
AttemptCount int
|
|
LockedUntil time.Time
|
|
LastAttempt time.Time
|
|
}
|
|
|
|
// NewLoginAttemptTracker creates a new login attempt tracker
|
|
func NewLoginAttemptTracker(maxAttempts int, lockoutDuration, cleanupInterval time.Duration) *LoginAttemptTracker {
|
|
tracker := &LoginAttemptTracker{
|
|
attempts: make(map[string]*AttemptInfo),
|
|
maxAttempts: maxAttempts,
|
|
lockoutDuration: lockoutDuration,
|
|
cleanupInterval: cleanupInterval,
|
|
}
|
|
|
|
// Start cleanup goroutine
|
|
go tracker.cleanup()
|
|
|
|
return tracker
|
|
}
|
|
|
|
// RecordFailedAttempt records a failed login attempt
|
|
func (t *LoginAttemptTracker) RecordFailedAttempt(identifier string) (locked bool, remainingTime time.Duration) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
info, exists := t.attempts[identifier]
|
|
|
|
if !exists {
|
|
info = &AttemptInfo{
|
|
AttemptCount: 1,
|
|
LastAttempt: now,
|
|
}
|
|
t.attempts[identifier] = info
|
|
return false, 0
|
|
}
|
|
|
|
// Check if currently locked
|
|
if now.Before(info.LockedUntil) {
|
|
return true, time.Until(info.LockedUntil)
|
|
}
|
|
|
|
// Reset attempts if last attempt was more than lockoutDuration ago
|
|
if now.Sub(info.LastAttempt) > t.lockoutDuration {
|
|
info.AttemptCount = 1
|
|
} else {
|
|
info.AttemptCount++
|
|
}
|
|
|
|
info.LastAttempt = now
|
|
|
|
// Check if max attempts reached
|
|
if info.AttemptCount >= t.maxAttempts {
|
|
info.LockedUntil = now.Add(t.lockoutDuration)
|
|
return true, t.lockoutDuration
|
|
}
|
|
|
|
return false, 0
|
|
}
|
|
|
|
// IsLocked checks if an identifier is currently locked out
|
|
func (t *LoginAttemptTracker) IsLocked(identifier string) (locked bool, remainingTime time.Duration) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
info, exists := t.attempts[identifier]
|
|
if !exists {
|
|
return false, 0
|
|
}
|
|
|
|
now := time.Now()
|
|
if now.Before(info.LockedUntil) {
|
|
return true, time.Until(info.LockedUntil)
|
|
}
|
|
|
|
return false, 0
|
|
}
|
|
|
|
// ClearAttempts clears failed login attempts for an identifier
|
|
func (t *LoginAttemptTracker) ClearAttempts(identifier string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
delete(t.attempts, identifier)
|
|
}
|
|
|
|
// cleanup removes old attempt records periodically
|
|
func (t *LoginAttemptTracker) cleanup() {
|
|
ticker := time.NewTicker(t.cleanupInterval)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
t.mu.Lock()
|
|
now := time.Now()
|
|
for identifier, info := range t.attempts {
|
|
// Remove records that are not locked and haven't had recent attempts
|
|
if now.After(info.LockedUntil) && now.Sub(info.LastAttempt) > t.lockoutDuration*2 {
|
|
delete(t.attempts, identifier)
|
|
}
|
|
}
|
|
t.mu.Unlock()
|
|
}
|
|
}
|