From 62b2f880a1a209e396d3adaef6a38395298011c6 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 15:29:06 +0000 Subject: access: an ended session recorded until the login's end Review 4, F7: `DELETE /auth/session` recorded the session as ended until the `session_exp` of the token it came with. A token handed to another member's dashboard has its `session_exp` cut to its own end (five minutes), and so has the session that dashboard keeps with it: logging out there recorded an end minutes away, the next remake's snapshot left it out, and the original browser's tokens of the same session, refreshable for days, were taken again. A token now keeps the login's end as it logged in (`Claims::login_exp`), which no refresh changes, and an ended session is recorded until then (or the token's `session_exp`, if later, as for a token from before the field). --- src/access/token.rs | 10 +++++++++- src/api/login.rs | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/access/token.rs b/src/access/token.rs index ae1546c..a9fc270 100644 --- a/src/access/token.rs +++ b/src/access/token.rs @@ -46,8 +46,15 @@ pub(crate) struct Claims { pub(crate) exp: u64, /// The login it comes from, kept by refreshed tokens (16 hex digits). pub(crate) session: String, - /// Until when it may be refreshed (Unix ms): the login's end. + /// Until when it may be refreshed (Unix ms): the login's end, or sooner for one that + /// was given a lifetime of its own (it ends its session with it). pub(crate) session_exp: u64, + /// The login's end (Unix ms), as it logged in: what no refresh changes, a token cut + /// short included. An ended session is recorded until then (`registry::session_path`), + /// so it covers every token of the session, wherever it ends. 0 in a token from + /// before it was kept: `session_exp` then. + #[serde(default)] + pub(crate) login_exp: u64, /// Less than the user may do: only what these allow too. #[serde(default, skip_serializing_if = "Option::is_none")] pub(crate) limit: Option>, @@ -206,6 +213,7 @@ mod tests { exp: 2_000, session: new_session(), session_exp: 10_000, + login_exp: 10_000, limit: None, } } diff --git a/src/api/login.rs b/src/api/login.rs index 57c1859..dc9d895 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -272,6 +272,7 @@ fn new_login( exp, session: token::new_session(), session_exp: ended_with(session_exp, exp, lifetime), + login_exp: session_exp, limit, } } @@ -532,7 +533,10 @@ pub(crate) async fn end_session( /// registry, else on this node only. async fn end(state: &AppState, claims: &Claims, by: &str) -> Result<(), ApiError> { let path = registry::session_path(&claims.user, &claims.session); - let until = claims.session_exp; + // The login's end, not this token's refresh deadline (a token handed to another + // member's dashboard ends its session in minutes): every token of the session is + // refused until it. + let until = claims.login_exp.max(claims.session_exp); let recorded = if state.is_ready().await { state .mutate_doc_for(Some(by.to_owned()), Sign::Protocol, move |doc| { -- cgit v1.3.1