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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
|
"""
Staying signed in.
Reported: after an hour or so of watching a film, every action answers "token
expired or invalid", and the only way back is signing out and in again. Also on
simply reopening the tab the next day.
Both come from the same place. The access token lasts an hour and the refresh
token thirty days, but nothing used the second one. `hubFetch` reported a 401 as
an error like any other, and watching a film is precisely the activity during
which the hub hears nothing at all — the video travels over WebRTC — so the hour
ran out with no request to notice.
Underneath that was a worse one. The hub *rotates*: `/v1/users/token/refresh`
revokes the token presented, returns a replacement, and treats a revoked token
presented again as theft, revoking the whole family. The client kept only the
access token out of that response and dropped the new refresh token. So the
refresh token was spent on first use, and the second attempt did not merely fail
— it destroyed the family, which is why signing out and back in was the only
cure.
These run the shipped `hubFetch`, `refreshAccessToken` and `ensureFreshToken`
against a fake hub that enforces the rotation rule. That rule is the point: a
stub which accepted the same refresh token twice would have passed against the
broken client.
"""
import json
import shutil
import subprocess
import re
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
APP = STATIC / "app.js"
HARNESS = Path(__file__).parent / "harness" / "session_harness.mjs"
pytestmark = pytest.mark.skipif(
shutil.which("node") is None or not APP.exists(),
reason="node or the SPA sources are not available")
def _run(scenario: str, app: Path = APP) -> dict:
proc = subprocess.run(
["node", str(HARNESS), str(app), json.dumps({"scenario": scenario})],
capture_output=True, text=True, timeout=60)
assert proc.returncode == 0, f"{proc.stdout}\n{proc.stderr}"
return json.loads(proc.stdout.strip().splitlines()[-1])
@pytest.fixture(scope="module")
def broken(tmp_path_factory):
"""The client as it shipped: the rotated refresh token dropped."""
out = tmp_path_factory.mktemp("session") / "broken.js"
src = APP.read_text()
replaced = src.replace(
" refreshToken: data.refresh_token || _auth.refreshToken,",
" refreshToken: _auth.refreshToken,")
assert replaced != src, (
"could not reconstruct the defect — the line it hinged on has moved, "
"and the A/B below would be comparing the fix against itself")
out.write_text(replaced)
return out
# ── The reported symptom ──────────────────────────────────────────────────────
def test_an_expired_access_token_renews_itself(app=None):
"""A tab reopened the next morning, or a film watched for an hour."""
r = _run("expired-access")
assert not r["signedOut"], "the session was thrown away instead of renewed"
assert r["finalCallAccepted"], "the request was not replayed after renewing"
assert r["refreshCalls"] == 1
def test_renewal_survives_being_needed_more_than_once():
"""The defect underneath the symptom.
The hub hands back a new refresh token every time and revokes the old one.
Keep the old one and the second renewal is read as theft.
"""
r = _run("repeat")
assert not r["familyRevoked"], (
f"the family was revoked after presenting {r['refreshTokensPresented']} "
"— the rotated token is not being stored")
assert not r["signedOut"]
assert len(set(r["refreshTokensPresented"])) == len(r["refreshTokensPresented"]), (
f"the same refresh token was presented twice: {r['refreshTokensPresented']}")
def test_the_defect_is_reproduced_by_dropping_the_rotated_token(broken):
"""Otherwise the test above proves nothing.
This is the shipped behaviour, and it ends exactly where the report did:
signed out, with nothing but signing back in to be done about it.
"""
r = _run("repeat", app=broken)
assert r["familyRevoked"], (
"dropping the rotated refresh token no longer breaks anything, so the "
"test above is not guarding what it claims")
assert r["signedOut"]
assert r["refreshTokensPresented"] == ["RT-1", "RT-1"]
# ── Renewing exactly once ─────────────────────────────────────────────────────
def test_two_requests_racing_share_one_renewal():
"""Two 401s at the same instant must not present the token twice.
They would each renew, the second presenting what the first had already
spent — which the hub cannot tell from a stolen token, and answers by
revoking the family. The failure mode is worse than the problem.
"""
r = _run("concurrent")
assert r["refreshCalls"] == 1, (
f"{r['refreshCalls']} renewals for one expiry")
assert not r["familyRevoked"]
assert r["finalCallAccepted"], "the queued requests were not replayed"
def test_a_valid_token_is_left_alone():
"""Renewing on every call would be its own kind of broken."""
r = _run("fresh-access")
assert r["refreshCalls"] == 0, "a token with an hour left was renewed anyway"
assert r["finalCallAccepted"]
# ── When there is genuinely nothing left ──────────────────────────────────────
def test_an_unusable_refresh_token_signs_out_cleanly():
"""Thirty days later, or after the family was revoked.
There is nothing to salvage, and the alternative is a session that fails
every call for ever while looking signed in.
"""
r = _run("refresh-rejected")
assert r["signedOut"], (
"the session survived a refusal, so every later call fails with no way "
"for the person to understand why")
assert r["storedRefreshToken"] is None
assert r["refreshCalls"] == 1, "it kept trying a token the hub had refused"
# ── The lifetimes themselves ──────────────────────────────────────────────────
def test_the_access_token_outlives_a_film_on_its_own():
"""Not because it has to — renewal covers any length — but as a floor.
Renewal makes the access token's life invisible in normal use. This exists
for the abnormal one: if renewal fails, a token this short is how long
someone has before they notice. An hour was less than a feature film.
"""
from meshbay_hub.config import JWTConfig
ttl = JWTConfig().access_token_ttl
assert ttl >= 4 * 3600, (
f"{ttl / 3600:.1f} h does not cover a long film if renewal fails")
assert ttl <= 12 * 3600, (
f"{ttl / 3600:.1f} h is a long time for a leaked token to stay usable, "
"and it lets the renewal path go a whole day without being exercised — "
"which is how it came to be broken without anyone noticing")
def test_the_session_is_much_longer_than_the_token():
"""The two must not be confused: the session is the refresh token."""
from meshbay_hub.config import JWTConfig
cfg = JWTConfig()
assert cfg.refresh_token_ttl >= 7 * 86400
assert cfg.refresh_token_ttl > cfg.access_token_ttl * 20, (
"the refresh token is barely longer than the access token, so renewing "
"buys almost nothing and signing in again comes round just as fast")
# ── The margin ────────────────────────────────────────────────────────────────
def test_renewal_happens_before_expiry_not_after():
"""A margin, so the first click after a long film does not pay for a 401."""
src = APP.read_text()
import re
margin = int(re.search(r"const TOKEN_RENEW_MARGIN_S = (\d+)", src).group(1))
assert margin >= 300, (
f"{margin} s of margin against a one-hour token is thin: a backgrounded "
"tab has its timers throttled and may not check for minutes")
assert "visibilitychange" in src, (
"nothing re-checks when the tab comes back, which is exactly when the "
"token is most likely to have aged out unnoticed")
# ── What renewal must not disturb ─────────────────────────────────────────────
def test_renewing_does_not_tear_down_the_webrtc_connection():
"""The regression that renewal introduced.
The effect that dials the node listed `token` among its dependencies. That
was harmless while a token never changed during a session — it only ran
out. Once the session renews itself the string rotates, and the effect tore
the connection down and rebuilt it each time. Worst on arrival: a stored
token past its life is renewed the instant the page mounts, which is when
the group page is negotiating ICE, so the browser abandoned the handshake
and the node sat in `connecting` for ever.
Signing in or out must still re-run it, so the dependency is whether there
is a token, not which one.
"""
src = APP.read_text()
i = src.index("means tearing down the WebRTC connection")
deps = src[i:src.index(");", i)]
assert "Boolean(token)" in deps, (
"the WebRTC effect depends on the token's value again — every renewal "
"drops the connection, and one landing mid-handshake never recovers")
assert "[groupId, token," not in deps
def test_the_connection_signs_its_offer_with_a_live_token():
"""The other half: not re-running means the captured token can be stale.
It signs the offer relayed through the hub, where an expired one is a 401
and no connection at all.
"""
src = APP.read_text()
connect = src[src.index("const connect = async () => {"):]
connect = connect[:connect.index("\n };")]
assert "await ensureFreshToken()" in connect, (
"the offer is signed with whatever token the effect captured, which is "
"no longer refreshed by a re-run")
# Asserted on the argument, not on the whole call: the first argument is
# the hub's base URL and became configurable when the interface started
# shipping in a package. Pinning the literal made this fail for a change
# that had nothing to do with tokens.
built = re.search(r"new window\.MeshBayTransport\(([^)]*)\)", connect)
assert built, "the transport is not built in connect()"
args = [a.strip() for a in built.group(1).split(",")]
assert args[-1] == "live", (
f"the transport is built with {args[-1]!r} rather than the live token")
|