diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 17 | ||||
| -rw-r--r-- | src/api/dashboard/login.html | 18 | ||||
| -rw-r--r-- | src/api/login.rs | 26 |
3 files changed, 52 insertions, 9 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index fa4d436..17ff44b 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -2058,7 +2058,7 @@ if (handOver) { // The fragment that opens another node's dashboard (`id`'s) as this page is now // (`handOver`). What is in focus stays, but `id` itself: its page is its own. Also this // node's own page, reloaded for a new version (`id` null). -function handOff(id) { +function handOff(id, handed) { const settings = {}; try { for (let i = 0; i < localStorage.length; i++) { @@ -2076,6 +2076,9 @@ function handOff(id) { legend: !document.getElementById("legends").hidden, figures: [...open], document: [...documentOpen], + // A token reading only, for five minutes: the other node's login page begins a + // session with it (`login.html`), where the browser has none there. + token: handed || undefined, })); } // `[x, y]` turned by `angle`. @@ -2446,11 +2449,19 @@ addEventListener("keydown", event => { // opened in this page; else its halo flashes red, larger than usual, and "no dashboard" // rises from it in the same red, both fading out. This node's own: here already. const FLASH_MS = 1200; -function openDashboard(id) { +const READ_ALL = [{ path: [], rights: ["read"] }, { path: ["_continuum"], rights: ["read"] }]; +async function openDashboard(id) { const member = state.graph.members[id]; if (!member || member.self) return; if (member.dashboard) { - location.assign(member.dashboard.split("#")[0] + handOff(id)); + let handed = null; + if (token) { + try { + handed = (await tokenCall("../auth/refresh", token.token, + { limit: READ_ALL, lifetime: 300 })).token; + } catch (_) { /* the other dashboard asks for a login then */ } + } + location.assign(member.dashboard.split("#")[0] + handOff(id, handed)); return; } const layer = document.getElementById("flashes"); diff --git a/src/api/dashboard/login.html b/src/api/dashboard/login.html index 77668ee..e09d31c 100644 --- a/src/api/dashboard/login.html +++ b/src/api/dashboard/login.html @@ -73,6 +73,24 @@ document.getElementById("token").addEventListener("submit", async event => { why.textContent = ""; try { await begin(event.target.token.value.trim()); } catch (error) { failed(error); } }); +// Opened from another member's dashboard (`handOff`), sent here for want of a session: +// its token, reading only, begins one; the rest goes on to the dashboard. +(async () => { + const HAND_OFF = "#continuum="; + if (!location.hash.startsWith(HAND_OFF)) return; + const fragment = location.hash; + try { history.replaceState(null, "", location.pathname + location.search); } catch (_) {} + let handed; + try { handed = JSON.parse(decodeURIComponent(fragment.slice(HAND_OFF.length))); } catch (_) { return; } + if (!handed || typeof handed.token !== "string") return; + try { + await call("auth/session", "POST", "Bearer " + handed.token); + delete handed.token; + location.replace("admin/dashboard" + HAND_OFF + encodeURIComponent(JSON.stringify(handed))); + } catch (error) { + why.textContent = "The token handed over isn't taken (" + error.message + "): log in."; + } +})(); document.getElementById("out").addEventListener("click", async () => { await fetch("auth/session", { method: "DELETE", credentials: "same-origin" }).catch(() => {}); why.textContent = "Logged out of this browser."; 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() |
