Compare commits

...

3 Commits

Author SHA1 Message Date
Stavros
eb86fff0e4 fix: fix typo 2026-03-23 16:15:40 +02:00
Stavros
6b0d804ba3 fix: review comments 2026-03-22 23:06:23 +02:00
Stavros
f9b1aeb23e feat: add lockdown mode on multiple login attempts 2026-03-22 22:59:41 +02:00

View File

@@ -21,8 +21,11 @@ import (
"golang.org/x/oauth2" "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 MaxOAuthPendingSessions = 256
const OAuthCleanupCount = 16 const OAuthCleanupCount = 16
const MaxLoginAttemptRecords = 256
type OAuthPendingSession struct { type OAuthPendingSession struct {
State string State string
@@ -43,6 +46,11 @@ type LoginAttempt struct {
LockedUntil time.Time LockedUntil time.Time
} }
type Lockdown struct {
Active bool
ActiveUntil time.Time
}
type AuthServiceConfig struct { type AuthServiceConfig struct {
Users []config.User Users []config.User
OauthWhitelist []string OauthWhitelist []string
@@ -69,6 +77,7 @@ type AuthService struct {
ldap *LdapService ldap *LdapService
queries *repository.Queries queries *repository.Queries
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
lockdown *Lockdown
} }
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService { 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() auth.loginMutex.RLock()
defer auth.loginMutex.RUnlock() 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 { if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 {
return false, 0 return false, 0
} }
@@ -227,6 +241,14 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
auth.loginMutex.Lock() auth.loginMutex.Lock()
defer auth.loginMutex.Unlock() defer auth.loginMutex.Unlock()
if len(auth.loginAttempts) >= MaxLoginAttemptRecords {
if auth.lockdown != nil && auth.lockdown.Active {
return
}
go auth.lockdownMode()
return
}
attempt, exists := auth.loginAttempts[identifier] attempt, exists := auth.loginAttempts[identifier]
if !exists { if !exists {
attempt = &LoginAttempt{} attempt = &LoginAttempt{}
@@ -746,3 +768,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)
timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil))
defer timer.Stop()
auth.loginMutex.Unlock()
<-timer.C
auth.loginMutex.Lock()
tlog.App.Info().Msg("Lockdown period ended, resuming normal operation")
auth.lockdown = nil
auth.loginMutex.Unlock()
}