diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 53c879d..c49b881 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -21,8 +21,11 @@ import ( "golang.org/x/oauth2" ) +// hard-defaults, may make configurable in the future if needed, +// but for now these are just safety limits to prevent unbounded memory usage const MaxOAuthPendingSessions = 256 const OAuthCleanupCount = 16 +const MaxLoginAttemptRecords = 5 type OAuthPendingSession struct { State string @@ -43,6 +46,11 @@ type LoginAttempt struct { LockedUntil time.Time } +type Lockdown struct { + Active bool + ActiveUntil time.Time +} + type AuthServiceConfig struct { Users []config.User OauthWhitelist []string @@ -69,6 +77,7 @@ type AuthService struct { ldap *LdapService queries *repository.Queries oauthBroker *OAuthBrokerService + lockdown *Lockdown } func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService { @@ -202,6 +211,11 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) { auth.loginMutex.RLock() defer auth.loginMutex.RUnlock() + if auth.lockdown != nil && auth.lockdown.Active { + remaining := int(time.Until(auth.lockdown.ActiveUntil).Seconds()) + return true, remaining + } + if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { return false, 0 } @@ -227,6 +241,11 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { auth.loginMutex.Lock() defer auth.loginMutex.Unlock() + if len(auth.loginAttempts) >= MaxLoginAttemptRecords { + go auth.lockdownMode() + return + } + attempt, exists := auth.loginAttempts[identifier] if !exists { attempt = &LoginAttempt{} @@ -746,3 +765,31 @@ func (auth *AuthService) ensureOAuthSessionLimit() { } } } + +func (auth *AuthService) lockdownMode() { + auth.loginMutex.Lock() + + tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.") + + auth.lockdown = &Lockdown{ + Active: true, + ActiveUntil: time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second), + } + + // At this point all login attemps will also expire so, + // we might as well clear them to free up memory + auth.loginAttempts = make(map[string]*LoginAttempt) + + auth.loginMutex.Unlock() + + timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil)) + defer timer.Stop() + + <-timer.C + + auth.loginMutex.Lock() + + tlog.App.Info().Msg("Lockdown period ended, resuming normal operation") + auth.lockdown = nil + auth.loginMutex.Unlock() +}