diff --git a/config/deployments/production.toml b/config/deployments/production.toml index a02e0a300835..892f11e9415b 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -80,7 +80,7 @@ worldline.base_url = "https://eu.sandbox.api-ingenico.com/" worldpay.base_url = "https://try.access.worldpay.com/" zen.base_url = "https://api.zen.com/" zen.secondary_base_url = "https://secure.zen.com/" -threedsecureio.base_url = "https://service.sandbox.3dsecure.io" +threedsecureio.base_url = "https://service.3dsecure.io" [delayed_session_response] connectors_with_delayed_session_response = "trustpay,payme" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 459f4d7b0eb2..f712871abe1c 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -149,6 +149,127 @@ impl Connector { pub fn requires_defend_dispute(&self) -> bool { matches!(self, Self::Checkout) } + pub fn is_separate_authentication_supported(&self) -> bool { + #[cfg(feature = "dummy_connector")] + match self { + Self::DummyConnector1 + | Self::DummyConnector2 + | Self::DummyConnector3 + | Self::DummyConnector4 + | Self::DummyConnector5 + | Self::DummyConnector6 + | Self::DummyConnector7 => false, + Self::Aci + | Self::Adyen + | Self::Airwallex + | Self::Authorizedotnet + | Self::Bambora + | Self::Bankofamerica + | Self::Bitpay + | Self::Bluesnap + | Self::Boku + | Self::Braintree + | Self::Cashtocode + | Self::Coinbase + | Self::Cryptopay + | Self::Dlocal + | Self::Fiserv + | Self::Forte + | Self::Globalpay + | Self::Globepay + | Self::Gocardless + | Self::Helcim + | Self::Iatapay + | Self::Klarna + | Self::Mollie + | Self::Multisafepay + | Self::Nexinets + | Self::Nmi + | Self::Nuvei + | Self::Opennode + | Self::Payme + | Self::Paypal + | Self::Payu + | Self::Placetopay + | Self::Powertranz + | Self::Prophetpay + | Self::Rapyd + | Self::Shift4 + | Self::Square + | Self::Stax + | Self::Trustpay + | Self::Tsys + | Self::Volt + | Self::Wise + | Self::Worldline + | Self::Worldpay + | Self::Zen + | Self::Signifyd + | Self::Plaid + | Self::Riskified + | Self::Threedsecureio + | Self::Cybersource + | Self::Noon + | Self::Stripe => false, + Self::Checkout => true, + } + #[cfg(not(feature = "dummy_connector"))] + match self { + Self::Aci + | Self::Adyen + | Self::Airwallex + | Self::Authorizedotnet + | Self::Bambora + | Self::Bankofamerica + | Self::Bitpay + | Self::Bluesnap + | Self::Boku + | Self::Braintree + | Self::Cashtocode + | Self::Coinbase + | Self::Cryptopay + | Self::Dlocal + | Self::Fiserv + | Self::Forte + | Self::Globalpay + | Self::Globepay + | Self::Gocardless + | Self::Helcim + | Self::Iatapay + | Self::Klarna + | Self::Mollie + | Self::Multisafepay + | Self::Nexinets + | Self::Nmi + | Self::Nuvei + | Self::Opennode + | Self::Payme + | Self::Paypal + | Self::Payu + | Self::Placetopay + | Self::Powertranz + | Self::Prophetpay + | Self::Rapyd + | Self::Shift4 + | Self::Square + | Self::Stax + | Self::Trustpay + | Self::Tsys + | Self::Volt + | Self::Wise + | Self::Worldline + | Self::Worldpay + | Self::Zen + | Self::Signifyd + | Self::Plaid + | Self::Riskified + | Self::Threedsecureio + | Self::Cybersource + | Self::Noon + | Self::Stripe => false, + Self::Checkout => true, + } + } } #[derive( diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 33d355d255ab..bf34a23adf80 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2708,7 +2708,7 @@ pub struct PaymentsResponse { pub external_authentication_details: Option, /// Flag indicating if external 3ds authentication is made or not - pub request_external_3ds_authentication: Option, + pub external_3ds_authentication_attempted: Option, /// Date Time expiry of the payment #[schema(example = "2022-09-10T10:11:12Z")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index f470ffbc03b1..1b38907211f8 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2133,6 +2133,15 @@ pub enum PaymentSource { ExternalAuthenticator, } +impl PaymentSource { + pub fn is_for_internal_use_only(&self) -> bool { + match self { + Self::Dashboard | Self::Sdk | Self::MerchantServer | Self::Postman => false, + Self::Webhook | Self::ExternalAuthenticator => true, + } + } +} + #[derive( Clone, Copy, diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index 27c71ffb0884..c7ce64c54b90 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -209,6 +209,7 @@ pub async fn perform_pre_authentication( if authentication_data .as_ref() .is_some_and(|authentication_data| authentication_data.is_separate_authn_required()) + || authentication.authentication_status.is_failed() { *should_continue_confirm_transaction = false; } diff --git a/crates/router/src/core/authentication/utils.rs b/crates/router/src/core/authentication/utils.rs index 059b0950badd..a2138ff1c07b 100644 --- a/crates/router/src/core/authentication/utils.rs +++ b/crates/router/src/core/authentication/utils.rs @@ -13,7 +13,7 @@ use crate::{ routes::AppState, services::{self, execute_connector_processing_step}, types::{ - api, + api::{self, ConnectorCallType}, authentication::{AuthNFlowType, AuthenticationResponseData}, storage, transformers::ForeignFrom, @@ -22,6 +22,34 @@ use crate::{ utils::OptionExt, }; +pub fn get_connector_name_if_separate_authn_supported( + connector_call_type: &ConnectorCallType, +) -> Option { + match connector_call_type { + ConnectorCallType::PreDetermined(connector_data) => { + if connector_data + .connector_name + .is_separate_authentication_supported() + { + Some(connector_data.connector_name.to_string()) + } else { + None + } + } + ConnectorCallType::Retryable(connectors) => connectors.first().and_then(|connector_data| { + if connector_data + .connector_name + .is_separate_authentication_supported() + { + Some(connector_data.connector_name.to_string()) + } else { + None + } + }), + ConnectorCallType::SessionMultiple(_) => None, + } +} + pub async fn update_trackers( state: &AppState, router_data: RouterData, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2544bffda720..a31c17a3f2b1 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -169,7 +169,7 @@ where let should_add_task_to_process_tracker = should_add_task_to_process_tracker(&payment_data); - payment_data = tokenize_in_router_when_confirm_false( + payment_data = tokenize_in_router_when_confirm_false_or_external_authentication( state, &operation, &mut payment_data, @@ -217,6 +217,18 @@ where should_continue_capture, ); + operation + .to_domain()? + .call_external_three_ds_authentication_if_eligible( + state, + &mut payment_data, + &mut should_continue_transaction, + &connector_details, + &business_profile, + &key_store, + ) + .await?; + if should_continue_transaction { #[cfg(feature = "frm")] match ( @@ -1015,6 +1027,70 @@ impl PaymentRedirectFlow for PaymentRedirectSyn } } +#[derive(Clone, Debug)] +pub struct PaymentAuthenticateCompleteAuthorize; + +#[async_trait::async_trait] +impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize { + async fn call_payment_flow( + &self, + state: &AppState, + merchant_account: domain::MerchantAccount, + merchant_key_store: domain::MerchantKeyStore, + req: PaymentsRedirectResponseData, + connector_action: CallConnectorAction, + ) -> RouterResponse { + let payment_confirm_req = api::PaymentsRequest { + payment_id: Some(req.resource_id.clone()), + merchant_id: req.merchant_id.clone(), + feature_metadata: Some(api_models::payments::FeatureMetadata { + redirect_response: Some(api_models::payments::RedirectResponse { + param: req.param.map(Secret::new), + json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()), + }), + }), + ..Default::default() + }; + Box::pin(payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( + state.clone(), + merchant_account, + merchant_key_store, + PaymentConfirm, + payment_confirm_req, + services::api::AuthFlow::Merchant, + connector_action, + None, + HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator), + )) + .await + } + fn generate_response( + &self, + payments_response: api_models::payments::PaymentsResponse, + business_profile: diesel_models::business_profile::BusinessProfile, + payment_id: String, + connector: String, + ) -> RouterResult { + helpers::get_handle_response_url( + payment_id, + &business_profile, + payments_response, + connector, + ) + } + + fn get_payment_action(&self) -> services::PaymentAction { + services::PaymentAction::CompleteAuthorize + } +} + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] pub async fn call_connector_service( @@ -1989,7 +2065,7 @@ where Ok(payment_data_and_tokenization_action) } -pub async fn tokenize_in_router_when_confirm_false( +pub async fn tokenize_in_router_when_confirm_false_or_external_authentication( state: &AppState, operation: &BoxedOperation<'_, F, Req, Ctx>, payment_data: &mut PaymentData, @@ -2002,23 +2078,27 @@ where Ctx: PaymentMethodRetrieve, { // On confirm is false and only router related - let payment_data = if !is_operation_confirm(operation) { - let (_operation, payment_method_data, pm_id) = operation - .to_domain()? - .make_pm_data( - state, - payment_data, - validate_result.storage_scheme, - merchant_key_store, - customer, - ) - .await?; - payment_data.payment_method_data = payment_method_data; - payment_data.payment_attempt.payment_method_id = pm_id; - payment_data - } else { - payment_data - }; + let is_external_authentication_requested = payment_data + .payment_intent + .request_external_three_ds_authentication; + let payment_data = + if !is_operation_confirm(operation) || is_external_authentication_requested == Some(true) { + let (_operation, payment_method_data, pm_id) = operation + .to_domain()? + .make_pm_data( + state, + payment_data, + validate_result.storage_scheme, + merchant_key_store, + customer, + ) + .await?; + payment_data.payment_method_data = payment_method_data; + payment_data.payment_attempt.payment_method_id = pm_id; + payment_data + } else { + payment_data + }; Ok(payment_data.to_owned()) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 40ba633bdb97..ec4412d93904 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -896,6 +896,16 @@ pub fn create_redirect_url( ) + creds_identifier_path.as_ref() } +pub fn create_authentication_url( + router_base_url: &String, + payment_attempt: &PaymentAttempt, +) -> String { + format!( + "{router_base_url}/payments/{}/3ds/authentication", + payment_attempt.payment_id + ) +} + pub fn create_authorize_url( router_base_url: &String, payment_attempt: &PaymentAttempt, @@ -1811,7 +1821,8 @@ pub async fn store_payment_method_data_in_vault( &state.conf.temp_locker_enable_config, payment_attempt.connector.clone(), payment_method, - ) { + ) || payment_intent.request_external_three_ds_authentication == Some(true) + { let parent_payment_method_token = store_in_vault_and_generate_ppmt( state, payment_method_data, @@ -2913,6 +2924,13 @@ impl MerchantConnectorAccountType { Self::CacheVal(_) => None, } } + + pub fn get_connector_name(&self) -> Option { + match self { + Self::DbVal(db_val) => Some(db_val.connector_name.to_string()), + Self::CacheVal(_) => None, + } + } } /// Query for merchant connector account either by business label or profile id diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index f0e526d2e21a..b9f9618fa6e1 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -35,7 +35,9 @@ use crate::{ routes::AppState, services, types::{ - self, api, domain, + self, + api::{self, ConnectorCallType}, + domain, storage::{self, enums}, PaymentsResponseData, }, @@ -167,6 +169,18 @@ pub trait Domain: Send + Sync { Ok(()) } + async fn call_external_three_ds_authentication_if_eligible<'a>( + &'a self, + _state: &AppState, + _payment_data: &mut PaymentData, + _should_continue_confirm_transaction: &mut bool, + _connector_call_type: &ConnectorCallType, + _merchant_account: &storage::BusinessProfile, + _key_store: &domain::MerchantKeyStore, + ) -> CustomResult<(), errors::ApiErrorResponse> { + Ok(()) + } + #[instrument(skip_all)] async fn guard_payment_against_blocklist<'a>( &'a self, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 96de0cc924bd..33c742f8125b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -2,16 +2,17 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::ext_traits::{AsyncExt, Encode}; +use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; +use router_env::{instrument, logger, tracing}; use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ + authentication::{self, types}, blocklist::utils as blocklist_utils, errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, @@ -25,7 +26,7 @@ use crate::{ routes::AppState, services, types::{ - api::{self, PaymentIdTypeExt}, + api::{self, ConnectorCallType, PaymentIdTypeExt}, domain, storage::{self, enums as storage_enums}, }, @@ -112,7 +113,12 @@ impl helpers::validate_customer_access(&payment_intent, auth_flow, request)?; - if let Some(common_enums::PaymentSource::Webhook) = payment_confirm_source { + if [ + Some(common_enums::PaymentSource::Webhook), + Some(common_enums::PaymentSource::ExternalAuthenticator), + ] + .contains(&payment_confirm_source) + { helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ @@ -548,6 +554,27 @@ impl .payment_method_data .apply_additional_payment_data(additional_payment_data) }); + let authentication = match payment_attempt.authentication_id.clone() { + Some(authentication_id) => { + let authentication = state + .store + .find_authentication_by_merchant_id_authentication_id( + merchant_id.to_string(), + authentication_id.clone(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("Error while fetching authentication record with authentication_id {authentication_id}"))?; + let authentication_data: authentication::types::AuthenticationData = authentication + .authentication_data + .clone() + .parse_value("authentication data") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing authentication_data")?; + Some((authentication, authentication_data)) + } + None => None, + }; payment_attempt.payment_method_billing_address_id = payment_method_billing .as_ref() @@ -596,7 +623,7 @@ impl incremental_authorization_details: None, authorizations: vec![], frm_metadata: request.frm_metadata.clone(), - authentication: None, + authentication, }; let get_trackers_response = operations::GetTrackerResponse { @@ -716,6 +743,128 @@ impl Domain( + &'a self, + state: &AppState, + payment_data: &mut PaymentData, + should_continue_confirm_transaction: &mut bool, + connector_call_type: &ConnectorCallType, + business_profile: &storage::BusinessProfile, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult<(), errors::ApiErrorResponse> { + // if authentication has already happened, then payment_data.authentication will be Some. + // We should do post authn call to fetch the authentication data from 3ds connector + let authentication = payment_data.authentication.clone(); + let is_authentication_type_3ds = payment_data.payment_attempt.authentication_type + == Some(common_enums::AuthenticationType::ThreeDs); + let separate_authentication_requested = payment_data + .payment_intent + .request_external_three_ds_authentication + .unwrap_or(false); + let connector_supports_separate_authn = + authentication::utils::get_connector_name_if_separate_authn_supported( + connector_call_type, + ); + logger::info!("is_pre_authn_call {:?}", authentication.is_none()); + logger::info!( + "separate_authentication_requested {:?}", + separate_authentication_requested + ); + if let Some(payment_connector) = match connector_supports_separate_authn { + Some(payment_connector) + if is_authentication_type_3ds && separate_authentication_requested => + { + Some(payment_connector) + } + _ => None, + } { + let authentication_details: api_models::admin::AuthenticationConnectorDetails = + business_profile + .authentication_connector_details + .clone() + .get_required_value("authentication_details") + .attach_printable("authentication_details not configured by the merchant")? + .parse_value("AuthenticationDetails") + .change_context(errors::ApiErrorResponse::UnprocessableEntity { + message: "Invalid data format found for authentication_details".into(), + }) + .attach_printable( + "Error while parsing authentication_details from merchant_account", + )?; + let authentication_connector_name = authentication_details + .authentication_connectors + .first() + .ok_or(errors::ApiErrorResponse::UnprocessableEntity { message: format!("No authentication_connector found for profile_id {}", business_profile.profile_id) }) + .into_report() + .attach_printable("No authentication_connector found from merchant_account.authentication_details")? + .to_string(); + let profile_id = &business_profile.profile_id; + let authentication_connector_mca = helpers::get_merchant_connector_account( + state, + &business_profile.merchant_id, + None, + key_store, + profile_id, + &authentication_connector_name, + None, + ) + .await?; + if let Some(authentication_data) = authentication { + // call post authn service + authentication::perform_post_authentication( + state, + authentication_connector_name.clone(), + business_profile.clone(), + authentication_connector_mca, + authentication::types::PostAuthenthenticationFlowInput::PaymentAuthNFlow { + payment_data, + authentication_data, + should_continue_confirm_transaction, + }, + ) + .await?; + } else { + let payment_connector_mca = helpers::get_merchant_connector_account( + state, + &business_profile.merchant_id, + None, + key_store, + profile_id, + &payment_connector, + None, + ) + .await?; + // call pre authn service + let card_number = payment_data.payment_method_data.as_ref().and_then(|pmd| { + if let api_models::payments::PaymentMethodData::Card(card) = pmd { + Some(card.card_number.clone()) + } else { + None + } + }); + // External 3DS authentication is applicable only for cards + if let Some(card_number) = card_number { + authentication::perform_pre_authentication( + state, + authentication_connector_name, + authentication::types::PreAuthenthenticationFlowInput::PaymentAuthNFlow { + payment_data, + should_continue_confirm_transaction, + card_number, + }, + business_profile, + authentication_connector_mca, + payment_connector_mca, + ) + .await?; + } + } + Ok(()) + } else { + Ok(()) + } + } + #[instrument(skip_all)] async fn guard_payment_against_blocklist<'a>( &'a self, @@ -753,8 +902,13 @@ impl let browser_info = payment_data.payment_attempt.browser_info.clone(); let frm_message = payment_data.frm_message.clone(); - let (intent_status, attempt_status, (error_code, error_message)) = match frm_suggestion { - Some(FrmSuggestion::FrmCancelTransaction) => ( + let default_status_result = ( + storage_enums::IntentStatus::Processing, + storage_enums::AttemptStatus::Pending, + (None, None), + ); + let status_handler_for_frm_results = |frm_suggestion: FrmSuggestion| match frm_suggestion { + FrmSuggestion::FrmCancelTransaction => ( storage_enums::IntentStatus::Failed, storage_enums::AttemptStatus::Failure, frm_message.map_or((None, None), |fraud_check| { @@ -764,18 +918,48 @@ impl ) }), ), - Some(FrmSuggestion::FrmManualReview) => ( + FrmSuggestion::FrmManualReview => ( storage_enums::IntentStatus::RequiresMerchantAction, storage_enums::AttemptStatus::Unresolved, (None, None), ), - _ => ( - storage_enums::IntentStatus::Processing, - storage_enums::AttemptStatus::Pending, - (None, None), - ), + FrmSuggestion::FrmAutoRefund => default_status_result.clone(), }; + let status_handler_for_authentication_results = + |(authentication, authentication_data): &( + storage::Authentication, + types::AuthenticationData, + )| { + if authentication.authentication_status.is_failed() { + ( + storage_enums::IntentStatus::Failed, + storage_enums::AttemptStatus::Failure, + ( + Some(Some("EXTERNAL_AUTHENTICATION_FAILURE".to_string())), + Some(Some("external authentication failure".to_string())), + ), + ) + } else if authentication_data.is_separate_authn_required() { + ( + storage_enums::IntentStatus::RequiresCustomerAction, + storage_enums::AttemptStatus::AuthenticationPending, + (None, None), + ) + } else { + default_status_result.clone() + } + }; + + let (intent_status, attempt_status, (error_code, error_message)) = + match (frm_suggestion, payment_data.authentication.as_ref()) { + (Some(frm_suggestion), _) => status_handler_for_frm_results(frm_suggestion), + (_, Some(authentication_details)) => { + status_handler_for_authentication_results(authentication_details) + } + _ => default_status_result, + }; + let connector = payment_data.payment_attempt.connector.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); @@ -852,6 +1036,19 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + let ( + external_three_ds_authentication_attempted, + authentication_connector, + authentication_id, + ) = match payment_data.authentication.as_ref() { + Some((authentication, authentication_data)) => ( + Some(authentication_data.is_separate_authn_required()), + Some(authentication.authentication_connector.clone()), + Some(authentication.authentication_id.clone()), + ), + None => (None, None, None), + }; + let payment_attempt_fut = tokio::spawn( async move { m_db.update_payment_attempt_with_attempt_id( @@ -877,9 +1074,9 @@ impl merchant_connector_id, surcharge_amount, tax_amount, - external_three_ds_authentication_attempted: None, - authentication_connector: None, - authentication_id: None, + external_three_ds_authentication_attempted, + authentication_connector, + authentication_id, payment_method_billing_address_id, fingerprint_id: m_fingerprint_id, payment_method_id: m_payment_method_id, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index bcc13ea1cef2..62d0e1e342a7 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -919,7 +919,8 @@ impl PaymentCreate { authorization_count: None, fingerprint_id: None, session_expiry: Some(session_expiry), - request_external_three_ds_authentication: None, + request_external_three_ds_authentication: request + .request_external_three_ds_authentication, }) } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 2de4ddff34e3..447ca14b156a 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -787,6 +787,25 @@ async fn payment_response_update_tracker( payment_data.payment_attempt = payment_attempt; + payment_data.authentication = match payment_data.authentication { + Some((authentication, authentication_data)) => { + let authentication_update = storage::AuthenticationUpdate::PostAuthorizationUpdate { + authentication_lifecycle_status: + storage::enums::AuthenticationLifecycleStatus::Used, + }; + let updated_authentication = state + .store + .update_authentication_by_merchant_id_authentication_id( + authentication, + authentication_update, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + Some((updated_authentication, authentication_data)) + } + None => None, + }; + let amount_captured = get_total_amount_captured( router_data.request, router_data.amount_captured, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 66f0527c35d2..394566394875 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -2,7 +2,7 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::ext_traits::AsyncExt; +use common_utils::ext_traits::{AsyncExt, ValueExt}; use error_stack::ResultExt; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -10,6 +10,7 @@ use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ + authentication, errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{ @@ -400,6 +401,33 @@ async fn get_tracker_for_sync< id: profile_id.to_string(), })?; + let authentication = match payment_attempt.authentication_id.clone() { + Some(authentication_id) => { + let authentication = db + .find_authentication_by_merchant_id_authentication_id( + merchant_account.merchant_id.to_string(), + authentication_id.clone(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("Error while fetching authentication record with authentication_id {authentication_id}"))?; + let authentication_data: authentication::types::AuthenticationData = authentication + .authentication_data + .clone() + .map(|authentication_data_value| { + authentication_data_value + .parse_value("AuthenticationData") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing authentication_data") + }) + .transpose()? + .unwrap_or_default(); + + Some((authentication, authentication_data)) + } + None => None, + }; + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -452,7 +480,7 @@ async fn get_tracker_for_sync< frm_message: frm_response.ok(), incremental_authorization_details: None, authorizations, - authentication: None, + authentication, frm_metadata: None, }; diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 0ce77dfcd13a..513d5b3d4000 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -332,6 +332,9 @@ impl } None => storage_enums::IntentStatus::RequiresPaymentMethod, }; + payment_intent.request_external_three_ds_authentication = request + .request_external_three_ds_authentication + .or(payment_intent.request_external_three_ds_authentication); Self::populate_payment_attempt_with_request(&mut payment_attempt, request); diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 0ae04aaddcec..66cea42414b0 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -412,6 +412,11 @@ where ) }; + let external_authentication_details = payment_data + .authentication + .as_ref() + .map(ForeignInto::foreign_into); + let attempts_response = payment_data.attempts.map(|attempts| { attempts .into_iter() @@ -527,6 +532,7 @@ where || next_action_voucher.is_some() || next_action_containing_qr_code_url.is_some() || next_action_containing_wait_screen.is_some() + || payment_data.authentication.is_some() { next_action_response = bank_transfer_next_steps .map(|bank_transfer| { @@ -556,7 +562,47 @@ where &payment_intent, ), } - })); + })) + .or(match payment_data.authentication.as_ref(){ + Some((_authentication, authentication_data)) => { + if payment_intent.status == common_enums::IntentStatus::RequiresCustomerAction && authentication_data.cavv.is_none() && authentication_data.is_separate_authn_required(){ + // if preAuthn and separate authentication needed. + let payment_connector_name = payment_attempt.connector + .as_ref() + .get_required_value("connector")?; + Some(api_models::payments::NextActionData::ThreeDsInvoke { + three_ds_data: api_models::payments::ThreeDsData { + three_ds_authentication_url: helpers::create_authentication_url( + &server.base_url, + &payment_attempt, + ), + three_ds_authorize_url: helpers::create_authorize_url( + &server.base_url, + &payment_attempt, + payment_connector_name, + ), + three_ds_method_details: authentication_data.three_ds_method_data.three_ds_method_url.as_ref().map(|three_ds_method_url|{ + api_models::payments::ThreeDsMethodData::AcsThreeDsMethodData { + three_ds_method_data_submission: true, + three_ds_method_data: authentication_data + .three_ds_method_data + .three_ds_method_data + .clone(), + three_ds_method_url: Some(three_ds_method_url.to_owned()), + } + }).unwrap_or(api_models::payments::ThreeDsMethodData::AcsThreeDsMethodData { + three_ds_method_data_submission: false, + three_ds_method_data: "".into(), + three_ds_method_url: None, + }), + }, + }) + }else{ + None + } + }, + None => None + }); }; // next action check for third party sdk session (for ex: Apple pay through trustpay has third party sdk session response) @@ -723,10 +769,14 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_external_authentication_details(external_authentication_details) .set_fingerprint(payment_intent.fingerprint_id) .set_authorization_count(payment_intent.authorization_count) .set_incremental_authorizations(incremental_authorizations_response) .set_expires_on(payment_intent.session_expiry) + .set_external_3ds_authentication_attempted( + payment_attempt.external_three_ds_authentication_attempted, + ) .set_payment_method_id(payment_attempt.payment_method_id) .set_payment_method_status(payment_data.payment_method_status) .to_owned(), @@ -795,7 +845,10 @@ where incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, authorization_count: payment_intent.authorization_count, incremental_authorizations: incremental_authorizations_response, + external_authentication_details, expires_on: payment_intent.session_expiry, + external_3ds_authentication_attempted: payment_attempt + .external_three_ds_authentication_attempted, ..Default::default() }, headers, @@ -1152,7 +1205,7 @@ impl TryFrom> for types::PaymentsAuthoriz | Some(RequestIncrementalAuthorization::Default) ), metadata: additional_data.payment_data.payment_intent.metadata, - authentication_data: None, + authentication_data: payment_data.authentication.map(|auth| auth.1), customer_acceptance: payment_data.customer_acceptance, }) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0e43f9c81d95..6a3f042a7eb3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -365,6 +365,9 @@ impl Payments { .service( web::resource("/{payment_id}/incremental_authorization").route(web::post().to(payments_incremental_authorization)), ) + .service( + web::resource("/{payment_id}/{merchant_id}/authorize/{connector}").route(web::post().to(post_3ds_payments_authorize)), + ) .service( web::resource("/{payment_id}/3ds/authentication").route(web::post().to(payments_external_authentication)), ); diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index c83e52199e1c..73d891c89843 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1260,6 +1260,65 @@ pub async fn payments_external_authentication( .await } +#[utoipa::path( + post, + path = "/payments/{payment_id}/{merchant_id}/authorize/{connector}", + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + request_body=PaymentsRequest, + responses( + (status = 200, description = "Payment Authorized", body = PaymentsResponse), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Authorize a Payment", + security(("api_key" = []), ("publishable_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsAuthorize, payment_id))] +pub async fn post_3ds_payments_authorize( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: Option>, + path: web::Path<(String, String, String)>, +) -> impl Responder { + let flow = Flow::PaymentsAuthorize; + + let (payment_id, merchant_id, connector) = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + let param_string = req.query_string(); + let payload = payments::PaymentsRedirectResponseData { + resource_id: payment_types::PaymentIdType::PaymentIntentId(payment_id), + merchant_id: Some(merchant_id.clone()), + force_sync: true, + json_payload: json_payload.map(|payload| payload.0), + param: Some(param_string.to_string()), + connector: Some(connector), + creds_identifier: None, + }; + + let locking_action = payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req| { + >::handle_payments_redirect_response( + &payments::PaymentAuthenticateCompleteAuthorize {}, + state, + auth.merchant_account, + auth.key_store, + req, + ) + }, + &auth::MerchantIdAuth(merchant_id), + locking_action, + )) + .await +} + pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, ) -> errors::RouterResult<()> { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 29f924fc65b1..ed962ce2958f 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -5,10 +5,11 @@ use common_utils::{ consts::X_HS_LATENCY, crypto::Encryptable, ext_traits::{StringExt, ValueExt}, + fp_utils::when, pii, }; use diesel_models::enums as storage_enums; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use super::domain; @@ -966,11 +967,19 @@ impl ForeignTryFrom<&HeaderMap> for api_models::payments::HeaderPayload { ) }) .transpose()?; - + when( + payment_confirm_source.is_some_and(|payment_confirm_source| { + payment_confirm_source.is_for_internal_use_only() + }), + || { + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid data received in payment_confirm_source header".into(), + })) + }, + )?; let x_hs_latency = get_header_value_by_key(X_HS_LATENCY.into(), headers) .map(|value| value == Some("true")) .unwrap_or(false); - Ok(Self { payment_confirm_source, x_hs_latency: Some(x_hs_latency), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 66007b693428..1f57fe2c70f2 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -14267,7 +14267,7 @@ ], "nullable": true }, - "request_external_3ds_authentication": { + "external_3ds_authentication_attempted": { "type": "boolean", "description": "Flag indicating if external 3ds authentication is made or not", "nullable": true