mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
refactor(core): refactor authentication core to fetch authentication only within it (#4138)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -7040,9 +7040,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.35"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef89ece63debf11bc32d1ed8d078ac870cbeb44da02afb02a9ff135ae7ca0582"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
|
||||
@ -177,8 +177,8 @@ impl Connector {
|
||||
matches!(self, Self::Checkout)
|
||||
}
|
||||
pub fn is_separate_authentication_supported(&self) -> bool {
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
match self {
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
Self::DummyConnector1
|
||||
| Self::DummyConnector2
|
||||
| Self::DummyConnector3
|
||||
@ -243,66 +243,6 @@ impl Connector {
|
||||
| Self::Stripe => false,
|
||||
Self::Checkout | Self::Nmi => true,
|
||||
}
|
||||
#[cfg(not(feature = "dummy_connector"))]
|
||||
match self {
|
||||
Self::Aci
|
||||
| Self::Adyen
|
||||
| Self::Airwallex
|
||||
| Self::Authorizedotnet
|
||||
| Self::Bambora
|
||||
| Self::Bankofamerica
|
||||
| Self::Billwerk
|
||||
| Self::Bitpay
|
||||
| Self::Bluesnap
|
||||
| Self::Boku
|
||||
| Self::Braintree
|
||||
| Self::Cashtocode
|
||||
| Self::Coinbase
|
||||
| Self::Cryptopay
|
||||
| Self::Dlocal
|
||||
| Self::Ebanx
|
||||
| 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::Zsl
|
||||
| Self::Signifyd
|
||||
| Self::Plaid
|
||||
| Self::Riskified
|
||||
| Self::Threedsecureio
|
||||
| Self::Cybersource
|
||||
| Self::Noon
|
||||
| Self::Netcetera
|
||||
| Self::Stripe => false,
|
||||
Self::Checkout => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -101,7 +101,6 @@ pub enum AuthenticationUpdate {
|
||||
message_version: common_utils::types::SemanticVersion,
|
||||
connector_metadata: Option<serde_json::Value>,
|
||||
authentication_status: common_enums::AuthenticationStatus,
|
||||
payment_method_id: Option<String>,
|
||||
acquirer_bin: Option<String>,
|
||||
acquirer_merchant_id: Option<String>,
|
||||
},
|
||||
@ -309,7 +308,6 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
|
||||
message_version,
|
||||
connector_metadata,
|
||||
authentication_status,
|
||||
payment_method_id,
|
||||
acquirer_bin,
|
||||
acquirer_merchant_id,
|
||||
} => Self {
|
||||
@ -321,7 +319,6 @@ impl From<AuthenticationUpdate> for AuthenticationUpdateInternal {
|
||||
message_version: Some(message_version),
|
||||
connector_metadata,
|
||||
authentication_status: Some(authentication_status),
|
||||
payment_method_id,
|
||||
acquirer_bin,
|
||||
acquirer_merchant_id,
|
||||
..Default::default()
|
||||
|
||||
@ -5,19 +5,16 @@ pub mod types;
|
||||
|
||||
use api_models::payments;
|
||||
use common_enums::Currency;
|
||||
use common_utils::{
|
||||
errors::CustomResult,
|
||||
ext_traits::{Encode, StringExt, ValueExt},
|
||||
};
|
||||
use error_stack::{report, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::ResultExt;
|
||||
use masking::ExposeInterface;
|
||||
|
||||
use super::errors;
|
||||
use super::errors::{self, StorageErrorExt};
|
||||
use crate::{
|
||||
core::{errors::ApiErrorResponse, payments as payments_core},
|
||||
routes::AppState,
|
||||
types::{self as core_types, api, authentication::AuthenticationResponseData, storage},
|
||||
utils::{check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata, OptionExt},
|
||||
types::{self as core_types, api, domain, storage},
|
||||
utils::check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@ -64,133 +61,76 @@ pub async fn perform_authentication(
|
||||
)?;
|
||||
let response =
|
||||
utils::do_auth_connector_call(state, authentication_connector.clone(), router_data).await?;
|
||||
utils::update_trackers(state, response.clone(), authentication_data, None, None).await?;
|
||||
let authentication_response =
|
||||
response
|
||||
.response
|
||||
.map_err(|err| ApiErrorResponse::ExternalConnectorError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
connector: authentication_connector,
|
||||
status_code: err.status_code,
|
||||
reason: err.reason,
|
||||
})?;
|
||||
match authentication_response {
|
||||
AuthenticationResponseData::AuthNResponse {
|
||||
authn_flow_type,
|
||||
trans_status,
|
||||
..
|
||||
} => Ok(match authn_flow_type {
|
||||
core_types::authentication::AuthNFlowType::Challenge(challenge_params) => {
|
||||
core_types::api::AuthenticationResponse {
|
||||
trans_status,
|
||||
acs_url: challenge_params.acs_url,
|
||||
challenge_request: challenge_params.challenge_request,
|
||||
acs_reference_number: challenge_params.acs_reference_number,
|
||||
acs_trans_id: challenge_params.acs_trans_id,
|
||||
three_dsserver_trans_id: challenge_params.three_dsserver_trans_id,
|
||||
acs_signed_content: challenge_params.acs_signed_content,
|
||||
}
|
||||
}
|
||||
core_types::authentication::AuthNFlowType::Frictionless => {
|
||||
core_types::api::AuthenticationResponse {
|
||||
trans_status,
|
||||
acs_url: None,
|
||||
challenge_request: None,
|
||||
acs_reference_number: None,
|
||||
acs_trans_id: None,
|
||||
three_dsserver_trans_id: None,
|
||||
acs_signed_content: None,
|
||||
}
|
||||
}
|
||||
}),
|
||||
_ => Err(report!(errors::ApiErrorResponse::InternalServerError))
|
||||
.attach_printable("unexpected response in authentication flow")?,
|
||||
}
|
||||
let authentication =
|
||||
utils::update_trackers(state, response.clone(), authentication_data, None).await?;
|
||||
response
|
||||
.response
|
||||
.map_err(|err| ApiErrorResponse::ExternalConnectorError {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
connector: authentication_connector,
|
||||
status_code: err.status_code,
|
||||
reason: err.reason,
|
||||
})?;
|
||||
core_types::api::authentication::AuthenticationResponse::try_from(authentication)
|
||||
}
|
||||
|
||||
pub async fn perform_post_authentication<F: Clone + Send>(
|
||||
pub async fn perform_post_authentication(
|
||||
state: &AppState,
|
||||
authentication_connector: String,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
business_profile: core_types::storage::BusinessProfile,
|
||||
merchant_connector_account: payments_core::helpers::MerchantConnectorAccountType,
|
||||
authentication_flow_input: types::PostAuthenthenticationFlowInput<'_, F>,
|
||||
) -> CustomResult<(), ApiErrorResponse> {
|
||||
match authentication_flow_input {
|
||||
types::PostAuthenthenticationFlowInput::PaymentAuthNFlow {
|
||||
payment_data,
|
||||
authentication,
|
||||
should_continue_confirm_transaction,
|
||||
} => {
|
||||
let is_pull_mechanism_enabled =
|
||||
check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(
|
||||
merchant_connector_account
|
||||
.get_metadata()
|
||||
.map(|metadata| metadata.expose()),
|
||||
);
|
||||
let authentication_status =
|
||||
if !authentication.authentication_status.is_terminal_status()
|
||||
&& is_pull_mechanism_enabled
|
||||
{
|
||||
let router_data = transformers::construct_post_authentication_router_data(
|
||||
authentication_connector.clone(),
|
||||
business_profile.clone(),
|
||||
merchant_connector_account,
|
||||
&authentication,
|
||||
)?;
|
||||
let router_data =
|
||||
utils::do_auth_connector_call(state, authentication_connector, router_data)
|
||||
.await?;
|
||||
let updated_authentication = utils::update_trackers(
|
||||
state,
|
||||
router_data,
|
||||
authentication.clone(),
|
||||
payment_data.token.clone(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let authentication_status = updated_authentication.authentication_status;
|
||||
payment_data.authentication = Some(updated_authentication);
|
||||
authentication_status
|
||||
} else {
|
||||
authentication.authentication_status
|
||||
};
|
||||
//If authentication is not successful, skip the payment connector flows and mark the payment as failure
|
||||
if !(authentication_status == api_models::enums::AuthenticationStatus::Success) {
|
||||
*should_continue_confirm_transaction = false;
|
||||
}
|
||||
}
|
||||
types::PostAuthenthenticationFlowInput::PaymentMethodAuthNFlow { other_fields: _ } => {
|
||||
// todo!("Payment method post authN operation");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_payment_id_from_pre_authentication_flow_input<F: Clone + Send>(
|
||||
pre_authentication_flow_input: &types::PreAuthenthenticationFlowInput<'_, F>,
|
||||
) -> Option<String> {
|
||||
match pre_authentication_flow_input {
|
||||
types::PreAuthenthenticationFlowInput::PaymentAuthNFlow { payment_data, .. } => {
|
||||
Some(payment_data.payment_intent.payment_id.clone())
|
||||
}
|
||||
_ => None,
|
||||
authentication_id: String,
|
||||
) -> CustomResult<storage::Authentication, ApiErrorResponse> {
|
||||
let (authentication_connector, three_ds_connector_account) =
|
||||
utils::get_authentication_connector_data(state, key_store, &business_profile).await?;
|
||||
let is_pull_mechanism_enabled =
|
||||
check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(
|
||||
three_ds_connector_account
|
||||
.get_metadata()
|
||||
.map(|metadata| metadata.expose()),
|
||||
);
|
||||
let authentication = state
|
||||
.store
|
||||
.find_authentication_by_merchant_id_authentication_id(
|
||||
business_profile.merchant_id.clone(),
|
||||
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}"))?;
|
||||
if !authentication.authentication_status.is_terminal_status() && is_pull_mechanism_enabled {
|
||||
let router_data = transformers::construct_post_authentication_router_data(
|
||||
authentication_connector.to_string(),
|
||||
business_profile,
|
||||
three_ds_connector_account,
|
||||
&authentication,
|
||||
)?;
|
||||
let router_data =
|
||||
utils::do_auth_connector_call(state, authentication_connector.to_string(), router_data)
|
||||
.await?;
|
||||
utils::update_trackers(state, router_data, authentication, None).await
|
||||
} else {
|
||||
Ok(authentication)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn perform_pre_authentication<F: Clone + Send>(
|
||||
pub async fn perform_pre_authentication(
|
||||
state: &AppState,
|
||||
authentication_connector_name: String,
|
||||
authentication_flow_input: types::PreAuthenthenticationFlowInput<'_, F>,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
card_number: cards::CardNumber,
|
||||
token: String,
|
||||
business_profile: &core_types::storage::BusinessProfile,
|
||||
three_ds_connector_account: payments_core::helpers::MerchantConnectorAccountType,
|
||||
payment_connector_account: payments_core::helpers::MerchantConnectorAccountType,
|
||||
) -> CustomResult<(), ApiErrorResponse> {
|
||||
let payment_id = get_payment_id_from_pre_authentication_flow_input(&authentication_flow_input);
|
||||
acquirer_details: Option<types::AcquirerDetails>,
|
||||
payment_id: Option<String>,
|
||||
) -> CustomResult<storage::Authentication, ApiErrorResponse> {
|
||||
let (authentication_connector, three_ds_connector_account) =
|
||||
utils::get_authentication_connector_data(state, key_store, business_profile).await?;
|
||||
let authentication_connector_name = authentication_connector.to_string();
|
||||
let authentication = utils::create_new_authentication(
|
||||
state,
|
||||
business_profile.merchant_id.clone(),
|
||||
authentication_connector_name.clone(),
|
||||
token,
|
||||
business_profile.profile_id.clone(),
|
||||
payment_id,
|
||||
three_ds_connector_account
|
||||
@ -199,74 +139,15 @@ pub async fn perform_pre_authentication<F: Clone + Send>(
|
||||
.attach_printable("Error while finding mca_id from merchant_connector_account")?,
|
||||
)
|
||||
.await?;
|
||||
match authentication_flow_input {
|
||||
types::PreAuthenthenticationFlowInput::PaymentAuthNFlow {
|
||||
payment_data,
|
||||
should_continue_confirm_transaction,
|
||||
card_number,
|
||||
} => {
|
||||
let router_data = transformers::construct_pre_authentication_router_data(
|
||||
authentication_connector_name.clone(),
|
||||
card_number,
|
||||
&three_ds_connector_account,
|
||||
business_profile.merchant_id.clone(),
|
||||
)?;
|
||||
let router_data = utils::do_auth_connector_call(
|
||||
state,
|
||||
authentication_connector_name.clone(),
|
||||
router_data,
|
||||
)
|
||||
.await?;
|
||||
let acquirer_details: types::AcquirerDetails = payment_connector_account
|
||||
.get_metadata()
|
||||
.get_required_value("merchant_connector_account.metadata")?
|
||||
.peek()
|
||||
.clone()
|
||||
.parse_value("AcquirerDetails")
|
||||
.change_context(ApiErrorResponse::PreconditionFailed { message: "acquirer_bin and acquirer_merchant_id not found in Payment Connector's Metadata".to_string()})?;
|
||||
|
||||
let authentication = utils::update_trackers(
|
||||
state,
|
||||
router_data,
|
||||
authentication,
|
||||
payment_data.token.clone(),
|
||||
Some(acquirer_details),
|
||||
)
|
||||
.await?;
|
||||
if authentication.is_separate_authn_required()
|
||||
|| authentication.authentication_status.is_failed()
|
||||
{
|
||||
*should_continue_confirm_transaction = false;
|
||||
// If flow is going through external authentication, set the poll_config in payment_data which can be fetched while sending next_action block in confirm response
|
||||
let default_poll_config = core_types::PollConfig::default();
|
||||
let default_config_str = default_poll_config
|
||||
.encode_to_string_of_json()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while stringifying default poll config")?;
|
||||
let poll_config = state
|
||||
.store
|
||||
.find_config_by_key_unwrap_or(
|
||||
&core_types::PollConfig::get_poll_config_key(authentication_connector_name),
|
||||
Some(default_config_str),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("The poll config was not found in the DB")?;
|
||||
let poll_config: core_types::PollConfig = poll_config
|
||||
.config
|
||||
.parse_struct("PollConfig")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while parsing PollConfig")?;
|
||||
payment_data.poll_config = Some(poll_config)
|
||||
}
|
||||
payment_data.authentication = Some(authentication);
|
||||
}
|
||||
types::PreAuthenthenticationFlowInput::PaymentMethodAuthNFlow {
|
||||
card_number: _,
|
||||
other_fields: _,
|
||||
} => {
|
||||
// todo!("Payment method authN operation");
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
let router_data = transformers::construct_pre_authentication_router_data(
|
||||
authentication_connector_name.clone(),
|
||||
card_number,
|
||||
&three_ds_connector_account,
|
||||
business_profile.merchant_id.clone(),
|
||||
)?;
|
||||
let router_data =
|
||||
utils::do_auth_connector_call(state, authentication_connector_name, router_data).await?;
|
||||
|
||||
utils::update_trackers(state, router_data, authentication, acquirer_details).await
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use common_utils::ext_traits::ValueExt;
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
@ -10,39 +11,39 @@ use crate::{
|
||||
routes::AppState,
|
||||
services::{self, execute_connector_processing_step},
|
||||
types::{
|
||||
api::{self, ConnectorCallType},
|
||||
authentication::AuthenticationResponseData,
|
||||
storage,
|
||||
transformers::ForeignFrom,
|
||||
RouterData,
|
||||
api, authentication::AuthenticationResponseData, domain, storage,
|
||||
transformers::ForeignFrom, RouterData,
|
||||
},
|
||||
utils::OptionExt,
|
||||
};
|
||||
|
||||
pub fn get_connector_name_if_separate_authn_supported(
|
||||
connector_call_type: &ConnectorCallType,
|
||||
) -> Option<String> {
|
||||
pub fn get_connector_data_if_separate_authn_supported(
|
||||
connector_call_type: &api::ConnectorCallType,
|
||||
) -> Option<api::ConnectorData> {
|
||||
match connector_call_type {
|
||||
ConnectorCallType::PreDetermined(connector_data) => {
|
||||
api::ConnectorCallType::PreDetermined(connector_data) => {
|
||||
if connector_data
|
||||
.connector_name
|
||||
.is_separate_authentication_supported()
|
||||
{
|
||||
Some(connector_data.connector_name.to_string())
|
||||
Some(connector_data.clone())
|
||||
} 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,
|
||||
api::ConnectorCallType::Retryable(connectors) => {
|
||||
connectors.first().and_then(|connector_data| {
|
||||
if connector_data
|
||||
.connector_name
|
||||
.is_separate_authentication_supported()
|
||||
{
|
||||
Some(connector_data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
api::ConnectorCallType::SessionMultiple(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +51,6 @@ pub async fn update_trackers<F: Clone, Req>(
|
||||
state: &AppState,
|
||||
router_data: RouterData<F, Req, AuthenticationResponseData>,
|
||||
authentication: storage::Authentication,
|
||||
token: Option<String>,
|
||||
acquirer_details: Option<super::types::AcquirerDetails>,
|
||||
) -> RouterResult<storage::Authentication> {
|
||||
let authentication_update = match router_data.response {
|
||||
@ -72,7 +72,6 @@ pub async fn update_trackers<F: Clone, Req>(
|
||||
message_version,
|
||||
connector_metadata,
|
||||
authentication_status: common_enums::AuthenticationStatus::Pending,
|
||||
payment_method_id: token.map(|token| format!("eph_{}", token)),
|
||||
acquirer_bin: acquirer_details
|
||||
.as_ref()
|
||||
.map(|acquirer_details| acquirer_details.acquirer_bin.clone()),
|
||||
@ -144,6 +143,7 @@ pub async fn create_new_authentication(
|
||||
state: &AppState,
|
||||
merchant_id: String,
|
||||
authentication_connector: String,
|
||||
token: String,
|
||||
profile_id: String,
|
||||
payment_id: Option<String>,
|
||||
merchant_connector_id: String,
|
||||
@ -155,7 +155,7 @@ pub async fn create_new_authentication(
|
||||
merchant_id,
|
||||
authentication_connector,
|
||||
connector_authentication_id: None,
|
||||
payment_method_id: "".into(),
|
||||
payment_method_id: format!("eph_{}", token),
|
||||
authentication_type: None,
|
||||
authentication_status: common_enums::AuthenticationStatus::Started,
|
||||
authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus::Unused,
|
||||
@ -221,3 +221,53 @@ where
|
||||
.to_payment_failed_response()?;
|
||||
Ok(router_data)
|
||||
}
|
||||
|
||||
pub async fn get_authentication_connector_data(
|
||||
state: &AppState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
business_profile: &storage::BusinessProfile,
|
||||
) -> RouterResult<(
|
||||
api_models::enums::AuthenticationConnectors,
|
||||
payments::helpers::MerchantConnectorAccountType,
|
||||
)> {
|
||||
let authentication_details: api_models::admin::AuthenticationConnectorDetails =
|
||||
business_profile
|
||||
.authentication_connector_details
|
||||
.clone()
|
||||
.get_required_value("authentication_details")
|
||||
.change_context(errors::ApiErrorResponse::UnprocessableEntity {
|
||||
message: "authentication_connector_details is not available in business profile"
|
||||
.into(),
|
||||
})
|
||||
.attach_printable("authentication_connector_details not configured by the merchant")?
|
||||
.parse_value("AuthenticationConnectorDetails")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"Error while parsing authentication_connector_details from business_profile",
|
||||
)?;
|
||||
let authentication_connector = authentication_details
|
||||
.authentication_connectors
|
||||
.first()
|
||||
.ok_or(errors::ApiErrorResponse::UnprocessableEntity {
|
||||
message: format!(
|
||||
"No authentication_connector found for profile_id {}",
|
||||
business_profile.profile_id
|
||||
),
|
||||
})
|
||||
.attach_printable(
|
||||
"No authentication_connector found from merchant_account.authentication_details",
|
||||
)?
|
||||
.to_owned();
|
||||
let profile_id = &business_profile.profile_id;
|
||||
let authentication_connector_mca = payments::helpers::get_merchant_connector_account(
|
||||
state,
|
||||
&business_profile.merchant_id,
|
||||
None,
|
||||
key_store,
|
||||
profile_id,
|
||||
authentication_connector.to_string().as_str(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok((authentication_connector, authentication_connector_mca))
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ use crate::{
|
||||
connector,
|
||||
consts::{self, BASE64_ENGINE},
|
||||
core::{
|
||||
authentication,
|
||||
errors::{self, CustomResult, RouterResult, StorageErrorExt},
|
||||
mandate::helpers::MandateGenericData,
|
||||
payment_methods::{cards, vault, PaymentMethodRetrieve},
|
||||
@ -4356,6 +4357,103 @@ pub fn validate_mandate_data_and_future_usage(
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PaymentExternalAuthenticationFlow {
|
||||
PreAuthenticationFlow {
|
||||
acquirer_details: authentication::types::AcquirerDetails,
|
||||
card_number: ::cards::CardNumber,
|
||||
token: String,
|
||||
},
|
||||
PostAuthenticationFlow {
|
||||
authentication_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn get_payment_external_authentication_flow_during_confirm<F: Clone>(
|
||||
state: &AppState,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
business_profile: &storage::BusinessProfile,
|
||||
payment_data: &mut PaymentData<F>,
|
||||
connector_call_type: &api::ConnectorCallType,
|
||||
) -> RouterResult<Option<PaymentExternalAuthenticationFlow>> {
|
||||
let authentication_id = payment_data.payment_attempt.authentication_id.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 separate_three_ds_authentication_attempted = payment_data
|
||||
.payment_attempt
|
||||
.external_three_ds_authentication_attempted
|
||||
.unwrap_or(false);
|
||||
let connector_supports_separate_authn =
|
||||
authentication::utils::get_connector_data_if_separate_authn_supported(connector_call_type);
|
||||
logger::info!("is_pre_authn_call {:?}", authentication_id.is_none());
|
||||
logger::info!(
|
||||
"separate_authentication_requested {:?}",
|
||||
separate_authentication_requested
|
||||
);
|
||||
logger::info!(
|
||||
"payment connector supports external authentication: {:?}",
|
||||
connector_supports_separate_authn.is_some()
|
||||
);
|
||||
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
|
||||
}
|
||||
});
|
||||
Ok(if separate_three_ds_authentication_attempted {
|
||||
authentication_id.map(|authentication_id| {
|
||||
PaymentExternalAuthenticationFlow::PostAuthenticationFlow { authentication_id }
|
||||
})
|
||||
} else if separate_authentication_requested && is_authentication_type_3ds {
|
||||
if let Some((connector_data, card_number)) =
|
||||
connector_supports_separate_authn.zip(card_number)
|
||||
{
|
||||
let token = payment_data
|
||||
.token
|
||||
.clone()
|
||||
.get_required_value("token")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable(
|
||||
"payment_data.token should not be None while making pre authentication call",
|
||||
)?;
|
||||
let payment_connector_mca = get_merchant_connector_account(
|
||||
state,
|
||||
&business_profile.merchant_id,
|
||||
None,
|
||||
key_store,
|
||||
&business_profile.profile_id,
|
||||
connector_data.connector_name.to_string().as_str(),
|
||||
connector_data.merchant_connector_id.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
let acquirer_details: authentication::types::AcquirerDetails = payment_connector_mca
|
||||
.get_metadata()
|
||||
.get_required_value("merchant_connector_account.metadata")?
|
||||
.peek()
|
||||
.clone()
|
||||
.parse_value("AcquirerDetails")
|
||||
.change_context(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message:
|
||||
"acquirer_bin and acquirer_merchant_id not found in Payment Connector's Metadata"
|
||||
.to_string(),
|
||||
})?;
|
||||
Some(PaymentExternalAuthenticationFlow::PreAuthenticationFlow {
|
||||
card_number,
|
||||
token,
|
||||
acquirer_details,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_redis_key_for_extended_card_info(merchant_id: &str, payment_id: &str) -> String {
|
||||
format!("{merchant_id}_{payment_id}_extended_card_info")
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ use std::marker::PhantomData;
|
||||
|
||||
use api_models::{admin::ExtendedCardInfoConfig, enums::FrmSuggestion, payments::ExtendedCardInfo};
|
||||
use async_trait::async_trait;
|
||||
use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
|
||||
use common_utils::ext_traits::{AsyncExt, Encode, StringExt, ValueExt};
|
||||
use error_stack::{report, ResultExt};
|
||||
use futures::FutureExt;
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
@ -28,6 +28,7 @@ use crate::{
|
||||
routes::{app::ReqState, AppState},
|
||||
services,
|
||||
types::{
|
||||
self,
|
||||
api::{self, ConnectorCallType, PaymentIdTypeExt},
|
||||
domain,
|
||||
storage::{self, enums as storage_enums},
|
||||
@ -587,18 +588,6 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
|
||||
.map(|(payment_method_data, additional_payment_data)| {
|
||||
payment_method_data.apply_additional_payment_data(additional_payment_data)
|
||||
});
|
||||
let authentication = payment_attempt.authentication_id.as_ref().async_map(|authentication_id| async move {
|
||||
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}"))
|
||||
}).await
|
||||
.transpose()?;
|
||||
|
||||
payment_attempt.payment_method_billing_address_id = payment_method_billing
|
||||
.as_ref()
|
||||
@ -644,7 +633,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
|
||||
incremental_authorization_details: None,
|
||||
authorizations: vec![],
|
||||
frm_metadata: request.frm_metadata.clone(),
|
||||
authentication,
|
||||
authentication: None,
|
||||
recurring_details,
|
||||
poll_config: None,
|
||||
};
|
||||
@ -785,117 +774,81 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
|
||||
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) })
|
||||
|
||||
.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(
|
||||
let external_authentication_flow =
|
||||
helpers::get_payment_external_authentication_flow_during_confirm(
|
||||
state,
|
||||
&business_profile.merchant_id,
|
||||
None,
|
||||
key_store,
|
||||
profile_id,
|
||||
&authentication_connector_name,
|
||||
None,
|
||||
business_profile,
|
||||
payment_data,
|
||||
connector_call_type,
|
||||
)
|
||||
.await?;
|
||||
if let Some(authentication) = authentication {
|
||||
// call post authn service
|
||||
authentication::perform_post_authentication(
|
||||
payment_data.authentication = match external_authentication_flow {
|
||||
Some(helpers::PaymentExternalAuthenticationFlow::PreAuthenticationFlow {
|
||||
acquirer_details,
|
||||
card_number,
|
||||
token,
|
||||
}) => {
|
||||
let authentication = authentication::perform_pre_authentication(
|
||||
state,
|
||||
authentication_connector_name.clone(),
|
||||
business_profile.clone(),
|
||||
authentication_connector_mca,
|
||||
authentication::types::PostAuthenthenticationFlowInput::PaymentAuthNFlow {
|
||||
payment_data,
|
||||
authentication,
|
||||
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,
|
||||
card_number,
|
||||
token,
|
||||
business_profile,
|
||||
Some(acquirer_details),
|
||||
Some(payment_data.payment_attempt.payment_id.clone()),
|
||||
)
|
||||
.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?;
|
||||
if authentication.is_separate_authn_required()
|
||||
|| authentication.authentication_status.is_failed()
|
||||
{
|
||||
*should_continue_confirm_transaction = false;
|
||||
let default_poll_config = types::PollConfig::default();
|
||||
let default_config_str = default_poll_config
|
||||
.encode_to_string_of_json()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while stringifying default poll config")?;
|
||||
let poll_config = state
|
||||
.store
|
||||
.find_config_by_key_unwrap_or(
|
||||
&types::PollConfig::get_poll_config_key(
|
||||
authentication.authentication_connector.clone(),
|
||||
),
|
||||
Some(default_config_str),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("The poll config was not found in the DB")?;
|
||||
let poll_config: types::PollConfig = poll_config
|
||||
.config
|
||||
.parse_struct("PollConfig")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Error while parsing PollConfig")?;
|
||||
payment_data.poll_config = Some(poll_config)
|
||||
}
|
||||
Some(authentication)
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
Some(helpers::PaymentExternalAuthenticationFlow::PostAuthenticationFlow {
|
||||
authentication_id,
|
||||
}) => {
|
||||
let authentication = authentication::perform_post_authentication(
|
||||
state,
|
||||
key_store,
|
||||
business_profile.clone(),
|
||||
authentication_id.clone(),
|
||||
)
|
||||
.await?;
|
||||
//If authentication is not successful, skip the payment connector flows and mark the payment as failure
|
||||
if authentication.authentication_status
|
||||
!= api_models::enums::AuthenticationStatus::Success
|
||||
{
|
||||
*should_continue_confirm_transaction = false;
|
||||
}
|
||||
Some(authentication)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
|
||||
@ -15,7 +15,7 @@ pub struct Authentication;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PostAuthentication;
|
||||
use crate::{connector, services, types};
|
||||
use crate::{connector, services, types, types::storage};
|
||||
|
||||
#[derive(Clone, serde::Deserialize, Debug, serde::Serialize)]
|
||||
pub struct AcquirerDetails {
|
||||
@ -34,6 +34,28 @@ pub struct AuthenticationResponse {
|
||||
pub acs_signed_content: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<storage::Authentication> for AuthenticationResponse {
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
fn try_from(authentication: storage::Authentication) -> Result<Self, Self::Error> {
|
||||
let trans_status = authentication.trans_status.ok_or(errors::ApiErrorResponse::InternalServerError).attach_printable("trans_status must be populated in authentication table authentication call is successful")?;
|
||||
let acs_url = authentication
|
||||
.acs_url
|
||||
.map(|url| url::Url::from_str(&url))
|
||||
.transpose()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("not a valid URL")?;
|
||||
Ok(Self {
|
||||
trans_status,
|
||||
acs_url,
|
||||
challenge_request: authentication.challenge_request,
|
||||
acs_reference_number: authentication.acs_reference_number,
|
||||
acs_trans_id: authentication.acs_trans_id,
|
||||
three_dsserver_trans_id: authentication.three_ds_server_trans_id,
|
||||
acs_signed_content: authentication.acs_signed_content,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, Debug, serde::Serialize)]
|
||||
pub struct PostAuthenticationResponse {
|
||||
pub trans_status: String,
|
||||
|
||||
Reference in New Issue
Block a user