mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
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:
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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_";
|
||||
|
||||
@ -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(),
|
||||
);
|
||||
|
||||
|
||||
@ -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(
|
||||
let response = utils::user::get_dashboard_entry_response(
|
||||
&state,
|
||||
user_from_db,
|
||||
update_status_result,
|
||||
token,
|
||||
)?,
|
||||
))
|
||||
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,
|
||||
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")]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -33,6 +33,8 @@ use crate::{
|
||||
utils::OptionExt,
|
||||
};
|
||||
pub mod blacklist;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod cookies;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthenticationData {
|
||||
|
||||
62
crates/router/src/services/authentication/cookies.rs
Normal file
62
crates/router/src/services/authentication/cookies.rs
Normal 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()
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user