Skip to content

Commit d3cda06

Browse files
authored
feat: add lockdown mode on multiple login attempts (#727)
* feat: add lockdown mode on multiple login attempts * fix: review comments * fix: fix typo
1 parent f26c217 commit d3cda06

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

internal/service/auth_service.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import (
2121
"golang.org/x/oauth2"
2222
)
2323

24+
// hard-defaults, may make configurable in the future if needed,
25+
// but for now these are just safety limits to prevent unbounded memory usage
2426
const MaxOAuthPendingSessions = 256
2527
const OAuthCleanupCount = 16
28+
const MaxLoginAttemptRecords = 256
2629

2730
type OAuthPendingSession struct {
2831
State string
@@ -43,6 +46,11 @@ type LoginAttempt struct {
4346
LockedUntil time.Time
4447
}
4548

49+
type Lockdown struct {
50+
Active bool
51+
ActiveUntil time.Time
52+
}
53+
4654
type AuthServiceConfig struct {
4755
Users []config.User
4856
OauthWhitelist []string
@@ -69,6 +77,7 @@ type AuthService struct {
6977
ldap *LdapService
7078
queries *repository.Queries
7179
oauthBroker *OAuthBrokerService
80+
lockdown *Lockdown
7281
}
7382

7483
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) {
202211
auth.loginMutex.RLock()
203212
defer auth.loginMutex.RUnlock()
204213

214+
if auth.lockdown != nil && auth.lockdown.Active {
215+
remaining := int(time.Until(auth.lockdown.ActiveUntil).Seconds())
216+
return true, remaining
217+
}
218+
205219
if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 {
206220
return false, 0
207221
}
@@ -227,6 +241,14 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
227241
auth.loginMutex.Lock()
228242
defer auth.loginMutex.Unlock()
229243

244+
if len(auth.loginAttempts) >= MaxLoginAttemptRecords {
245+
if auth.lockdown != nil && auth.lockdown.Active {
246+
return
247+
}
248+
go auth.lockdownMode()
249+
return
250+
}
251+
230252
attempt, exists := auth.loginAttempts[identifier]
231253
if !exists {
232254
attempt = &LoginAttempt{}
@@ -746,3 +768,31 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
746768
}
747769
}
748770
}
771+
772+
func (auth *AuthService) lockdownMode() {
773+
auth.loginMutex.Lock()
774+
775+
tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.")
776+
777+
auth.lockdown = &Lockdown{
778+
Active: true,
779+
ActiveUntil: time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second),
780+
}
781+
782+
// At this point all login attemps will also expire so,
783+
// we might as well clear them to free up memory
784+
auth.loginAttempts = make(map[string]*LoginAttempt)
785+
786+
timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil))
787+
defer timer.Stop()
788+
789+
auth.loginMutex.Unlock()
790+
791+
<-timer.C
792+
793+
auth.loginMutex.Lock()
794+
795+
tlog.App.Info().Msg("Lockdown period ended, resuming normal operation")
796+
auth.lockdown = nil
797+
auth.loginMutex.Unlock()
798+
}

0 commit comments

Comments
 (0)