Skip to content

Commit

Permalink
feat: expire client secret after a merchant configurable intent fufli…
Browse files Browse the repository at this point in the history
…ment time (#956)
  • Loading branch information
prajjwalkumar17 authored May 3, 2023
1 parent ed99655 commit 03a9643
Show file tree
Hide file tree
Showing 21 changed files with 201 additions and 49 deletions.
12 changes: 12 additions & 0 deletions crates/api_models/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub struct MerchantAccountCreate {
#[cfg(not(feature = "multiple_mca"))]
#[schema(value_type = Option<PrimaryBusinessDetails>)]
pub primary_business_details: Option<Vec<PrimaryBusinessDetails>>,
///Will be used to expire client secret after certain amount of time to be supplied in seconds
///(900) for 15 mins
#[schema(example = 900)]
pub intent_fulfillment_time: Option<u32>,
}

#[derive(Clone, Debug, Deserialize, ToSchema)]
Expand Down Expand Up @@ -135,6 +139,10 @@ pub struct MerchantAccountUpdate {

///Default business details for connector routing
pub primary_business_details: Option<Vec<PrimaryBusinessDetails>>,

///Will be used to expire client secret after certain amount of time to be supplied in seconds
///(900) for 15 mins
pub intent_fulfillment_time: Option<u32>,
}

#[derive(Clone, Debug, ToSchema, Serialize)]
Expand Down Expand Up @@ -201,6 +209,10 @@ pub struct MerchantAccountResponse {
///Default business details for connector routing
#[schema(value_type = Vec<PrimaryBusinessDetails>)]
pub primary_business_details: Vec<PrimaryBusinessDetails>,

///Will be used to expire client secret after certain amount of time to be supplied in seconds
///(900) for 15 mins
pub intent_fulfillment_time: Option<i64>,
}

#[derive(Clone, Debug, Deserialize, ToSchema, Serialize)]
Expand Down
3 changes: 3 additions & 0 deletions crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub(crate) const ALPHABETS: [char; 62] = [
/// API client request timeout (in seconds)
pub const REQUEST_TIME_OUT: u64 = 30;

///Payment intent fulfillment default timeout (in seconds)
pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60;

// String literals
pub(crate) const NO_ERROR_MESSAGE: &str = "No error message";
pub(crate) const NO_ERROR_CODE: &str = "No error code";
Expand Down
3 changes: 2 additions & 1 deletion crates/router/src/core/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub async fn create_merchant_account(
locker_id: req.locker_id,
metadata: req.metadata,
primary_business_details,
intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from),
};

let merchant_account = db
Expand Down Expand Up @@ -184,7 +185,6 @@ pub async fn get_merchant_account(
)?,
))
}

pub async fn merchant_account_update(
db: &dyn StorageInterface,
merchant_id: &String,
Expand Down Expand Up @@ -258,6 +258,7 @@ pub async fn merchant_account_update(
metadata: req.metadata,
publishable_key: None,
primary_business_details,
intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from),
};

let response = db
Expand Down
5 changes: 2 additions & 3 deletions crates/router/src/core/cards_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ pub async fn retrieve_card_info(
let db = &*state.store;

verify_iin_length(&request.card_iin)?;
helpers::verify_client_secret(
helpers::verify_payment_intent_time_and_client_secret(
db,
merchant_account.storage_scheme,
&merchant_account,
request.client_secret,
&merchant_account.merchant_id,
)
.await?;

Expand Down
5 changes: 2 additions & 3 deletions crates/router/src/core/payment_methods/cards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,11 +787,10 @@ pub async fn list_payment_methods(
let db = &*state.store;
let pm_config_mapping = &state.conf.pm_filters;

let payment_intent = helpers::verify_client_secret(
let payment_intent = helpers::verify_payment_intent_time_and_client_secret(
db,
merchant_account.storage_scheme,
&merchant_account,
req.client_secret.clone(),
&merchant_account.merchant_id,
)
.await?;

Expand Down
14 changes: 11 additions & 3 deletions crates/router/src/core/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use self::operations::{
};
use self::{
flows::{ConstructFlowSpecificData, Feature},
helpers::authenticate_client_secret,
operations::{payment_complete_authorize, BoxedOperation, Operation},
};
use crate::{
Expand All @@ -30,9 +31,10 @@ use crate::{
logger, pii,
routes::AppState,
scheduler::utils as pt_utils,
services,
services::{self, api::Authenticate},
types::{
self, api,
self,
api::{self},
storage::{self, enums as storage_enums},
},
utils::{Encode, OptionExt, ValueExt},
Expand All @@ -48,6 +50,7 @@ pub async fn payments_operation_core<F, Req, Op, FData>(
) -> RouterResult<(PaymentData<F>, Req, Option<storage::Customer>)>
where
F: Send + Clone + Sync,
Req: Authenticate,
Op: Operation<F, Req> + Send + Sync,

// To create connector flow specific interface data
Expand Down Expand Up @@ -82,6 +85,11 @@ where
&merchant_account,
)
.await?;
authenticate_client_secret(
req.get_client_secret(),
&payment_data.payment_intent,
merchant_account.intent_fulfillment_time,
)?;

let (operation, customer) = operation
.to_domain()?
Expand Down Expand Up @@ -197,7 +205,7 @@ where
F: Send + Clone + Sync,
FData: Send + Sync,
Op: Operation<F, Req> + Send + Sync + Clone,
Req: Debug,
Req: Debug + Authenticate,
Res: transformers::ToResponse<Req, PaymentData<F>, Op>,
// To create connector flow specific interface data
PaymentData<F>: ConstructFlowSpecificData<F, FData, types::PaymentsResponseData>,
Expand Down
159 changes: 144 additions & 15 deletions crates/router/src/core/payments/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use common_utils::{
use error_stack::{report, IntoReport, ResultExt};
use masking::{ExposeOptionInterface, PeekInterface};
use router_env::{instrument, tracing};
use storage_models::enums;
use storage_models::{enums, merchant_account, payment_intent};
use time::Duration;
use uuid::Uuid;

use super::{
Expand Down Expand Up @@ -1187,14 +1188,29 @@ pub fn generate_mandate(
}
}

// A function to manually authenticate the client secret
// A function to manually authenticate the client secret with intent fulfillment time
pub(crate) fn authenticate_client_secret(
request_client_secret: Option<&String>,
payment_intent_client_secret: Option<&String>,
payment_intent: &payment_intent::PaymentIntent,
merchant_intent_fulfillment_time: Option<i64>,
) -> Result<(), errors::ApiErrorResponse> {
match (request_client_secret, payment_intent_client_secret) {
(Some(req_cs), Some(pi_cs)) if req_cs != pi_cs => {
Err(errors::ApiErrorResponse::ClientSecretInvalid)
match (request_client_secret, &payment_intent.client_secret) {
(Some(req_cs), Some(pi_cs)) => {
if req_cs != pi_cs {
Err(errors::ApiErrorResponse::ClientSecretInvalid)
} else {
//This is done to check whether the merchant_account's intent fulfillment time has expired or not
let payment_intent_fulfillment_deadline =
payment_intent.created_at.saturating_add(Duration::seconds(
merchant_intent_fulfillment_time
.unwrap_or(consts::DEFAULT_FULFILLMENT_TIME),
));
let current_timestamp = common_utils::date_time::now();
fp_utils::when(
current_timestamp > payment_intent_fulfillment_deadline,
|| Err(errors::ApiErrorResponse::ClientSecretExpired),
)
}
}
// If there is no client in payment intent, then it has expired
(Some(_), None) => Err(errors::ApiErrorResponse::ClientSecretExpired),
Expand Down Expand Up @@ -1239,11 +1255,10 @@ pub(crate) fn validate_pm_or_token_given(
}

// A function to perform database lookup and then verify the client secret
pub(crate) async fn verify_client_secret(
pub(crate) async fn verify_payment_intent_time_and_client_secret(
db: &dyn StorageInterface,
storage_scheme: storage_enums::MerchantStorageScheme,
merchant_account: &merchant_account::MerchantAccount,
client_secret: Option<String>,
merchant_id: &str,
) -> error_stack::Result<Option<storage::PaymentIntent>, errors::ApiErrorResponse> {
client_secret
.async_map(|cs| async move {
Expand All @@ -1252,13 +1267,17 @@ pub(crate) async fn verify_client_secret(
let payment_intent = db
.find_payment_intent_by_payment_id_merchant_id(
&payment_id,
merchant_id,
storage_scheme,
&merchant_account.merchant_id,
merchant_account.storage_scheme,
)
.await
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;

authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())?;
authenticate_client_secret(
Some(&cs),
&payment_intent,
merchant_account.intent_fulfillment_time,
)?;
Ok(payment_intent)
})
.await
Expand Down Expand Up @@ -1356,10 +1375,120 @@ mod tests {
use super::*;

#[test]
fn test_authenticate_client_secret() {
fn test_authenticate_client_secret_fulfillment_time_not_expired() {
let payment_intent = payment_intent::PaymentIntent {
id: 21,
payment_id: "23".to_string(),
merchant_id: "22".to_string(),
status: storage_enums::IntentStatus::RequiresCapture,
amount: 200,
currency: None,
amount_captured: None,
customer_id: None,
description: None,
return_url: None,
metadata: None,
connector_id: None,
shipping_address_id: None,
billing_address_id: None,
statement_descriptor_name: None,
statement_descriptor_suffix: None,
created_at: common_utils::date_time::now(),
modified_at: common_utils::date_time::now(),
last_synced: None,
setup_future_usage: None,
off_session: None,
client_secret: Some("1".to_string()),
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
};
let req_cs = Some("1".to_string());
let merchant_fulfillment_time = Some(900);
assert!(authenticate_client_secret(
req_cs.as_ref(),
&payment_intent,
merchant_fulfillment_time
)
.is_ok()); // Check if the result is an Ok variant
}

#[test]
fn test_authenticate_client_secret_fulfillment_time_expired() {
let payment_intent = payment_intent::PaymentIntent {
id: 21,
payment_id: "23".to_string(),
merchant_id: "22".to_string(),
status: storage_enums::IntentStatus::RequiresCapture,
amount: 200,
currency: None,
amount_captured: None,
customer_id: None,
description: None,
return_url: None,
metadata: None,
connector_id: None,
shipping_address_id: None,
billing_address_id: None,
statement_descriptor_name: None,
statement_descriptor_suffix: None,
created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)),
modified_at: common_utils::date_time::now(),
last_synced: None,
setup_future_usage: None,
off_session: None,
client_secret: Some("1".to_string()),
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
};
let req_cs = Some("1".to_string());
let merchant_fulfillment_time = Some(10);
assert!(authenticate_client_secret(
req_cs.as_ref(),
&payment_intent,
merchant_fulfillment_time
)
.is_err())
}

#[test]
fn test_authenticate_client_secret_expired() {
let payment_intent = payment_intent::PaymentIntent {
id: 21,
payment_id: "23".to_string(),
merchant_id: "22".to_string(),
status: storage_enums::IntentStatus::RequiresCapture,
amount: 200,
currency: None,
amount_captured: None,
customer_id: None,
description: None,
return_url: None,
metadata: None,
connector_id: None,
shipping_address_id: None,
billing_address_id: None,
statement_descriptor_name: None,
statement_descriptor_suffix: None,
created_at: common_utils::date_time::now().saturating_sub(Duration::seconds(20)),
modified_at: common_utils::date_time::now(),
last_synced: None,
setup_future_usage: None,
off_session: None,
client_secret: None,
active_attempt_id: "nopes".to_string(),
business_country: storage_enums::CountryAlpha2::AG,
business_label: "no".to_string(),
};
let req_cs = Some("1".to_string());
let pi_cs = Some("2".to_string());
assert!(authenticate_client_secret(req_cs.as_ref(), pi_cs.as_ref()).is_err())
let merchant_fulfillment_time = Some(10);
assert!(authenticate_client_secret(
req_cs.as_ref(),
&payment_intent,
merchant_fulfillment_time
)
.is_err())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Co
)
.await?;

helpers::authenticate_client_secret(
request.client_secret.as_ref(),
payment_intent.client_secret.as_ref(),
)?;

let browser_info = request
.browser_info
.clone()
Expand Down
5 changes: 0 additions & 5 deletions crates/router/src/core/payments/operations/payment_confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
)
.await?;

helpers::authenticate_client_secret(
request.client_secret.as_ref(),
payment_intent.client_secret.as_ref(),
)?;

let browser_info = request
.browser_info
.clone()
Expand Down
5 changes: 0 additions & 5 deletions crates/router/src/core/payments/operations/payment_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsSessionRequest>

let amount = payment_intent.amount.into();

helpers::authenticate_client_secret(
Some(&request.client_secret),
payment_intent.client_secret.as_ref(),
)?;

let shipping_address = helpers::get_address_for_payment_request(
db,
None,
Expand Down
5 changes: 0 additions & 5 deletions crates/router/src/core/payments/operations/payment_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ impl<F: Send + Clone> GetTracker<F, PaymentData<F>, api::PaymentsRequest> for Pa
"update",
)?;

helpers::authenticate_client_secret(
request.client_secret.as_ref(),
payment_intent.client_secret.as_ref(),
)?;

let (token, payment_method_type, setup_mandate) =
helpers::get_token_pm_type_mandate_details(
state,
Expand Down
Loading

0 comments on commit 03a9643

Please sign in to comment.