aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-08 14:12:39 +0000
committertv <tv@krebsco.de>2026-11-08 15:25:54 +0100
commit5de2994c6d8f5f266d60b4febdf9252a035488ad (patch)
tree47424d1b16ddb58e4d6ccd3dbd3b109369d04e26 /src
parent84a137b4db8d901848df8e06dc45dfca2206dbae (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/api/login.rs35
1 files changed, 28 insertions, 7 deletions
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<u64>,
) -> 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>) -> 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<AppState>) -> Result<Response, ApiError> {
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);
+ }
}