fix(auth): wrong passwords from elsewhere can no longer lock the owner out

The per-account password limit refused every attempt once ten failures
had landed in fifteen minutes, counted before the password was checked.
Ten guesses from two addresses therefore locked a password-only account,
the app's password sign-in included, and repeating them kept it locked
for as long as anyone cared to.

A place the account has signed in from before is now neither counted nor
held back: a remembered browser, a phone presenting the device id a login
of this account was issued to, or an address a session or device login of
this account came from in the last thirty days. Anywhere else gets five
free failures a day and then a wait that doubles with each failure, from
thirty seconds to at most fifteen minutes, instead of a wall. The per-IP
limit is unchanged, and an unknown identifier is limited the same way on
its hash.

Under an SSO-only policy, a password sign-in to an account other than the
one a pending link names is now refused before the password is checked,
so that exception no longer tells right passwords from wrong ones.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 22:42:40 +02:00
1 parent 1900792756
commit 995bd2385a
11 files changed
+527 -57

No files matched your search

+12 -1
View File
@@ -1865,6 +1865,15 @@ paths:
200 with `data: null, error: null` and `meta.mfaRequired: true` plus a single-use `meta.mfaTicket` and the
`meta.methods` list. The client must POST the ticket + a code to `/api/auth/mfa/verify` to obtain the token
bundle. Accounts without MFA are unchanged.
Two limits apply. Per source address, five attempts in fifteen minutes. Per account, failed passwords from a
place the account has not signed in from before make the next attempt wait: five failures a day are free, then
each one doubles the wait, from thirty seconds up to at most fifteen minutes. A place the account has signed in
from is not counted and not held back: a remembered browser (trusted-device cookie), a phone that sends the
`X-Device-Id` a login of this account was issued to, or an address a session or device login of this account
came from in the last thirty days. Both answer 429 with `Retry-After`. An identifier that names no account is
limited the same way, so a 429 says nothing about whether it exists.
requestBody:
required: true
content:
@@ -1884,7 +1893,9 @@ paths:
"403":
description: "Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in
through SSO instead. The one exception is a browser the SSO callback sent back with a pending link to
confirm an existing account: it may sign in with that one account's password, which links the account."
confirm an existing account: it may sign in with that one account's password, which links the account. A
sign-in to any other account on that browser is refused before its password is checked, so the answer is the
same whether the password was right or wrong."
content:
application/json:
schema: