Cap concurrent password checks and count attempts before checking

Every argon2 run takes 64 MiB and nothing limited how many ran at once,
so parallel sign-in attempts could run the server out of memory (8 at
once used about 600 MB). At most two now run at once; at most 16
sign-ins wait for one, more get HTTP 429. 30 parallel sign-ins peaked
at 275 MB.

A sign-in attempt now counts toward the lockout before its password is
checked, so parallel attempts cannot get past it; a right password
takes its own attempt back. IPv6 addresses are locked out by /64.
This commit is contained in:
Daniel Redetzke
2026-10-05 00:23:05 +03:00
parent a59a095691
commit b54ff1b002
4 changed files with 136 additions and 34 deletions
+19 -8
View File
@@ -236,23 +236,34 @@ func newMFAState() mfaState {
var errBadTicket = errors.New("the sign-in expired; enter your password again")
// failLocked counts a failed attempt from ip toward the lockout. a.mu must
// be held.
func (a *Auth) failLocked(ip string) {
f := a.fails[ip]
// failLocked counts a failed attempt from ip toward the lockout and returns
// a function that takes it back, for an attempt counted before it was
// checked. a.mu must be held, also when calling undo.
func (a *Auth) failLocked(ip string) (undo func()) {
key := lockKey(ip)
f := a.fails[key]
if f == nil {
f = &failState{}
a.fails[ip] = f
a.fails[key] = f
}
f.count++
if f.count >= maxFailures {
locked := f.count >= maxFailures
if locked {
f.count = 0
f.until = time.Now().Add(lockoutTime)
}
return func() {
switch {
case locked:
f.count, f.until = maxFailures-1, time.Time{}
case f.count > 0:
f.count--
}
}
}
func (a *Auth) lockedLocked(ip string) bool {
f := a.fails[ip]
f := a.fails[lockKey(ip)]
return f != nil && time.Now().Before(f.until)
}
@@ -302,7 +313,7 @@ func (a *Auth) finishSignIn(u *User, ip string) string {
cfg := a.store.Get()
a.mu.Lock()
defer a.mu.Unlock()
delete(a.fails, ip)
delete(a.fails, lockKey(ip))
a.logins[u.ID] = tokenUse{At: time.Now(), IP: ip}
return a.newSessionLocked(cfg, u, sessionInfo{Started: time.Now(), IP: ip})
}