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

View File

@ -58,6 +58,24 @@ impl<T: Eq + PartialEq + Clone> Maskable<T> {
pub fn new_normal(item: T) -> Self {
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`

View File

@ -46,6 +46,7 @@ blake3 = "1.3.3"
bytes = "1.4.0"
clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] }
config = { version = "0.13.3", features = ["toml"] }
cookie = "0.18.0"
diesel = { version = "2.1.0", features = ["postgres"] }
digest = "0.9"
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_COOKIE_NAME: &str = "login_token";
pub const USER_BLACKLIST_PREFIX: &str = "BU_";
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 diesel_models::ephemeral_key;
use error_stack::{report, IntoReport, ResultExt};
use masking::Maskable;
use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentData};
@ -466,20 +467,25 @@ where
.map(|status_code| {
vec![(
"connector_http_status_code".to_string(),
status_code.to_string(),
Maskable::new_normal(status_code.to_string()),
)]
})
.unwrap_or_default();
if let Some(payment_confirm_source) = payment_intent.payment_confirm_source {
headers.push((
"payment_confirm_source".to_string(),
payment_confirm_source.to_string(),
Maskable::new_normal(payment_confirm_source.to_string()),
))
}
headers.extend(
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(),
);

View File

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

View File

@ -158,12 +158,13 @@ pub async fn transfer_org_ownership(
.await
.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;
Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
))
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
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(
@ -202,12 +203,17 @@ pub async fn accept_invitation(
.change_context(UserErrors::InternalServerError)?
.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;
return Ok(ApplicationResponse::Json(
utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
));
let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
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)

View File

@ -9,7 +9,10 @@ use std::{
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};
pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient};
use common_enums::Currency;
@ -20,7 +23,7 @@ use common_utils::{
request::RequestContent,
};
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 serde::Serialize;
use serde_json::json;
@ -110,7 +113,7 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
&self,
_req: &types::RouterData<T, Req, Resp>,
_connectors: &Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
) -> CustomResult<Vec<(String, Maskable<String>)>, errors::ConnectorError> {
Ok(vec![])
}
@ -847,7 +850,7 @@ pub enum ApplicationResponse<R> {
Form(Box<RedirectionFormData>),
PaymentLinkForm(Box<PaymentLinkAction>),
FileData((Vec<u8>, mime::Mime)),
JsonWithHeaders((R, Vec<(String, String)>)),
JsonWithHeaders((R, Vec<(String, Maskable<String>)>)),
}
#[derive(Debug, Eq, PartialEq)]
@ -1046,7 +1049,7 @@ where
);
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);
}
}
@ -1126,7 +1129,7 @@ where
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
.iter()
.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>(
response: T,
mut headers: Vec<(String, String)>,
headers: Vec<(String, Maskable<String>)>,
request_duration: Option<Duration>,
) -> HttpResponse {
let mut response_builder = HttpResponse::Ok();
for (name, value) in headers.iter_mut() {
if name == X_HS_LATENCY {
for (header_name, header_value) in headers {
let is_sensitive_header = header_value.is_masked();
let mut header_value = header_value.into_inner();
if header_name == X_HS_LATENCY {
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;
*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

View File

@ -33,6 +33,8 @@ use crate::{
utils::OptionExt,
};
pub mod blacklist;
#[cfg(feature = "olap")]
pub mod cookies;
#[derive(Clone, Debug)]
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
.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(),
}
}