From 5de2994c6d8f5f266d60b4febdf9252a035488ad Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 14:12:39 +0000 Subject: login: a token asked to live less ends its session with it Review finding 8, first part: a refreshed token's lifetime came from the refresh request, and it kept the session's end. So the five-minute read-all token a dashboard hands to another member's refreshed into a 12-hour one, renewable for seven days. A token asked to live less (`lifetime`, at login or refresh) now ends its session with it: nobody refreshes it past its end, its holder included, and a session begun from it (POST /auth/session) ends then too. --- src/api/login.rs | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/api/login.rs b/src/api/login.rs index 3b4ee32..31a5d7f 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -248,6 +248,9 @@ fn new_login( lifetime: Option, ) -> Claims { let session_exp = now.saturating_add(state.session_lifetime.as_millis() as u64); + let exp = now + .saturating_add(lifetime_ms(state, lifetime)) + .min(session_exp); Claims { v: 1, swarm, @@ -255,15 +258,23 @@ fn new_login( user: user.to_owned(), login: login.to_owned(), iat: now, - exp: now - .saturating_add(lifetime_ms(state, lifetime)) - .min(session_exp), + exp, session: token::new_session(), - session_exp, + session_exp: ended_with(session_exp, exp, lifetime), limit, } } +/// When a session ends, `session_exp` at the latest, for a token ending at `exp`: with +/// it, where it was asked to live less (`lifetime`). A token to hand over is refreshed +/// past its end by nobody, its holder included. +fn ended_with(session_exp: u64, exp: u64, lifetime: Option) -> u64 { + match lifetime { + Some(_) => session_exp.min(exp), + None => session_exp, + } +} + /// `POST /auth/challenge`: one to sign for a login by key, valid for five minutes. pub(crate) async fn challenge(State(state): State) -> Result { let (_, now) = issuing(&state).await?; @@ -426,14 +437,16 @@ async fn refreshed( Some(limit) } }; + let exp = now + .saturating_add(lifetime_ms(state, lifetime)) + .min(earlier.session_exp); let claims = Claims { v: 1, swarm, issuer: state.local_id().to_owned(), iat: now, - exp: now - .saturating_add(lifetime_ms(state, lifetime)) - .min(earlier.session_exp), + exp, + session_exp: ended_with(earlier.session_exp, exp, lifetime), limit, ..(**earlier).clone() }; @@ -534,4 +547,12 @@ mod tests { assert!(!within(&[cap(&["logs"], &["read"])], &earlier)); assert!(!within(&[cap(&["notes"], &["read", "grant"])], &earlier)); } + + /// A token asked to live less ends its session with it; another, the session goes on. + #[test] + fn a_short_token_ends_its_session() { + assert_eq!(ended_with(7_000, 300, Some(300)), 300); + assert_eq!(ended_with(7_000, 300, None), 7_000); + assert_eq!(ended_with(200, 300, Some(300)), 200); + } } -- cgit v1.3.1