mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
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:
committed by
GitHub
parent
d4dbaadb06
commit
00cd96d097
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user