aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/login.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:22 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:22 +0100
commit56a3dca1874fba17c3d174be451af9478b3bfb9d (patch)
tree819e3a1d4902c6847b429dd69269ef475d067a45 /src/api/login.rs
parent7323e8ed8598cda9784493aa3334d947120b53d8 (diff)
dashboard: a token handed over to another member's dashboard
Opening another member's dashboard passes a token reading only, for five minutes, in the URL's fragment; its login page begins a session with it. Tokens may be asked for with a shorter lifetime. Auth plan M3 (hand-off).
Diffstat (limited to 'src/api/login.rs')
-rw-r--r--src/api/login.rs26
1 files changed, 20 insertions, 6 deletions
diff --git a/src/api/login.rs b/src/api/login.rs
index 1907bac..3b4ee32 100644
--- a/src/api/login.rs
+++ b/src/api/login.rs
@@ -146,6 +146,9 @@ pub(crate) async fn bearer(
pub(crate) struct LimitRequest {
#[serde(default)]
limit: Option<Vec<CapRequest>>,
+ /// A shorter life than `--token-lifetime`, in seconds: a token to hand over, say.
+ #[serde(default)]
+ lifetime: Option<u64>,
}
impl LimitRequest {
@@ -209,7 +212,8 @@ pub(crate) async fn login(
Some("log in with a password or through the proxy; a token is refreshed at /auth/refresh".to_owned()),
));
}
- let limit = body.map(|Json(body)| body).unwrap_or_default().limit()?;
+ let body = body.map(|Json(body)| body).unwrap_or_default();
+ let limit = body.limit()?;
let (swarm, now) = issuing(&state).await?;
{
let replica = state.doc.read().await;
@@ -222,11 +226,17 @@ pub(crate) async fn login(
));
}
}
- let claims = new_login(&state, swarm, now, &user.0, via.0, limit);
+ let claims = new_login(&state, swarm, now, &user.0, via.0, limit, body.lifetime);
info!(user = %user.0, login = via.0, session = %claims.session, "Logged in");
answer(&state, &claims)
}
+/// How long a token lives: `--token-lifetime`, or less if `asked` (seconds) is less.
+fn lifetime_ms(state: &AppState, asked: Option<u64>) -> u64 {
+ let lifetime = state.token_lifetime.as_millis() as u64;
+ asked.map_or(lifetime, |secs| secs.saturating_mul(1000).min(lifetime))
+}
+
/// The claims of a new login of `user`, by `login`, now: a session of its own.
fn new_login(
state: &AppState,
@@ -235,6 +245,7 @@ fn new_login(
user: &str,
login: &str,
limit: Option<Vec<Capability>>,
+ lifetime: Option<u64>,
) -> Claims {
let session_exp = now.saturating_add(state.session_lifetime.as_millis() as u64);
Claims {
@@ -245,7 +256,7 @@ fn new_login(
login: login.to_owned(),
iat: now,
exp: now
- .saturating_add(state.token_lifetime.as_millis() as u64)
+ .saturating_add(lifetime_ms(state, lifetime))
.min(session_exp),
session: token::new_session(),
session_exp,
@@ -292,6 +303,7 @@ pub(crate) async fn login_key(
) -> Result<Response, ApiError> {
let limit = LimitRequest {
limit: request.limit,
+ lifetime: None,
}
.limit()?;
let (swarm, now) = issuing(&state).await?;
@@ -322,7 +334,7 @@ pub(crate) async fn login_key(
ApiError(StatusCode::UNAUTHORIZED, Some(why))
})?;
}
- let claims = new_login(&state, swarm, now, &request.user, "key", limit);
+ let claims = new_login(&state, swarm, now, &request.user, "key", limit, None);
info!(user = %request.user, login = "key", session = %claims.session, "Logged in");
answer(&state, &claims)
}
@@ -396,7 +408,9 @@ async fn refreshed(
Some("the login ended: log in again".to_owned()),
));
}
- let limit = match body.map(|Json(body)| body).unwrap_or_default().limit()? {
+ let body = body.map(|Json(body)| body).unwrap_or_default();
+ let lifetime = body.lifetime;
+ let limit = match body.limit()? {
None => earlier.limit.clone(),
Some(limit) => {
if earlier
@@ -418,7 +432,7 @@ async fn refreshed(
issuer: state.local_id().to_owned(),
iat: now,
exp: now
- .saturating_add(state.token_lifetime.as_millis() as u64)
+ .saturating_add(lifetime_ms(state, lifetime))
.min(earlier.session_exp),
limit,
..(**earlier).clone()