fix(auth): a cancel at Google lands where the browser is; the probe says why it refused

A refused session keeps its cookie, so the cancel branch, keyed on the cookie's account, sent a
signed-out browser to the in-app toast the login page never shows: it now asks whether the
session holds. The probe's log line named every refusal 'its account no longer holds'; it now
carries the reason. The pruning docstring says again that a renewal may land a request's time
after the revoke, and the test pins the day of margin. The test clock clamps in the zone asked for.
This commit is contained in:
2026-11-14 20:07:13 +01:00
parent 7dd50db9b7
commit 7a1487f554
6 changed files with 98 additions and 19 deletions
+3 -1
View File
@@ -332,7 +332,9 @@ class _NeverBack(_dt.datetime):
t = cls.clock.now(tz)
last = cls.last
if last is not None and (last.tzinfo is None) == (t.tzinfo is None) and t <= last:
t = last + _dt.timedelta(microseconds=1)
# In the zone asked for: a Pacific reading clamped to a UTC one gave the UTC date.
nudged = last + _dt.timedelta(microseconds=1)
t = nudged.astimezone(t.tzinfo) if t.tzinfo is not None else nudged
cls.last = t
return t
+18 -1
View File
@@ -223,6 +223,20 @@ def test_a_cookie_from_before_a_sign_in_as_another_account_is_refused(client, ma
assert client.get("/api/me").json()["email"] == "late2b@example.com"
def test_the_probe_says_why_a_session_was_refused(client, make_user, caplog):
# Only some refusals mean the account changed; a cookie with no session id is not one of them.
make_user(email="why@example.com", password=_PW)
assert _login(client, "why@example.com", _PW).status_code == 200
_hold(client, _cookie({k: v for k, v in _session(client).items() if k != "sid"}))
with caplog.at_level("INFO", logger="siftlode.auth"):
assert client.get("/api/me").json() == {"authenticated": False}
[line] = [
r for r in caplog.records if getattr(r, "event", None) == "auth.session.probe_rejected"
]
assert line.getMessage() == "a session was refused"
assert line.attrs["reason"] == "no_session_id"
def test_a_cookie_from_before_session_ids_signs_in_once_more(client, make_user):
# Its sign-out could revoke nothing, so a request of its still in flight signed the browser back
# in. Every such cookie would have run out within 14 days anyway: its holder signs in once more.
@@ -250,11 +264,14 @@ def test_a_revoke_drops_only_the_revocations_no_cookie_can_outlive(db):
life = timedelta(seconds=SESSION_MAX_AGE)
db.add(RevokedSession(sid="outlived", revoked_at=now - life - timedelta(days=2)))
db.add(RevokedSession(sid="still-live", revoked_at=now - life + timedelta(hours=1)))
# Past the cookie's life but inside the day of margin: a renewal a request's time after the
# revoke may still be valid.
db.add(RevokedSession(sid="in-the-margin", revoked_at=now - life - timedelta(hours=12)))
db.commit()
revoke_session(db, SimpleNamespace(session={"user_id": 1, "sid": "this-one"}, cookies={}))
db.commit()
left = set(db.scalars(select(RevokedSession.sid)).all())
assert left == {"still-live", "this-one"}
assert left == {"still-live", "in-the-margin", "this-one"}
def _session(client) -> dict:
+23
View File
@@ -59,6 +59,29 @@ def _sign_in(client, google, state: str | None = None, **userinfo):
)
@pytest.mark.parametrize("session", ["holds", "refused"])
def test_a_cancel_at_google_lands_where_the_browser_is(client, google, make_user, session):
# Signed in, the cancel is a toast in the app; signed out, the login page's banner. A refused
# session keeps its cookie and its account id, and its browser is on the login page.
import base64
import json
from itsdangerous import TimestampSigner
make_user(email="cancel@example.com", password=_PW)
_password_login(client, "cancel@example.com")
if session == "refused":
signer = TimestampSigner(str(settings.secret_key))
payload = json.loads(base64.b64decode(signer.unsign(client.cookies["session"])))
payload.pop("sid") # a cookie from before session ids
refused = signer.sign(base64.b64encode(json.dumps(payload).encode())).decode()
client.cookies.clear()
client.cookies.set("session", refused)
r = _sign_in(client, google, state="not-this-flow")
expected = "/?oauth=cancelled" if session == "holds" else "/?login=oauth_cancelled"
assert r.headers["location"] == expected
def _rows(db, model, column, value):
db.expire_all()
return db.execute(select(model).where(column == value)).scalars().all()
+23
View File
@@ -49,3 +49,26 @@ def test_a_test_reads_the_clock_the_app_stamps_with(monkeypatch):
monkeypatch.setattr(conftest._NeverBack, "last", None)
stamped = diagnostics.datetime.now(timezone.utc)
assert datetime.now(timezone.utc) >= stamped
def test_a_clamped_reading_is_in_the_zone_asked_for(monkeypatch):
# The Pacific quota day read through a clamp came back in UTC: its date was the next day's.
from zoneinfo import ZoneInfo
import conftest
pacific = ZoneInfo("America/Los_Angeles")
utc_reading = datetime(2099, 1, 2, 3, 0, 0, tzinfo=timezone.utc) # 1 January 19:00 in Pacific
stepped_back = datetime(2099, 1, 1, 18, 59, 59, tzinfo=pacific) # a second earlier
class Stepping(datetime):
@classmethod
def now(cls, tz=None):
return utc_reading if tz is timezone.utc else stepped_back
monkeypatch.setattr(conftest._NeverBack, "clock", Stepping)
monkeypatch.setattr(conftest._NeverBack, "last", None)
conftest._NeverBack.now(timezone.utc)
clamped = conftest._NeverBack.now(pacific)
assert clamped.tzinfo is pacific
assert clamped.date().isoformat() == "2099-01-01"