fix(auth): a session in use slides instead of expiring on day 14
Starlette re-signs the session cookie only when the session changes, so a browser in daily use was signed out 14 days after sign-in, mid-episode. The first authenticated request a day after the last renewal now writes a fresh stamp, which re-signs the cookie; only 14 idle days end a session. The epoch stays the revoke.
This commit is contained in:
@@ -122,6 +122,56 @@ def test_deleting_the_account_signs_the_browser_out(client, make_user):
|
||||
assert "user_id" not in _session(client)
|
||||
|
||||
|
||||
_DAY = 86400
|
||||
_PROTECTED = "/api/me/notifications" # any route behind `current_user` that answers 200 offline
|
||||
|
||||
|
||||
def _signed_at(monkeypatch, at: int, payload: dict) -> str:
|
||||
"""A session cookie carrying `payload`, signed as if at the unix time `at`."""
|
||||
with monkeypatch.context() as m: # not `undo()`: that would also undo the fixtures' patches
|
||||
m.setattr(TimestampSigner, "get_timestamp", lambda self: at)
|
||||
return _cookie(payload)
|
||||
|
||||
|
||||
def _hold(client, cookie: str) -> None:
|
||||
"""Make `cookie` the browser's only session cookie (a plain `set` would sit beside the one the
|
||||
server set, under another domain)."""
|
||||
client.cookies.clear()
|
||||
client.cookies.set("session", cookie)
|
||||
|
||||
|
||||
def test_a_session_in_use_does_not_expire_after_the_cookie_lifetime(client, make_user, monkeypatch):
|
||||
# The cookie's signature is checked against a 14-day life, and Starlette re-signs it only when
|
||||
# the session changes. A browser in daily use must still stay signed in: a session that has not
|
||||
# been renewed for a day is renewed by the next authenticated request.
|
||||
make_user(email="slide@example.com", password=_PW)
|
||||
assert _login(client, "slide@example.com", _PW).status_code == 200
|
||||
now = int(TimestampSigner(str(settings.secret_key)).get_timestamp())
|
||||
payload = {**_session(client), "renewed": now - 2 * _DAY}
|
||||
old = _signed_at(monkeypatch, now - 13 * _DAY, payload)
|
||||
|
||||
_hold(client, old)
|
||||
r = client.get(_PROTECTED)
|
||||
assert r.status_code == 200
|
||||
renewed = r.cookies.get("session")
|
||||
assert renewed and renewed != old # the request re-signed the cookie
|
||||
|
||||
# Two days on, the cookie signed 13 days ago is past its 14 days; the renewed one is not.
|
||||
monkeypatch.setattr(TimestampSigner, "get_timestamp", lambda self: now + 2 * _DAY)
|
||||
_hold(client, old)
|
||||
assert client.get(_PROTECTED).status_code == 401
|
||||
_hold(client, renewed)
|
||||
assert client.get(_PROTECTED).status_code == 200
|
||||
|
||||
|
||||
def test_a_session_renewed_today_is_not_re_signed_on_every_request(client, make_user):
|
||||
make_user(email="slide2@example.com", password=_PW)
|
||||
assert _login(client, "slide2@example.com", _PW).status_code == 200
|
||||
r = client.get(_PROTECTED)
|
||||
assert r.status_code == 200
|
||||
assert "set-cookie" not in r.headers
|
||||
|
||||
|
||||
def _session(client) -> dict:
|
||||
"""The decoded payload of the client's session cookie."""
|
||||
raw = client.cookies.get("session")
|
||||
|
||||
Reference in New Issue
Block a user