aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 12:25:09 +0000
committertv <tv@krebsco.de>2026-11-09 13:32:05 +0100
commitd12ccc782d8e6eab46eb6e0976b747dca1013b36 (patch)
treede9d5ae9135a182dbb19a6067d240054483561d4 /src/api
parentb0e711f9f7e0d2d9f6b39dd987004d2f9b1dcda7 (diff)
access: ending a single session
Logging out of the dashboard (`DELETE /auth/session`) only cleared the cookie: the session's tokens stayed taken until they expired, by every node. Ending them took `POST /auth/logout`, which ends all of a user's sessions. Now `DELETE /auth/session` ends the session of the token it comes with (the cookie, or a bearer token): where the swarm has a registry, it is recorded as `_continuum/sessions/<user>/<session>` (`registry::session_path`), its value until when the session would have lasted, and every node refuses the session's tokens once the change reaches it (`token::Refused::Ended`): the page's own, its narrow tokens, and one handed to another member's dashboard, all refreshed from it and keeping its ID. The rules take the entry from the user itself or one who manages users, as a logout. A remake's snapshot leaves out entries past that time, a user's map where none is left: their tokens are expired anyway. Without a registry the document can't record it: the node ending it refuses the session's tokens itself (`AppState::ended_sessions`, until it restarts), others take them until they expire, as a logout there is refused.
Diffstat (limited to 'src/api')
-rw-r--r--src/api/login.rs62
1 files changed, 56 insertions, 6 deletions
diff --git a/src/api/login.rs b/src/api/login.rs
index cc70c18..ec0c9cf 100644
--- a/src/api/login.rs
+++ b/src/api/login.rs
@@ -113,7 +113,19 @@ pub(crate) async fn bearer(
let verified = {
let replica = state.doc.read().await;
token::verify(&replica.doc, &text, &swarm, now)
- };
+ }
+ .and_then(|claims| {
+ // Ended here where the document can't record it (no registry).
+ let ended = state
+ .ended_sessions
+ .lock_or_recover()
+ .contains_key(&claims.session);
+ if ended {
+ Err(token::Refused::Ended)
+ } else {
+ Ok(claims)
+ }
+ });
match verified {
Ok(claims) => {
request
@@ -419,17 +431,55 @@ fn over_tls(headers: &axum::http::HeaderMap) -> bool {
})
}
-/// `DELETE /auth/session`: the session cookie forgotten (the token itself stays valid
-/// until it expires; `POST /auth/logout` ends every one). A stale one is ignored anyway.
-pub(crate) async fn end_session() -> Response {
- (
+/// `DELETE /auth/session`: the session ended, with the token it comes with (the cookie,
+/// or `Authorization: Bearer`): recorded in the document (`registry::session_path`), so
+/// no node takes its tokens any more, where the swarm has a registry; else on this node
+/// only (`AppState::ended_sessions`), others taking them until they expire. And the
+/// session cookie forgotten. Without a token, only that.
+pub(crate) async fn end_session(
+ State(state): State<AppState>,
+ token: Option<Extension<Token>>,
+) -> Result<Response, ApiError> {
+ if let Some(Extension(token)) = token {
+ end(&state, &token.claims, &token.claims.user).await?;
+ }
+ Ok((
StatusCode::NO_CONTENT,
[(
axum::http::header::SET_COOKIE,
format!("{SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0"),
)],
)
- .into_response()
+ .into_response())
+}
+
+/// Ends the session of `claims`, as `by`: recorded in the document where the swarm has a
+/// registry, else on this node only.
+async fn end(state: &AppState, claims: &Claims, by: &str) -> Result<(), ApiError> {
+ let path = registry::session_path(&claims.user, &claims.session);
+ let until = claims.session_exp;
+ let recorded = if state.is_ready().await {
+ state
+ .mutate_doc_for(Some(by.to_owned()), Sign::Protocol, move |doc| {
+ if !registry::exists(&*doc) {
+ return Ok::<_, ApiError>(false);
+ }
+ crate::document::store::write_path(doc, &path, &json!(until))?;
+ Ok(true)
+ })
+ .await?
+ .0
+ } else {
+ false
+ };
+ if !recorded {
+ let now = state.clock.now_millis().as_u64();
+ let mut ended = state.ended_sessions.lock_or_recover();
+ ended.retain(|_, until| *until > now);
+ ended.insert(claims.session.clone(), until);
+ }
+ info!(user = %claims.user, session = %claims.session, by = %by, recorded, "Ended a session");
+ Ok(())
}
/// The claims a refresh of `token` gives (`limit` within its own).