diff options
| author | tv <tv@krebsco.de> | 2026-11-08 14:12:39 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-08 15:25:54 +0100 |
| commit | 5de2994c6d8f5f266d60b4febdf9252a035488ad (patch) | |
| tree | 47424d1b16ddb58e4d6ccd3dbd3b109369d04e26 | |
| parent | 84a137b4db8d901848df8e06dc45dfca2206dbae (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.
| -rw-r--r-- | docs/access.md | 5 | ||||
| -rw-r--r-- | docs/openapi.yaml | 2 | ||||
| -rw-r--r-- | src/api/login.rs | 35 |
3 files changed, 32 insertions, 10 deletions
diff --git a/docs/access.md b/docs/access.md index 7c576b0..a3b8864 100644 --- a/docs/access.md +++ b/docs/access.md @@ -100,8 +100,9 @@ curl -H "Authorization: Bearer ct1.…" http://other-node:3000/store * **How long**: `--token-lifetime` (12 hours), refreshed (`POST /auth/refresh`, on any node, which signs the new one) until `--session-lifetime` (7 days) after the login; then the user logs in again. A refreshed token may say less (`limit`), never more, - and live less (`lifetime`, in seconds): a dashboard opening another member's hands it - one reading only, for five minutes ([`dashboard.md`](dashboard.md)). + and live less (`lifetime`, in seconds), and then ends its session with it: nobody + refreshes it past its end. A dashboard opening another member's hands it one reading + only, for five minutes ([`dashboard.md`](dashboard.md)). * **Revoking**: a user disabled or removed is refused at once; all of a user's tokens, by the user (`POST /auth/logout`) or by one who may manage users (`DELETE /admin/users/{name}/tokens`), which sets `_continuum/logouts/<name>`: tokens issued diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 960ab14..f0d54a9 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1974,7 +1974,7 @@ components: items: { $ref: '#/components/schemas/CapabilityRequest' } lifetime: type: integer - description: Seconds the token lives, if less than `token-lifetime` (a token to hand over, say). + description: Seconds the token lives, if less than `token-lifetime` (a token to hand over, say). Its session ends with it, so it is not refreshed past its end. TokenAnswer: type: object properties: 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); + } } |
