MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWebmailLoginAccountLockout confirms repeated wrong-password attempts against
|
||||
// one account eventually get refused with a generic lockout message rather than
|
||||
// checking the password at all, and that the lockout doesn't touch a different
|
||||
// account from the same IP (the per-IP throttle, not this per-account layer, would
|
||||
// apply there).
|
||||
func TestWebmailLoginAccountLockout(t *testing.T) {
|
||||
app := newTestApp(t)
|
||||
app.Cfg.Section("Auth").Key("login_attempt_limit").SetValue("3")
|
||||
mux := app.Mux()
|
||||
domains, _ := app.DB.ListDomains()
|
||||
victimID := createTestMailboxWithPassword(t, app, "lockout-victim@example.com", domains[0].ID, "the-real-password-1!")
|
||||
otherID := createTestMailboxWithPassword(t, app, "lockout-other@example.com", domains[0].ID, "another-password-1!")
|
||||
|
||||
attempt := func(email, password string) *httptest.ResponseRecorder {
|
||||
form := url.Values{"email": {email}, "password": {password}}
|
||||
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.RemoteAddr = "203.0.113.9:12345"
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
rec := attempt("lockout-victim@example.com", "wrong password")
|
||||
if !strings.Contains(rec.Body.String(), "Incorrect email or password") {
|
||||
t.Fatalf("attempt %d: expected a normal wrong-password error, got: %s", i, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
locked := attempt("lockout-victim@example.com", "the-real-password-1!") // even the CORRECT password now
|
||||
if !strings.Contains(locked.Body.String(), "Too many failed attempts") {
|
||||
t.Fatalf("expected the account locked out after repeated failures, got: %s", locked.Body.String())
|
||||
}
|
||||
if locked.Result().Cookies() != nil {
|
||||
for _, c := range locked.Result().Cookies() {
|
||||
if c.Name == mailboxSessionCookieName && c.Value != "" {
|
||||
t.Fatal("expected no session granted while locked out, even with the correct password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A different account from the same IP is unaffected by the per-account lockout.
|
||||
rec := attempt("lockout-other@example.com", "another-password-1!")
|
||||
found := false
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
if c.Name == mailboxSessionCookieName {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected a different account from the same IP to log in normally")
|
||||
}
|
||||
_ = victimID
|
||||
_ = otherID
|
||||
}
|
||||
|
||||
// TestLoginRateLimitPerIP confirms the per-IP throttle kicks in independent of which
|
||||
// account is being tried, once enough requests arrive from one source IP.
|
||||
func TestLoginRateLimitPerIP(t *testing.T) {
|
||||
app := newTestApp(t)
|
||||
app.loginLimiter = newIPRateLimiter(3, 1<<62) // tiny limit, effectively-infinite window for a deterministic test
|
||||
mux := app.Mux()
|
||||
domains, _ := app.DB.ListDomains()
|
||||
createTestMailboxWithPassword(t, app, "ratelimit@example.com", domains[0].ID, "correct-password-1!")
|
||||
|
||||
attempt := func() *httptest.ResponseRecorder {
|
||||
form := url.Values{"email": {"ratelimit@example.com"}, "password": {"correct-password-1!"}}
|
||||
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/login", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.RemoteAddr = "203.0.113.10:12345"
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if rec := attempt(); rec.Code == http.StatusTooManyRequests {
|
||||
t.Fatalf("attempt %d: unexpectedly rate-limited early", i)
|
||||
}
|
||||
}
|
||||
if rec := attempt(); rec.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("expected 429 past the per-IP limit, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user