8000 fix(auth): don't revive a session that ended during a refresh by adeelahmad · Pull Request #198 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions fastapi_backend/app/domain/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,27 @@ def refresh_session(db, cfg, raw):
if not _active(u):
return None
new = secrets.token_urlsafe(32)
db.run(
["UPDATE $old SET rotated = true, rotated_at = rotated_at OR $now", "CREATE $new CONTENT $d"],
old=R("login_session", sha(raw)),
now=store.now(),
new=R("login_session", sha(new)),
d={"account": u["id"], "sid": s["sid"], "created_at": store.now(), "expires_at": _later(cfg["server"].get("session_hours", 168))},
)
try:
# one step: a sign-out (or a password change, a passkey removed) that deleted the session since it was read
# leaves nothing to rotate, and the session stays ended instead of coming back under the new token
db.run(
[
"LET $kept = (UPDATE $old SET rotated = true, rotated_at = rotated_at OR $now RETURN id)",
"IF array::len($kept) = 0 { THROW 'the session has ended' }",
"CREATE $new CONTENT $d",
],
old=R("login_session", sha(raw)),
now=store.now(),
new=R("login_session", sha(new)),
d={
"account": u["id"],
"sid": s["sid"],
"created_at": store.now(),
"expires_at": _later(cfg["server"].get("session_hours", 168)),
},
)
except RuntimeError:
return None
return public(u), new, s["sid"]


Expand Down
19 changes: 19 additions & 0 deletions fastapi_backend/tests/api/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from unittest import mock

from app.domain import auth
from tests.helpers import login, make_user

Expand Down Expand Up @@ -110,3 +112,20 @@ def test_welcome_tour_opens_until_finished(client, db):

tok = client.post("/api/v1/tokens", json={"name": "ci", "scope": "read"}, headers=h).json()
assert client.post("/api/v1/auth/me/tour", headers={"Authorization": f"Bearer {tok['token']}"}).status_code == 403


def test_a_refresh_racing_a_sign_out_does_not_bring_the_session_back(db, cfg):
"""The session is read, then rotated: a sign-out in between must leave it ended, not revived under a new token."""
uid = auth.create_account(db, "ada@x.io", "ada password 12")
raw, sid = auth.start_session(db, cfg, uid)
real = auth.get_account

def signed_out_meanwhile(db_, account):
auth.end_session(db, raw) # another tab signs out after the refresh read the session
return real(db_, account)

with mock.patch.object(auth, "get_account", signed_out_meanwhile):
assert auth.refresh_session(db, cfg, raw) is None
assert not auth.session_active(db, sid)
raw2, _ = auth.start_session(db, cfg, uid)
assert auth.refresh_session(db, cfg, raw2) is not None # an ordinary refresh still works
Loading
0