fix(auth): a cookie that lands after sign-out no longer signs the browser back in
A renewal answers with the session its request started with. Each session now carries an id; signing out, or signing in as someone else, revokes it (revoked_sessions, migration 0120), and a cookie carrying a revoked id is refused. A cookie from before session ids gets one drawn from the cookie itself. The race after log-out-everywhere fails closed and stays.
This commit is contained in:
@@ -172,9 +172,66 @@ def test_a_session_renewed_today_is_not_re_signed_on_every_request(client, make_
|
||||
assert "set-cookie" not in r.headers
|
||||
|
||||
|
||||
def test_a_session_with_no_renewal_stamp_is_renewed(client, make_user, monkeypatch):
|
||||
# A cookie signed before the session slid carries no `renewed`: its first request stamps one.
|
||||
make_user(email="slide3@example.com", password=_PW)
|
||||
assert _login(client, "slide3@example.com", _PW).status_code == 200
|
||||
payload = {k: v for k, v in _session(client).items() if k != "renewed"}
|
||||
now = int(TimestampSigner(str(settings.secret_key)).get_timestamp())
|
||||
_hold(client, _signed_at(monkeypatch, now - 13 * _DAY, payload))
|
||||
r = client.get(_PROTECTED)
|
||||
assert r.status_code == 200
|
||||
assert isinstance(_decode(r.cookies.get("session")).get("renewed"), int)
|
||||
|
||||
|
||||
# A request still in flight when the browser signs out (or signs in as someone else) answers with the
|
||||
# session it started with — re-signed, if it renewed it — and that late cookie lands after the one the
|
||||
# sign-out cleared. Holding it must not sign the browser back in.
|
||||
def test_a_cookie_that_lands_after_sign_out_is_refused(client, make_user):
|
||||
make_user(email="late1@example.com", password=_PW)
|
||||
assert _login(client, "late1@example.com", _PW).status_code == 200
|
||||
in_flight = client.cookies.get("session")
|
||||
assert client.post("/auth/logout").status_code == 200
|
||||
_hold(client, in_flight)
|
||||
assert client.get(_PROTECTED).status_code == 401
|
||||
assert client.get("/api/me").json() == {"authenticated": False}
|
||||
|
||||
|
||||
def test_a_cookie_from_before_a_sign_in_as_another_account_is_refused(client, make_user):
|
||||
make_user(email="late2a@example.com", password=_PW)
|
||||
make_user(email="late2b@example.com", password=_PW)
|
||||
assert _login(client, "late2a@example.com", _PW).status_code == 200
|
||||
in_flight = client.cookies.get("session")
|
||||
assert _login(client, "late2b@example.com", _PW).status_code == 200
|
||||
bob = client.cookies.get("session")
|
||||
_hold(client, in_flight)
|
||||
assert client.get(_PROTECTED).status_code == 401
|
||||
_hold(client, bob) # Bob's own session is untouched
|
||||
assert client.get("/api/me").json()["email"] == "late2b@example.com"
|
||||
|
||||
|
||||
def test_a_session_from_before_session_ids_gets_one_and_its_sign_out_holds(client, make_user):
|
||||
make_user(email="late3@example.com", password=_PW)
|
||||
assert _login(client, "late3@example.com", _PW).status_code == 200
|
||||
old = _cookie({k: v for k, v in _session(client).items() if k != "sid"})
|
||||
_hold(client, old)
|
||||
r = client.get(_PROTECTED)
|
||||
assert r.status_code == 200
|
||||
in_flight = r.cookies.get("session")
|
||||
assert _decode(in_flight).get("sid")
|
||||
_hold(client, in_flight)
|
||||
assert client.post("/auth/logout").status_code == 200
|
||||
_hold(client, in_flight)
|
||||
assert client.get(_PROTECTED).status_code == 401
|
||||
|
||||
|
||||
def _session(client) -> dict:
|
||||
"""The decoded payload of the client's session cookie."""
|
||||
raw = client.cookies.get("session")
|
||||
return _decode(client.cookies.get("session"))
|
||||
|
||||
|
||||
def _decode(raw: str | None) -> dict:
|
||||
"""The payload of a session cookie's value."""
|
||||
if not raw:
|
||||
return {}
|
||||
data = TimestampSigner(str(settings.secret_key)).unsign(raw.encode())
|
||||
|
||||
Reference in New Issue
Block a user