@@ -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
2426const MaxOAuthPendingSessions = 256
2527const OAuthCleanupCount = 16
28+ const MaxLoginAttemptRecords = 256
2629
2730type 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+
4654type 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
7483func 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 {}
@@ -747,9 +769,37 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
747769 }
748770}
749771
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+ }
799+
750800// Function only used for testing - do not use in prod!
751801func (auth * AuthService ) ClearRateLimitsTestingOnly () {
752802 auth .loginMutex .Lock ()
753803 auth .loginAttempts = make (map [string ]* LoginAttempt )
754- auth .loginMutex .Unlock ()
804+ auth .loginMutex .Unlock ()
755805}
0 commit comments