diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs index 6c1685b858..78f494a4c4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/mod.rs @@ -6,3 +6,4 @@ pub mod logout_handler; pub mod password_reset_redeem_handler; pub mod password_reset_request_handler; pub mod session_info_handler; +pub mod session_token_info_handler; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/session_token_info_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/session_token_info_handler.rs new file mode 100644 index 0000000000..99b118df57 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/session_token_info_handler.rs @@ -0,0 +1,88 @@ +use std::collections::BTreeSet; +use std::fmt; +use std::sync::Arc; + +use actix_web::{HttpRequest, HttpResponse, web}; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::web::Json; +use log::warn; +use sqlx::MySqlPool; +use utoipa::ToSchema; + +use enums::by_table::users::user_feature_flag::UserFeatureFlag; +use http_server_common::response::response_error_helpers::to_simple_json_error; +use tokens::tokens::users::UserToken; + +use crate::http_server::common_responses::user_details_lite::UserDetailsLight; +use crate::http_server::cookies::anonymous_visitor_tracking::avt_cookie_manager::AvtCookieManager; +use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags; +use crate::http_server::session::session_checker::SessionChecker; +use crate::state::server_state::ServerState; + +#[derive(Serialize, ToSchema)] +pub struct SessionTokenInfoSuccessResponse { + /// A signed session that can be sent as a header, bypassing cookies. + /// This is useful for API clients that don't support cookies or Google + /// browsers killing cross-domain cookies. + pub maybe_signed_session: Option, +} + +#[derive(Debug, ToSchema)] +pub enum SessionTokenInfoError { + NotAuthorized, + ServerError, +} + +impl ResponseError for SessionTokenInfoError { + fn status_code(&self) -> StatusCode { + match *self { + SessionTokenInfoError::NotAuthorized => StatusCode::UNAUTHORIZED, + SessionTokenInfoError::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let error_reason = match self { + SessionTokenInfoError::ServerError => "server error".to_string(), + SessionTokenInfoError::NotAuthorized => "not authorized".to_string(), + }; + + to_simple_json_error(&error_reason, self.status_code()) + } +} + +// NB: Not using derive_more::Display since Clion doesn't understand it. +impl fmt::Display for SessionTokenInfoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[utoipa::path( + get, + tag = "Users", + path = "/v1/session_token", + responses( + (status = 200, description = "Success response", body = SessionTokenInfoSuccessResponse), + (status = 500, description = "Server error", body = SessionTokenInfoError), + ), +)] +pub async fn session_token_info_handler( + http_request: HttpRequest, + session_checker: web::Data, + server_state: web::Data>, +) -> Result, SessionTokenInfoError> +{ + let maybe_session_payload = server_state + .session_cookie_manager + .check_and_return_session_token_decodes(&http_request) + .map_err(|e| { + warn!("Session cookie decode error: {:?}", e); + SessionTokenInfoError::ServerError + })?; + + Ok(Json(SessionTokenInfoSuccessResponse { + maybe_signed_session: maybe_session_payload, + })) +} diff --git a/crates/service/web/storyteller_web/src/http_server/routes/user_routes.rs b/crates/service/web/storyteller_web/src/http_server/routes/user_routes.rs index 96fed6fe82..3e99a9befc 100644 --- a/crates/service/web/storyteller_web/src/http_server/routes/user_routes.rs +++ b/crates/service/web/storyteller_web/src/http_server/routes/user_routes.rs @@ -14,6 +14,7 @@ use crate::http_server::endpoints::users::logout_handler::logout_handler; use crate::http_server::endpoints::users::password_reset_redeem_handler::password_reset_redeem_handler; use crate::http_server::endpoints::users::password_reset_request_handler::password_reset_request_handler; use crate::http_server::endpoints::users::session_info_handler::session_info_handler; +use crate::http_server::endpoints::users::session_token_info_handler::session_token_info_handler; use crate::http_server::endpoints::w2l::list_user_w2l_inference_results::list_user_w2l_inference_results_handler; use crate::http_server::endpoints::w2l::list_user_w2l_templates::list_user_w2l_templates_handler; @@ -73,6 +74,11 @@ pub fn add_user_routes (app: App) -> App .route(web::get().to(session_info_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::resource("/v1/session_token") + .route(web::get().to(session_token_info_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service( web::resource("/v1/password_reset/request") .route(web::post().to(password_reset_request_handler)) diff --git a/crates/service/web/storyteller_web/src/http_server/session/http/http_user_session_manager.rs b/crates/service/web/storyteller_web/src/http_server/session/http/http_user_session_manager.rs index 174c45522e..09aab7ed1d 100644 --- a/crates/service/web/storyteller_web/src/http_server/session/http/http_user_session_manager.rs +++ b/crates/service/web/storyteller_web/src/http_server/session/http/http_user_session_manager.rs @@ -80,15 +80,7 @@ impl HttpUserSessionManager { request: &HttpRequest ) -> AnyhowResult> { - let mut signed_session_payload= request.cookie(SESSION_COOKIE_NAME) - .map(|cookie| cookie.value().to_string()); - - if signed_session_payload.is_none() { - signed_session_payload = request.headers().get(SESSION_HEADER_NAME) - .map(|header| header.to_str()) - .transpose()? - .map(|payload| payload.to_string()); - } + let signed_session_payload = self.session_payload_from_request(request)?; let signed_session_payload = match signed_session_payload { Some(payload) => payload, @@ -103,6 +95,41 @@ impl HttpUserSessionManager { Ok(payload) => Ok(Some(payload)), } } + + // NB: THIS IS ONLY FOR A QUICK HACK FOR FREAKING CORS UGH + // THIS IS A HUGE STUPID SECURITY VULN. DAMNIT GOOGLE DAMNIT CORS. + pub fn check_and_return_session_token_decodes(&self, request: &HttpRequest) -> AnyhowResult> { + let signed_session_payload = self.session_payload_from_request(request)?; + + let signed_session_payload = match signed_session_payload { + Some(payload) => payload, + None => return Ok(None), + }; + + match self.payload_signer.decode(&signed_session_payload) { + Err(e) => { + warn!("Session cookie decode error: {:?}", e); + return Err(anyhow!("Could not decode session cookie: {:?}", e)); + }, + Ok(_payload) => {}, // Good! We'll just discard this. + } + + Ok(Some(signed_session_payload)) + } + + fn session_payload_from_request(&self, request: &HttpRequest) -> AnyhowResult> { + let mut signed_session_payload= request.cookie(SESSION_COOKIE_NAME) + .map(|cookie| cookie.value().to_string()); + + if signed_session_payload.is_none() { + signed_session_payload = request.headers().get(SESSION_HEADER_NAME) + .map(|header| header.to_str()) + .transpose()? + .map(|payload| payload.to_string()); + } + + Ok(signed_session_payload) + } } #[cfg(test)]