feat(users): Implemented Set-Cookie (#3865)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Rachit Naithani
2024-03-11 12:05:18 +05:30
committed by GitHub
parent e1b894770f
commit 44eef46e5d
11 changed files with 198 additions and 66 deletions

21
Cargo.lock generated
View File

@ -224,7 +224,7 @@ dependencies = [
"bytes 1.5.0", "bytes 1.5.0",
"bytestring", "bytestring",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cookie", "cookie 0.16.2",
"derive_more", "derive_more",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -616,7 +616,7 @@ dependencies = [
"base64 0.21.5", "base64 0.21.5",
"bytes 1.5.0", "bytes 1.5.0",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"cookie", "cookie 0.16.2",
"derive_more", "derive_more",
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -1763,6 +1763,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "cookie"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
dependencies = [
"time",
"version_check",
]
[[package]] [[package]]
name = "cookie-factory" name = "cookie-factory"
version = "0.3.2" version = "0.3.2"
@ -2524,7 +2534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64" checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"cookie", "cookie 0.16.2",
"futures-core", "futures-core",
"futures-util", "futures-util",
"http", "http",
@ -5219,6 +5229,7 @@ dependencies = [
"common_enums", "common_enums",
"common_utils", "common_utils",
"config", "config",
"cookie 0.18.0",
"currency_conversion", "currency_conversion",
"data_models", "data_models",
"derive_deref", "derive_deref",
@ -6414,7 +6425,7 @@ dependencies = [
"async-trait", "async-trait",
"base64 0.13.1", "base64 0.13.1",
"chrono", "chrono",
"cookie", "cookie 0.16.2",
"fantoccini", "fantoccini",
"futures 0.3.28", "futures 0.3.28",
"http", "http",
@ -7461,7 +7472,7 @@ checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bytes 1.5.0", "bytes 1.5.0",
"cookie", "cookie 0.16.2",
"http", "http",
"log", "log",
"serde", "serde",

View File

@ -58,6 +58,24 @@ impl<T: Eq + PartialEq + Clone> Maskable<T> {
pub fn new_normal(item: T) -> Self { pub fn new_normal(item: T) -> Self {
Self::Normal(item) Self::Normal(item)
} }
///
/// Checks whether the data is masked.
/// Returns `true` if the data is wrapped in the `Masked` variant,
/// returns `false` otherwise.
///
pub fn is_masked(&self) -> bool {
matches!(self, Self::Masked(_))
}
///
/// Checks whether the data is normal (not masked).
/// Returns `true` if the data is wrapped in the `Normal` variant,
/// returns `false` otherwise.
///
pub fn is_normal(&self) -> bool {
matches!(self, Self::Normal(_))
}
} }
/// Trait for providing a method on custom types for constructing `Maskable` /// Trait for providing a method on custom types for constructing `Maskable`

View File

@ -46,6 +46,7 @@ blake3 = "1.3.3"
bytes = "1.4.0" bytes = "1.4.0"
clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] }
config = { version = "0.13.3", features = ["toml"] } config = { version = "0.13.3", features = ["toml"] }
cookie = "0.18.0"
diesel = { version = "2.1.0", features = ["postgres"] } diesel = { version = "2.1.0", features = ["postgres"] }
digest = "0.9" digest = "0.9"
dyn-clone = "1.0.11" dyn-clone = "1.0.11"

View File

@ -68,6 +68,8 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes
pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
pub const JWT_TOKEN_COOKIE_NAME: &str = "login_token";
pub const USER_BLACKLIST_PREFIX: &str = "BU_"; pub const USER_BLACKLIST_PREFIX: &str = "BU_";
pub const ROLE_BLACKLIST_PREFIX: &str = "BR_"; pub const ROLE_BLACKLIST_PREFIX: &str = "BR_";

View File

@ -5,6 +5,7 @@ use common_enums::RequestIncrementalAuthorization;
use common_utils::{consts::X_HS_LATENCY, fp_utils}; use common_utils::{consts::X_HS_LATENCY, fp_utils};
use diesel_models::ephemeral_key; use diesel_models::ephemeral_key;
use error_stack::{report, IntoReport, ResultExt}; use error_stack::{report, IntoReport, ResultExt};
use masking::Maskable;
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentData}; use super::{flows::Feature, PaymentData};
@ -466,20 +467,25 @@ where
.map(|status_code| { .map(|status_code| {
vec![( vec![(
"connector_http_status_code".to_string(), "connector_http_status_code".to_string(),
status_code.to_string(), Maskable::new_normal(status_code.to_string()),
)] )]
}) })
.unwrap_or_default(); .unwrap_or_default();
if let Some(payment_confirm_source) = payment_intent.payment_confirm_source { if let Some(payment_confirm_source) = payment_intent.payment_confirm_source {
headers.push(( headers.push((
"payment_confirm_source".to_string(), "payment_confirm_source".to_string(),
payment_confirm_source.to_string(), Maskable::new_normal(payment_confirm_source.to_string()),
)) ))
} }
headers.extend( headers.extend(
external_latency external_latency
.map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())]) .map(|latency| {
vec![(
X_HS_LATENCY.to_string(),
Maskable::new_normal(latency.to_string()),
)]
})
.unwrap_or_default(), .unwrap_or_default(),
); );

View File

@ -93,12 +93,13 @@ pub async fn signup(
UserStatus::Active, UserStatus::Active,
) )
.await?; .await?;
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
Ok(ApplicationResponse::Json( let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, let response =
)) utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
auth::cookies::set_cookie_response(response, token)
} }
pub async fn signin_without_invite_checks( pub async fn signin_without_invite_checks(
@ -121,12 +122,12 @@ pub async fn signin_without_invite_checks(
user_from_db.compare_password(request.password)?; user_from_db.compare_password(request.password)?;
let user_role = user_from_db.get_role_from_db(state.clone()).await?; let user_role = user_from_db.get_role_from_db(state.clone()).await?;
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
Ok(ApplicationResponse::Json( let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, let response =
)) utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
auth::cookies::set_cookie_response(response, token)
} }
pub async fn signin( pub async fn signin(
@ -168,9 +169,9 @@ pub async fn signin(
.await? .await?
}; };
Ok(ApplicationResponse::Json( let response = signin_strategy.get_signin_response(&state).await?;
signin_strategy.get_signin_response(&state).await?, let token = utils::user::get_token_from_signin_response(&response);
)) auth::cookies::set_cookie_response(response, token)
} }
#[cfg(feature = "email")] #[cfg(feature = "email")]
@ -271,7 +272,7 @@ pub async fn connect_account(
pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> { pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> {
auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?; auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?;
Ok(ApplicationResponse::StatusOk) auth::cookies::remove_cookie_response()
} }
pub async fn change_password( pub async fn change_password(
@ -971,14 +972,14 @@ pub async fn accept_invite_from_email(
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &update_status_result) utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &update_status_result)
.await; .await;
Ok(ApplicationResponse::Json( let response = utils::user::get_dashboard_entry_response(
utils::user::get_dashboard_entry_response(
&state, &state,
user_from_db, user_from_db,
update_status_result, update_status_result,
token, token.clone(),
)?, )?;
))
auth::cookies::set_cookie_response(response, token)
} }
pub async fn create_internal_user( pub async fn create_internal_user(
@ -1130,17 +1131,17 @@ pub async fn switch_merchant_id(
(token, user_role.role_id.clone()) (token, user_role.role_id.clone())
}; };
Ok(ApplicationResponse::Json( let response = user_api::DashboardEntryResponse {
user_api::DashboardEntryResponse { token: token.clone(),
token,
name: user.get_name(), name: user.get_name(),
email: user.get_email(), email: user.get_email(),
user_id: user.get_user_id().to_string(), user_id: user.get_user_id().to_string(),
verification_days_left: None, verification_days_left: None,
user_role: role_id, user_role: role_id,
merchant_id: request.merchant_id, merchant_id: request.merchant_id,
}, };
))
auth::cookies::set_cookie_response(response, token)
} }
pub async fn create_merchant_account( pub async fn create_merchant_account(
@ -1318,9 +1319,10 @@ pub async fn verify_email_without_invite_checks(
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?; let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
Ok(ApplicationResponse::Json( let response =
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
))
auth::cookies::set_cookie_response(response, token)
} }
#[cfg(feature = "email")] #[cfg(feature = "email")]
@ -1373,9 +1375,9 @@ pub async fn verify_email(
.await .await
.map_err(|e| logger::error!(?e)); .map_err(|e| logger::error!(?e));
Ok(ApplicationResponse::Json( let response = signin_strategy.get_signin_response(&state).await?;
signin_strategy.get_signin_response(&state).await?, let token = utils::user::get_token_from_signin_response(&response);
)) auth::cookies::set_cookie_response(response, token)
} }
#[cfg(feature = "email")] #[cfg(feature = "email")]

View File

@ -158,12 +158,13 @@ pub async fn transfer_org_ownership(
.await .await
.to_not_found_response(UserErrors::InvalidRoleOperation)?; .to_not_found_response(UserErrors::InvalidRoleOperation)?;
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
Ok(ApplicationResponse::Json( let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?, let response =
)) utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
auth::cookies::set_cookie_response(response, token)
} }
pub async fn accept_invitation( pub async fn accept_invitation(
@ -202,12 +203,17 @@ pub async fn accept_invitation(
.change_context(UserErrors::InternalServerError)? .change_context(UserErrors::InternalServerError)?
.into(); .into();
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await; utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
return Ok(ApplicationResponse::Json( let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
)); let response = utils::user::get_dashboard_entry_response(
&state,
user_from_db,
user_role,
token.clone(),
)?;
return auth::cookies::set_cookie_response(response, token);
} }
Ok(ApplicationResponse::StatusOk) Ok(ApplicationResponse::StatusOk)

View File

@ -9,7 +9,10 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError}; use actix_web::{
body, http::header::HeaderValue, web, FromRequest, HttpRequest, HttpResponse, Responder,
ResponseError,
};
use api_models::enums::{CaptureMethod, PaymentMethodType}; use api_models::enums::{CaptureMethod, PaymentMethodType};
pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient};
use common_enums::Currency; use common_enums::Currency;
@ -20,7 +23,7 @@ use common_utils::{
request::RequestContent, request::RequestContent,
}; };
use error_stack::{report, IntoReport, Report, ResultExt}; use error_stack::{report, IntoReport, Report, ResultExt};
use masking::{PeekInterface, Secret}; use masking::{Maskable, PeekInterface, Secret};
use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag};
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
@ -110,7 +113,7 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
&self, &self,
_req: &types::RouterData<T, Req, Resp>, _req: &types::RouterData<T, Req, Resp>,
_connectors: &Connectors, _connectors: &Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> { ) -> CustomResult<Vec<(String, Maskable<String>)>, errors::ConnectorError> {
Ok(vec![]) Ok(vec![])
} }
@ -847,7 +850,7 @@ pub enum ApplicationResponse<R> {
Form(Box<RedirectionFormData>), Form(Box<RedirectionFormData>),
PaymentLinkForm(Box<PaymentLinkAction>), PaymentLinkForm(Box<PaymentLinkAction>),
FileData((Vec<u8>, mime::Mime)), FileData((Vec<u8>, mime::Mime)),
JsonWithHeaders((R, Vec<(String, String)>)), JsonWithHeaders((R, Vec<(String, Maskable<String>)>)),
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@ -1046,7 +1049,7 @@ where
); );
if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) { if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) {
if let Ok(external_latency) = value.parse::<u128>() { if let Ok(external_latency) = value.clone().into_inner().parse::<u128>() {
overhead_latency.replace(external_latency); overhead_latency.replace(external_latency);
} }
} }
@ -1126,7 +1129,7 @@ where
let incoming_request_header = request.headers(); let incoming_request_header = request.headers();
let incoming_header_to_log: HashMap<String, http::header::HeaderValue> = let incoming_header_to_log: HashMap<String, HeaderValue> =
incoming_request_header incoming_request_header
.iter() .iter()
.fold(HashMap::new(), |mut acc, (key, value)| { .fold(HashMap::new(), |mut acc, (key, value)| {
@ -1333,21 +1336,33 @@ pub fn http_server_error_json_response<T: body::MessageBody + 'static>(
pub fn http_response_json_with_headers<T: body::MessageBody + 'static>( pub fn http_response_json_with_headers<T: body::MessageBody + 'static>(
response: T, response: T,
mut headers: Vec<(String, String)>, headers: Vec<(String, Maskable<String>)>,
request_duration: Option<Duration>, request_duration: Option<Duration>,
) -> HttpResponse { ) -> HttpResponse {
let mut response_builder = HttpResponse::Ok(); let mut response_builder = HttpResponse::Ok();
for (header_name, header_value) in headers {
for (name, value) in headers.iter_mut() { let is_sensitive_header = header_value.is_masked();
if name == X_HS_LATENCY { let mut header_value = header_value.into_inner();
if header_name == X_HS_LATENCY {
if let Some(request_duration) = request_duration { if let Some(request_duration) = request_duration {
if let Ok(external_latency) = value.parse::<u128>() { if let Ok(external_latency) = header_value.parse::<u128>() {
let updated_duration = request_duration.as_millis() - external_latency; let updated_duration = request_duration.as_millis() - external_latency;
*value = updated_duration.to_string(); header_value = updated_duration.to_string();
} }
} }
} }
response_builder.append_header((name.clone(), value.clone())); let mut header_value = match HeaderValue::from_str(header_value.as_str()) {
Ok(header_value) => header_value,
Err(e) => {
logger::error!(?e);
return http_server_error_json_response("Something Went Wrong");
}
};
if is_sensitive_header {
header_value.set_sensitive(true);
}
response_builder.append_header((header_name, header_value));
} }
response_builder response_builder

View File

@ -33,6 +33,8 @@ use crate::{
utils::OptionExt, utils::OptionExt,
}; };
pub mod blacklist; pub mod blacklist;
#[cfg(feature = "olap")]
pub mod cookies;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AuthenticationData { pub struct AuthenticationData {

View File

@ -0,0 +1,62 @@
use cookie::{
time::{Duration, OffsetDateTime},
Cookie, SameSite,
};
use masking::{ExposeInterface, Mask, Secret};
use crate::{
consts::{JWT_TOKEN_COOKIE_NAME, JWT_TOKEN_TIME_IN_SECS},
core::errors::{UserErrors, UserResponse},
services::ApplicationResponse,
};
pub fn set_cookie_response<R>(response: R, token: Secret<String>) -> UserResponse<R> {
let jwt_expiry_in_seconds = JWT_TOKEN_TIME_IN_SECS
.try_into()
.map_err(|_| UserErrors::InternalServerError)?;
let (expiry, max_age) = get_expiry_and_max_age_from_seconds(jwt_expiry_in_seconds);
let header_value = create_cookie(token, expiry, max_age)
.to_string()
.into_masked();
let header_key = get_cookie_header();
let header = vec![(header_key, header_value)];
Ok(ApplicationResponse::JsonWithHeaders((response, header)))
}
pub fn remove_cookie_response() -> UserResponse<()> {
let (expiry, max_age) = get_expiry_and_max_age_from_seconds(0);
let header_key = get_cookie_header();
let header_value = create_cookie("".to_string().into(), expiry, max_age)
.to_string()
.into_masked();
let header = vec![(header_key, header_value)];
Ok(ApplicationResponse::JsonWithHeaders(((), header)))
}
fn create_cookie<'c>(
token: Secret<String>,
expires: OffsetDateTime,
max_age: Duration,
) -> Cookie<'c> {
Cookie::build((JWT_TOKEN_COOKIE_NAME, token.expose()))
.http_only(true)
.secure(true)
.same_site(SameSite::Strict)
.path("/")
.expires(expires)
.max_age(max_age)
.build()
}
fn get_expiry_and_max_age_from_seconds(seconds: i64) -> (OffsetDateTime, Duration) {
let max_age = Duration::seconds(seconds);
let expiry = OffsetDateTime::now_utc().saturating_add(max_age);
(expiry, max_age)
}
fn get_cookie_header() -> String {
actix_http::header::SET_COOKIE.to_string()
}

View File

@ -170,3 +170,10 @@ pub async fn get_user_from_db_by_email(
.await .await
.map(UserFromStorage::from) .map(UserFromStorage::from)
} }
pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> Secret<String> {
match resp {
user_api::SignInResponse::DashboardEntry(data) => data.token.clone(),
user_api::SignInResponse::MerchantSelect(data) => data.token.clone(),
}
}