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:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
|
||||
)
|
||||
|
||||
@@ -19,11 +20,14 @@ import (
|
||||
// both /rest/foo and /rest/foo.view because client conventions vary. Both
|
||||
// GET and POST are accepted; Subsonic's auth params live in the query string
|
||||
// either way.
|
||||
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, cfg Config, events *playevents.Writer) {
|
||||
//
|
||||
// hops is the live trusted-proxy depth, read per request so an admin change
|
||||
// applies without a restart; it locates the caller for the auth throttle.
|
||||
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, cfg Config, events *playevents.Writer, hops func() int) {
|
||||
b := &browseHandlers{pool: pool}
|
||||
m := newMediaHandlers(pool, events, logger)
|
||||
r.Route("/rest", func(sub chi.Router) {
|
||||
sub.Use(Middleware(pool, cfg))
|
||||
sub.Use(Middleware(pool, cfg, auth.NewLoginGuard(), hops))
|
||||
register(sub, "/ping", handlePing)
|
||||
register(sub, "/getLicense", handleGetLicense)
|
||||
register(sub, "/getMusicFolders", b.getMusicFolders)
|
||||
|
||||
Reference in New Issue
Block a user