aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_login_lockout.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-01 11:47:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-01 11:47:39 +0200
commit752b160c7c5e671e0db8f402a52fac27bb85ab06 (patch)
tree61be38fa0f5d38e2bda291b69aa1037f36112f23 /packages/meshbay-hub/tests/test_login_lockout.py
parent15e117673d2303bf476d4f78699e47913ce1aec0 (diff)
downloadmeshbay-752b160c7c5e671e0db8f402a52fac27bb85ab06.tar.gz
fix(hub): a stranger who knows your name locks only browsers you never used
A sign-in from a browser that presented no token is answered with one (known_browser, kept hashed, twenty per account); a later sign-in presenting it counts failures on its own row, which nobody else can spend. Passphrase checks inside an open session (change, e-mail, deletion, device, pepper) count on the account's own row, so a locked name no longer stops its owner there either; /me reports that row. Reset and erasure forget the browsers (F-15). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_login_lockout.py')
-rw-r--r--packages/meshbay-hub/tests/test_login_lockout.py20
1 files changed, 16 insertions, 4 deletions
diff --git a/packages/meshbay-hub/tests/test_login_lockout.py b/packages/meshbay-hub/tests/test_login_lockout.py
index 601edbd..8f06d2d 100644
--- a/packages/meshbay-hub/tests/test_login_lockout.py
+++ b/packages/meshbay-hub/tests/test_login_lockout.py
@@ -131,8 +131,13 @@ async def test_a_burst_of_concurrent_guesses_gets_no_more_than_the_limit(client)
@pytest.mark.asyncio
-async def test_change_password_counts_on_the_same_row(client):
- """It checks the same passphrase, so it is the same oracle."""
+async def test_change_password_counts_on_the_sessions_own_row(client):
+ """
+ It checks the passphrase, so it is counted and locked like a sign-in — on a
+ row of the session's own. A stranger failing at sign-in must not stop the
+ owner changing their passphrase from a session they hold, and failures here
+ must not lock the owner's other browsers out of signing in.
+ """
await _register(client, "grace_test")
token = (await _login(client, "grace_test", RIGHT)).json()["access_token"]
auth = {"Authorization": f"Bearer {token}"}
@@ -145,7 +150,7 @@ async def test_change_password_counts_on_the_same_row(client):
r = await client.post("/v1/users/password", headers=auth, json={
"old_auth_key": RIGHT, "new_auth_key": "n" * 44})
assert r.status_code == 429, r.text
- assert (await _login(client, "grace_test", RIGHT)).status_code == 429
+ assert (await _login(client, "grace_test", RIGHT)).status_code == 200
@pytest.mark.asyncio
@@ -157,10 +162,17 @@ async def test_a_signed_in_session_is_told_its_own_lockout(client):
auth = {"Authorization": f"Bearer {token}"}
assert (await client.get("/v1/users/me", headers=auth)).json()["passphrase_locked_for"] == 0
- await _fail(client, "olivia_test", 4)
+ for _ in range(4):
+ await client.post("/v1/users/password", headers=auth, json={
+ "old_auth_key": WRONG, "new_auth_key": "n" * 44})
left = (await client.get("/v1/users/me", headers=auth)).json()["passphrase_locked_for"]
assert 3500 <= left <= 3600
+ # A stranger failing at sign-in is not this session's lockout.
+ await _fail(client, "olivia_test", 4)
+ other = (await _login(client, "olivia_test", RIGHT))
+ assert other.status_code == 429
+
@pytest.mark.asyncio
async def test_an_attempt_that_checks_no_passphrase_is_not_counted(client, db_session):