Add logout functionality

This commit is contained in:
Olivier 'reivilibre 2025-06-12 22:32:05 +01:00
parent f25f42a830
commit e5179782e3
7 changed files with 155 additions and 3 deletions

View File

@ -356,6 +356,21 @@ impl<'a, 'txn> IdCoopStoreTxn<'a, 'txn> {
}))
}
/// Given the login session's ID, destroy the session.
pub async fn destroy_login_session(&mut self, login_session_id: i64) -> eyre::Result<()> {
sqlx::query!(
"
DELETE FROM login_sessions
WHERE login_session_id = $1
",
login_session_id
)
.execute(&mut **self.txn)
.await
.context("failed to destroy login session")?;
Ok(())
}
/// Given an access token's hash, looks up the corresponding application session.
pub async fn lookup_application_session(
&mut self,

View File

@ -51,9 +51,12 @@ use crate::{
},
};
pub mod ambient;
use self::logout::{get_logout, post_logout};
pub(crate) mod ambient;
pub mod errors;
pub mod login;
pub mod logout;
pub mod oauth_openid;
pub mod sessionless_xsrf;
@ -150,6 +153,17 @@ pub(crate) async fn make_router(
HeaderValue::from_static("DENY"),
)),
)
.route(
// CORS NOT to be made available on this endpoint!
// Block loading this in a frame!
"/logout",
get(get_logout)
.post(post_logout)
.layer(SetResponseHeaderLayer::overriding(
HeaderName::from_static("x-frame-options"),
HeaderValue::from_static("DENY"),
)),
)
.route(
"/oidc/token",
post(oidc_token).layer(CorsLayer::permissive().max_age(Duration::from_secs(600))),

View File

@ -135,6 +135,8 @@ pub const LOGIN_SESSION_TOKEN_HASH_BYTES: usize = 32;
/// e.g. perhaps the persona should be a hash of the user's UUID?
pub const LOGIN_SESSION_XSRF_SECRET_BYTES: usize = 32;
pub const LOGIN_SESSION_COOKIE_NAME: &str = "__Host-LoginSession";
/// Represents a login session, which is effectively just a 'web UI' session for a user.
pub struct LoginSession {
/// The system name of the user who is logged in to the idCoop web UI
@ -231,7 +233,7 @@ where
else {
return Err((StatusCode::UNAUTHORIZED, "No login session."));
};
let Some(cookie_val) = cookies.get("__Host-LoginSession").map(str::to_owned) else {
let Some(cookie_val) = cookies.get(LOGIN_SESSION_COOKIE_NAME).map(str::to_owned) else {
return Err((StatusCode::UNAUTHORIZED, "No login session."));
};
let Ok(login_session_token) = BASE64_URL_SAFE_NO_PAD.decode(&cookie_val) else {
@ -496,7 +498,7 @@ pub async fn post_login(
.context("failed to store session in database")?;
cookies.add(
Cookie::build(("__Host-LoginSession", login_session_token_b64.clone()))
Cookie::build((LOGIN_SESSION_COOKIE_NAME, login_session_token_b64.clone()))
.path("/")
.http_only(true)
.secure(true)

88
src/web/logout.rs Normal file
View File

@ -0,0 +1,88 @@
//! logout: let users log out
use std::sync::Arc;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Extension,
};
use hornbeam::render_template_string;
use tower_cookies::{Cookie, Cookies};
use crate::store::IdCoopStore;
use super::{
ambient::Ambient,
login::{LoginSession, LOGIN_SESSION_COOKIE_NAME},
sessionless_xsrf, DesiredLocale, Rendered, WebResult, TEMPLATING,
};
/// `GET /logout`
///
/// If logged in, show a button to send a POST request to log out.
///
/// If not logged in, shows a success message.
pub async fn get_logout(ambient: Ambient, DesiredLocale(locale): DesiredLocale) -> Response {
if ambient.user.is_some() {
// display logout button
Rendered(render_template_string!(TEMPLATING, logout_ask, locale, {
ambient
}))
.into_response()
} else {
// display success message
Rendered(render_template_string!(
TEMPLATING,
logout_success,
locale,
{ ambient }
))
.into_response()
}
}
/// `POST /logout`
///
/// If logged in, destroy session and show success message.
///
/// If not logged in, show success message.
///
/// We don't care about any specific form data.
///
/// We don't require an XSRF secret, because SameSite=Lax will protect us
/// in modern browsers.
/// In other browsers, being logged out is not a huge deal; it doesn't seem to be
/// a particularly interesting risk or attack vector, therefore.
pub async fn post_logout(
current_session: Result<LoginSession, (StatusCode, &'static str)>,
mut ambient: Ambient,
cookies: Cookies,
Extension(store): Extension<Arc<IdCoopStore>>,
DesiredLocale(locale): DesiredLocale,
) -> WebResult<Response> {
if let Ok(session) = current_session {
store
.txn(|mut txn| {
Box::pin(async move { txn.destroy_login_session(session.login_session_id).await })
})
.await?;
// Clear out cookies
for cookie_name in &[LOGIN_SESSION_COOKIE_NAME, sessionless_xsrf::COOKIE_NAME] {
if let Some(cookie) = cookies.get(cookie_name).map(Cookie::into_owned) {
cookies.remove(cookie);
}
}
ambient.user = None;
}
Ok(Rendered(render_template_string!(
TEMPLATING,
logout_success,
locale,
{ ambient }
))
.into_response())
}

View File

@ -0,0 +1,15 @@
CentredPage {$ambient}
:title
@logout_ask_title
:main
h1
@logout_ask_title
form {method = "POST"}
article
@logout_ask_main
fieldset.grid
button {type = "submit"}
@logout_ask_logout

View File

@ -0,0 +1,10 @@
CentredPage {$ambient}
:title
@logout_success_title
:main
h1
@logout_success_title
article
@logout_success_main

View File

@ -29,3 +29,11 @@ consent_deny = Deny
#consent_warn_info = Your username, user ID, display name and avatar will be sent to the service.
consent_warn_info = Your username and user ID will be sent to the service.
logout_ask_title = Log out?
logout_ask_logout = Confirm log out
logout_ask_main = Would you like to log out?
logout_success_title = Logged out!
logout_success_main = Successfully logged out. See you again soon!