feat(auth): throttle login, register, password reset and Subsonic auth failures (M462 #4976)
test-go / test (push) Successful in 1m54s
test-web / test (push) Successful in 1m34s
test-go / integration (push) Successful in 4m56s
android / Build + lint + test (push) Successful in 5m41s
release / Build signed APK (releases and dev) (push) Successful in 5m52s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
test-go / test (push) Successful in 1m54s
test-web / test (push) Successful in 1m34s
test-go / integration (push) Successful in 4m56s
android / Build + lint + test (push) Successful in 5m41s
release / Build signed APK (releases and dev) (push) Successful in 5m52s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
Every password-shaped check was mounted bare, so guessing was limited only by bcrypt cost. A shared in-memory AttemptLimiter now sits in front of them: - login: 10 failures per account and 50 per address per 15 min, checked before the user lookup and bcrypt; 429 with Retry-After. A success clears the account's count but not the address's. - unknown usernames run a dummy bcrypt compare, so timing no longer says which accounts exist. - register: 10 per address per hour; forgot-password: 5 per address and 3 per email per hour (applied whether or not the email matches); reset: 20 failed tokens per address per 15 min. - Subsonic /rest: same limits as login, counting only wrong credentials, since clients authenticate on every request. Web login, register, reset and forgot-password screens say how long to wait; web and Android carry copy for the rate_limited code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,16 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
@@ -36,14 +40,34 @@ func UserFromContext(ctx context.Context) (dbq.User, bool) {
|
||||
// users.subsonic_password (t/s or p). On failure it writes a Subsonic failed
|
||||
// envelope using the request's f= format; downstream handlers never see an
|
||||
// unauthenticated request.
|
||||
func Middleware(pool *pgxpool.Pool, cfg Config) func(http.Handler) http.Handler {
|
||||
//
|
||||
// guard throttles wrong credentials per account and per address, the same
|
||||
// limits as the native login. Only wrong-credential failures count: Subsonic
|
||||
// clients authenticate on every request, so counting successes would lock
|
||||
// out a client for playing an album. hops is the live trusted-proxy depth
|
||||
// used to find the caller's address. A nil guard disables throttling.
|
||||
func Middleware(pool *pgxpool.Pool, cfg Config, guard *auth.LoginGuard, hops func() int) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
addr := auth.ClientIP(r, hops())
|
||||
// apiKey requests have no account to charge; they count against
|
||||
// the address alone.
|
||||
account := r.URL.Query().Get("u")
|
||||
if blocked, wait := guard.Blocked(account, addr); blocked {
|
||||
secs := int(math.Ceil(wait.Seconds()))
|
||||
w.Header().Set("Retry-After", strconv.Itoa(max(secs, 1)))
|
||||
WriteFail(w, r, ErrGeneric, fmt.Sprintf("Too many failed sign-in attempts; try again in %d seconds", max(secs, 1)))
|
||||
return
|
||||
}
|
||||
user, code, msg := authenticate(r, pool, cfg)
|
||||
if code == ErrWrongCredentials {
|
||||
guard.Fail(account, addr)
|
||||
}
|
||||
if code != 0 {
|
||||
WriteFail(w, r, code, msg)
|
||||
return
|
||||
}
|
||||
guard.Succeed(account)
|
||||
ctx := context.WithValue(r.Context(), userCtxKey, user)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user