use super::error::{Error, Result}; use axum::Extension; use axum::{ extract::FromRequestParts, http::request::Parts, response::{IntoResponse, Redirect, Response}, }; use url::form_urlencoded; pub struct KeycloakVariables { pub base_url: String, pub realm: String, pub client_id: String, pub client_secret: String, pub enable_test_user: bool, } /// Session data available to the backend when the user is logged in #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct LoggedInData { pub id: String, // ID Token value associated with the authenticated session. pub token_id: String, pub username: String, pub avatar_url: Option, } /// Used for extracting in the server functions. /// If the `data` is `Some`, the user is logged in. pub struct UserSession { data: Option, } impl UserSession { /// Get the [`LoggedInData`]. /// /// Raises a [`Error::UserNotLoggedIn`] error if the user is not logged in. pub fn data(self) -> Result { self.data.ok_or(Error::UserNotLoggedIn) } } const LOGGED_IN_USER_SESSION_KEY: &str = "logged_in_data"; impl FromRequestParts for UserSession { type Rejection = Error; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let session = parts .extensions .get::() .cloned() .ok_or(Error::AuthSessionLayerNotFound( "Auth Session Layer not found".to_string(), ))?; let data: Option = session .get::(LOGGED_IN_USER_SESSION_KEY) .await?; Ok(Self { data }) } } /// Helper function to log the user in by setting the session data pub async fn login(session: &tower_sessions::Session, data: &LoggedInData) -> Result<()> { session.insert(LOGGED_IN_USER_SESSION_KEY, data).await?; Ok(()) } /// Handler to run when the user wants to logout #[axum::debug_handler] pub async fn logout( state: Extension, session: tower_sessions::Session, ) -> Result { let dashboard_base_url = "http://localhost:8000"; let redirect_uri = format!("{dashboard_base_url}/"); let encoded_redirect_uri: String = form_urlencoded::byte_serialize(redirect_uri.as_bytes()).collect(); // clear the session value for this session if let Some(login_data) = session .remove::(LOGGED_IN_USER_SESSION_KEY) .await? { let kc_base_url = &state.keycloak_variables.base_url; let kc_realm = &state.keycloak_variables.realm; let kc_client_id = &state.keycloak_variables.client_id; // Needed for running locally. // This will not panic on production and it will return the original so we can keep it let routed_kc_base_url = kc_base_url.replace("keycloak", "localhost"); let token_id = login_data.token_id; // redirect to Keycloak logout endpoint let logout_url = format!( "{routed_kc_base_url}/realms/{kc_realm}/protocol/openid-connect/logout\ ?post_logout_redirect_uri={encoded_redirect_uri}\ &client_id={kc_client_id}\ &id_token_hint={token_id}" ); Ok(Redirect::to(&logout_url).into_response()) } else { // No id_token in session; just redirect to homepage Ok(Redirect::to(&redirect_uri).into_response()) } }