aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_known_browser.py
blob: 260f08e4d1315ff51957658f9bbcd0e0b9b56cd4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""
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