""" A stranger who knows your name locks only the browsers you never used. The sign-in lockout counts wrong passphrases per username: four an hour from anyone kept the owner out of every browser for as long as they cared to keep going. A browser that signed in to the account before presents a token and has a counter of its own, which nobody else can spend. The token is not a credential — the passphrase is still asked — and a passphrase re-checked inside an open session is not the sign-in counter's business at all. """ import pytest from meshbay_hub.db.models import KnownBrowser, User from sqlalchemy import func, select from test_bundle_pepper import KEY, _register WRONG = "w" * 44 async def _sign_in(client, username, key=KEY, known=None): body = {"username": username, "auth_key": key} if known: body["known_browser"] = known return await client.post("/v1/users/login", json=body) async def _lock(client, username): for _ in range(4): await _sign_in(client, username, WRONG) assert (await _sign_in(client, username)).status_code == 429 @pytest.mark.asyncio async def test_a_known_browser_signs_in_through_a_strangers_lockout(client): await _register(client, "known_owner") token = (await _sign_in(client, "known_owner")).json()["known_browser"] await _lock(client, "known_owner") # the stranger, without a token r = await _sign_in(client, "known_owner", known=token) assert r.status_code == 200, r.text assert "known_browser" not in r.json(), "a known browser is not given a second token" @pytest.mark.asyncio async def test_another_accounts_token_is_no_way_round(client): await _register(client, "known_alice") await _register(client, "known_bobby") alices = (await _sign_in(client, "known_alice")).json()["known_browser"] await _lock(client, "known_bobby") assert (await _sign_in(client, "known_bobby", known=alices)).status_code == 429 @pytest.mark.asyncio async def test_a_known_browser_is_locked_by_its_own_failures(client): """Whoever holds the token still guesses at the same rate.""" await _register(client, "known_guess") token = (await _sign_in(client, "known_guess")).json()["known_browser"] for _ in range(4): assert (await _sign_in(client, "known_guess", WRONG, token)).status_code == 401 assert (await _sign_in(client, "known_guess", known=token)).status_code == 429 # ...and that spent nothing of a browser that has no token. assert (await _sign_in(client, "known_guess")).status_code == 200 @pytest.mark.asyncio async def test_a_locked_name_does_not_stop_its_owner_inside_a_session(client): await _register(client, "known_inside") session = (await _sign_in(client, "known_inside")).json()["access_token"] await _lock(client, "known_inside") r = await client.post("/v1/users/me/bundle-pepper", json={"auth_key": KEY}, headers={"Authorization": f"Bearer {session}"}) assert r.status_code == 200, r.text @pytest.mark.asyncio async def test_only_a_hash_is_kept_and_only_twenty(client, db_session): await _register(client, "known_many") tokens = [(await _sign_in(client, "known_many")).json()["known_browser"] for _ in range(25)] uid = (await db_session.execute( select(User.id).where(User.username == "known_many"))).scalar_one() rows = (await db_session.execute( select(KnownBrowser).where(KnownBrowser.user_id == uid))).scalars().all() assert len(rows) == 20 assert not any(t in {r.token_hash for r in rows} for t in tokens) @pytest.mark.asyncio async def test_erasing_the_account_forgets_its_browsers(client, db_session): await _register(client, "known_gone") login = (await _sign_in(client, "known_gone")).json() r = await client.request("DELETE", "/v1/users/me", json={"auth_key": KEY}, headers={"Authorization": f"Bearer {login['access_token']}"}) assert r.status_code == 200, r.text left = await db_session.scalar(select(func.count()).select_from(KnownBrowser)) assert left == 0