feat: throttle failed logins per client IP

PR C of SECURITY_PLAN.md: after 5 failed /login attempts an IP is refused
for 30 s, and each failure after that refuses it for twice as long, up
to 15 min. A success resets it, as does an hour without a failure.

- LoginThrottle.h holds the schedule and 8 IPs, with the time passed in
  so native tests cover it. While all 8 hold recent failures, every
  other IP shares one more slot, so many addresses don't each get five
  free tries.
- A refused IP gets 429 with Retry-After before its credentials are
  checked, so a right guess during a lockout reveals nothing. Home
  Assistant retries a 429 instead of asking for new credentials.
- The web UI's login panel shows a 401's or 429's message, where it
  popped a service error.
- test/e2e --lockout checks it on a Secured device and waits it out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-26 22:08:30 +01:00
parent 1612fd1587
commit bab83313ee
8 changed files with 326 additions and 1 deletions

View file

@ -1187,7 +1187,10 @@ class Security {
}
sec.pin = pin;
putJSONSync('/login', sec, (err, log) => {
if (err) ui.serviceError(err);
// Wrong credentials (401) or too many of them (429) say why in
// the panel, as an old 200 with success false did.
if (err && (err.htmlError === 401 || err.htmlError === 429) && err.msg) msg.innerHTML = err.msg;
else if (err) ui.serviceError(err);
else {
console.log(log);
if (log.success) {