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

@@ -178,6 +178,17 @@ describe("under an SSO-only policy", () => {
expect((await linkOf("acct-a")).oidcSub).toBe("idp-sub-1");
});
it("answers the same for another account's right and wrong password", async () => {
// Refused before the password is checked, so the exception cannot be
// used to test passwords of accounts the link does not name.
pendingLinkFor("acct-a");
const right = await passwordLogin("acct-b");
const wrong = await passwordLogin("acct-b", "not the password");
expect(right.status).toBe(403);
expect(wrong.status).toBe(403);
expect(await wrong.json()).toEqual(await right.json());
});
it("keeps it closed for any other account", async () => {
pendingLinkFor("acct-a");
const res = await passwordLogin("acct-b");