feat(router): add external authentication webhooks flow (#4339)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sai Harsha Vardhan
2024-04-16 15:54:46 +05:30
committed by GitHub
parent d4dbaadb06
commit 00cd96d097
22 changed files with 512 additions and 95 deletions

View File

@ -50,7 +50,7 @@ use crate::{
api::{self, mandates::MandateResponseExt},
domain::{self, types as domain_types},
storage::{self, enums},
transformers::{ForeignInto, ForeignTryFrom},
transformers::{ForeignFrom, ForeignInto, ForeignTryFrom},
},
utils::{self as helper_utils, generate_id, OptionExt, ValueExt},
workflows::outgoing_webhook_retry,
@ -416,6 +416,162 @@ pub async fn get_or_update_dispute_object(
}
}
#[allow(clippy::too_many_arguments)]
pub async fn external_authentication_incoming_webhook_flow<Ctx: PaymentMethodRetrieve>(
state: AppState,
req_state: ReqState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
source_verified: bool,
event_type: api_models::webhooks::IncomingWebhookEvent,
request_details: &api::IncomingWebhookRequestDetails<'_>,
connector: &(dyn api::Connector + Sync),
object_ref_id: api::ObjectReferenceId,
business_profile: diesel_models::business_profile::BusinessProfile,
merchant_connector_account: domain::MerchantConnectorAccount,
) -> CustomResult<WebhookResponseTracker, errors::ApiErrorResponse> {
if source_verified {
let authentication_details = connector
.get_external_authentication_details(request_details)
.switch()?;
let trans_status = authentication_details.trans_status;
let authentication_update = storage::AuthenticationUpdate::PostAuthenticationUpdate {
authentication_status: common_enums::AuthenticationStatus::foreign_from(
trans_status.clone(),
),
trans_status,
authentication_value: authentication_details.authentication_value,
eci: authentication_details.eci,
};
let authentication =
if let webhooks::ObjectReferenceId::ExternalAuthenticationID(authentication_id_type) =
object_ref_id
{
match authentication_id_type {
webhooks::AuthenticationIdType::AuthenticationId(authentication_id) => state
.store
.find_authentication_by_merchant_id_authentication_id(
merchant_account.merchant_id.clone(),
authentication_id.clone(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::AuthenticationNotFound {
id: authentication_id,
})
.attach_printable("Error while fetching authentication record"),
webhooks::AuthenticationIdType::ConnectorAuthenticationId(
connector_authentication_id,
) => state
.store
.find_authentication_by_merchant_id_connector_authentication_id(
merchant_account.merchant_id.clone(),
connector_authentication_id.clone(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::AuthenticationNotFound {
id: connector_authentication_id,
})
.attach_printable("Error while fetching authentication record"),
}
} else {
Err(errors::ApiErrorResponse::WebhookProcessingFailure).attach_printable(
"received a non-external-authentication id for retrieving authentication",
)
}?;
let updated_authentication = state
.store
.update_authentication_by_merchant_id_authentication_id(
authentication,
authentication_update,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error while updating authentication")?;
// Check if it's a payment authentication flow, payment_id would be there only for payment authentication flows
if let Some(payment_id) = updated_authentication.payment_id {
let is_pull_mechanism_enabled = helper_utils::check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata(merchant_connector_account.metadata.map(|metadata| metadata.expose()));
// Merchant doesn't have pull mechanism enabled, so we have to authorize whenever we receive a ARes webhook
if !is_pull_mechanism_enabled
&& event_type == webhooks::IncomingWebhookEvent::ExternalAuthenticationARes
{
let payment_confirm_req = api::PaymentsRequest {
payment_id: Some(api_models::payments::PaymentIdType::PaymentIntentId(
payment_id,
)),
merchant_id: Some(merchant_account.merchant_id.clone()),
..Default::default()
};
let payments_response = Box::pin(payments::payments_core::<
api::Authorize,
api::PaymentsResponse,
_,
_,
_,
Ctx,
>(
state.clone(),
req_state,
merchant_account.clone(),
key_store.clone(),
payments::PaymentConfirm,
payment_confirm_req,
services::api::AuthFlow::Merchant,
payments::CallConnectorAction::Trigger,
None,
HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator),
))
.await?;
match payments_response {
services::ApplicationResponse::JsonWithHeaders((payments_response, _)) => {
let payment_id = payments_response
.payment_id
.clone()
.get_required_value("payment_id")
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment id not received from payments core")?;
let status = payments_response.status;
let event_type: Option<enums::EventType> =
payments_response.status.foreign_into();
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
&key_store,
outgoing_event_type,
enums::EventClass::Payments,
payment_id.clone(),
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
primary_object_created_at,
)
.await?;
};
let response = WebhookResponseTracker::Payment { payment_id, status };
Ok(response)
}
_ => Err(errors::ApiErrorResponse::WebhookProcessingFailure).attach_printable(
"Did not get payment id as object reference id in webhook payments flow",
)?,
}
} else {
Ok(WebhookResponseTracker::NoEffect)
}
} else {
Ok(WebhookResponseTracker::NoEffect)
}
} else {
logger::error!(
"Webhook source verification failed for external authentication webhook flow"
);
Err(report!(
errors::ApiErrorResponse::WebhookAuthenticationFailed
))
}
}
pub async fn mandates_incoming_webhook_flow(
state: AppState,
merchant_account: domain::MerchantAccount,
@ -1365,7 +1521,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
// Fetch the merchant connector account to get the webhooks source secret
// `webhooks source secret` is a secret shared between the merchant and connector
// This is used for source verification and webhooks integrity
let (merchant_connector_account, connector) = fetch_optional_mca_and_connector(
let (merchant_connector_account, connector, connector_name) = fetch_optional_mca_and_connector(
&state,
&merchant_account,
connector_name_or_mca_id,
@ -1373,10 +1529,6 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
)
.await?;
let connector_name = connector.connector_name.to_string();
let connector = connector.connector;
let decoded_body = connector
.decode_webhook_body(
&*state.clone().store,
@ -1548,7 +1700,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
.attach_printable("Could not find resource object in incoming webhook body")?;
let webhook_details = api::IncomingWebhookDetails {
object_reference_id: object_ref_id,
object_reference_id: object_ref_id.clone(),
resource_object: serde_json::to_vec(&event_object)
.change_context(errors::ParsingError::EncodeError("byte-vec"))
.attach_printable("Unable to convert webhook payload to a value")
@ -1606,7 +1758,7 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
key_store,
webhook_details,
source_verified,
*connector,
connector,
&request_details,
event_type,
))
@ -1639,6 +1791,24 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType, Ctx: PaymentMethodRetr
.await
.attach_printable("Incoming webhook flow for mandates failed")?,
api::WebhookFlow::ExternalAuthentication => {
Box::pin(external_authentication_incoming_webhook_flow::<Ctx>(
state.clone(),
req_state,
merchant_account,
key_store,
source_verified,
event_type,
&request_details,
connector,
object_ref_id,
business_profile,
merchant_connector_account,
))
.await
.attach_printable("Incoming webhook flow for external authentication failed")?
}
_ => Err(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unsupported Flow Type received in incoming webhooks")?,
}
@ -1704,6 +1874,39 @@ pub async fn get_payment_id(
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
}
fn get_connector_by_connector_name(
state: &AppState,
connector_name: &str,
merchant_connector_id: Option<String>,
) -> CustomResult<(&'static (dyn api::Connector + Sync), String), errors::ApiErrorResponse> {
let authentication_connector =
api_models::enums::convert_authentication_connector(connector_name);
let (connector, connector_name) = if authentication_connector.is_some() {
let authentication_connector_data =
api::AuthenticationConnectorData::get_connector_by_name(connector_name)?;
(
authentication_connector_data.connector,
authentication_connector_data.connector_name.to_string(),
)
} else {
let connector_data = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
connector_name,
api::GetToken::Connector,
merchant_connector_id,
)
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "invalid connector name received".to_string(),
})
.attach_printable("Failed construction of ConnectorData")?;
(
connector_data.connector,
connector_data.connector_name.to_string(),
)
};
Ok((*connector, connector_name))
}
/// This function fetches the merchant connector account ( if the url used is /{merchant_connector_id})
/// if merchant connector id is not passed in the request, then this will return None for mca
async fn fetch_optional_mca_and_connector(
@ -1712,7 +1915,11 @@ async fn fetch_optional_mca_and_connector(
connector_name_or_mca_id: &str,
key_store: &domain::MerchantKeyStore,
) -> CustomResult<
(Option<domain::MerchantConnectorAccount>, api::ConnectorData),
(
Option<domain::MerchantConnectorAccount>,
&'static (dyn api::Connector + Sync),
String,
),
errors::ApiErrorResponse,
> {
let db = &state.store;
@ -1730,33 +1937,18 @@ async fn fetch_optional_mca_and_connector(
.attach_printable(
"error while fetching merchant_connector_account from connector_id",
)?;
let connector = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
let (connector, connector_name) = get_connector_by_connector_name(
state,
&mca.connector_name,
api::GetToken::Connector,
Some(mca.merchant_connector_id.clone()),
)
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "invalid connector name received".to_string(),
})
.attach_printable("Failed construction of ConnectorData")?;
)?;
Ok((Some(mca), connector))
Ok((Some(mca), connector, connector_name))
} else {
// Merchant connector account is already being queried, it is safe to set connector id as None
let connector = api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
connector_name_or_mca_id,
api::GetToken::Connector,
None,
)
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "invalid connector name received".to_string(),
})
.attach_printable("Failed construction of ConnectorData")?;
Ok((None, connector))
let (connector, connector_name) =
get_connector_by_connector_name(state, connector_name_or_mca_id, None)?;
Ok((None, connector, connector_name))
}
}