mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-23 13:32:53 +00:00
Rate limit webauthn endpoints
This commit is contained in:
1 parent
1f68a4b047
commit
3af089aada
2 files changed
+41
-2
No files matched your search
@@ -80,9 +80,15 @@ func (s *Handlers) credentialRoutes(r chi.Router) {
|
||||
authLimit := web.NewRateLimiter(10, 6*time.Second)
|
||||
r.With(authLimit.Middleware).Post("/login/password", s.Auth.LoginPassword)
|
||||
r.With(authLimit.Middleware).Post("/signup/password", s.Auth.SignupPassword)
|
||||
r.Post("/api/register/begin", s.Auth.RegisterBegin)
|
||||
// The user-initiated WebAuthn "begin" ceremonies share the same per-IP
|
||||
// credential-attempt bucket: register/begin persists an account row (unbounded
|
||||
// spam would flood the users table and squat usernames) and login/begin is a
|
||||
// username oracle. The finish steps require an in-progress ceremony, and
|
||||
// discoverable/begin auto-fires on page load (passive autofill) and writes
|
||||
// nothing, so those stay unthrottled.
|
||||
r.With(authLimit.Middleware).Post("/api/register/begin", s.Auth.RegisterBegin)
|
||||
r.Post("/api/register/finish", s.Auth.RegisterFinish)
|
||||
r.Post("/api/login/begin", s.Auth.LoginBegin)
|
||||
r.With(authLimit.Middleware).Post("/api/login/begin", s.Auth.LoginBegin)
|
||||
r.Post("/api/login/finish", s.Auth.LoginFinish)
|
||||
r.Post("/api/login/discoverable/begin", s.Auth.LoginDiscoverableBegin)
|
||||
r.Post("/api/login/discoverable/finish", s.Auth.LoginDiscoverableFinish)
|
||||
|
||||
@@ -61,6 +61,39 @@ func TestPasswordSignupAndLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWebAuthnBeginRateLimited: the user-initiated passkey "begin" ceremonies
|
||||
// share the credential-attempt limiter. register/begin persists an account row,
|
||||
// so an unthrottled loop would flood the users table and squat usernames; this
|
||||
// is a regression guard for the pre-release audit finding that they were
|
||||
// unlimited. A malformed body 400s in the handler, but the limiter middleware
|
||||
// runs first — which is all this exercises — so after the burst we get 429.
|
||||
func TestWebAuthnBeginRateLimited(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, path := range []string{"/api/register/begin", "/api/login/begin"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, _, ts, h := splitServer(t)
|
||||
|
||||
var got429, gotAllowed bool
|
||||
for i := 0; i < 15; i++ {
|
||||
resp := post(t, ts, h.auth, path, url.Values{})
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusTooManyRequests {
|
||||
got429 = true
|
||||
} else {
|
||||
gotAllowed = true
|
||||
}
|
||||
}
|
||||
if !gotAllowed {
|
||||
t.Fatalf("%s: every request throttled; burst should allow some through", path)
|
||||
}
|
||||
if !got429 {
|
||||
t.Fatalf("%s: never returned 429; the rate limiter is not wired", path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// assertAccountOK asserts a 303 back to /account carrying a success ("ok") flash.
|
||||
func assertAccountOK(t *testing.T, resp *http.Response, label string) {
|
||||
t.Helper()
|
||||
|
||||
Reference in new issue
Block a user