feat(core): add support for webhook additional source verification call for paypal (#2058)

Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
This commit is contained in:
chikke srujan
2023-09-20 19:28:02 +05:30
committed by GitHub
parent 8ee2ce1f4f
commit 2a9e09d812
13 changed files with 719 additions and 77 deletions

View File

@ -116,6 +116,9 @@ locker_signing_key_id = "1" # Key_id to sign basilisk hs locker
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which has delayed session response
[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal" # List of connectors which has additional source verification api-call
[jwekey] # 4 priv/pub key pair
locker_key_identifier1 = "" # key identifier for key rotation , should be same as basilisk
locker_key_identifier2 = "" # key identifier for key rotation , should be same as basilisk

View File

@ -407,6 +407,9 @@ discord_invite_url = "https://discord.gg/wJZ7DVW8mm"
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme"
[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal"
[mandates.supported_payment_methods]
pay_later.klarna = { connector_list = "adyen" }
wallet.google_pay = { connector_list = "stripe,adyen" }

View File

@ -187,6 +187,8 @@ cards = [
[delayed_session_response]
connectors_with_delayed_session_response = "trustpay,payme"
[webhook_source_verification_call]
connectors_with_webhook_source_verification_call = "paypal"
[scheduler]
stream = "SCHEDULER_STREAM"

View File

@ -115,6 +115,7 @@ pub enum OutgoingWebhookContent {
DisputeDetails(Box<disputes::DisputeResponse>),
}
#[derive(Debug, Clone, Serialize)]
pub struct ConnectorWebhookSecrets {
pub secret: Vec<u8>,
pub additional_secret: Option<masking::Secret<String>>,

View File

@ -91,6 +91,7 @@ pub struct Settings {
pub mandates: Mandates,
pub required_fields: RequiredFields,
pub delayed_session_response: DelayedSessionConfig,
pub webhook_source_verification_call: WebhookSourceVerificationCall,
pub connector_request_reference_id_config: ConnectorRequestReferenceIdConfig,
#[cfg(feature = "payouts")]
pub payouts: Payouts,
@ -630,6 +631,12 @@ pub struct DelayedSessionConfig {
pub connectors_with_delayed_session_response: HashSet<api_models::enums::Connector>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct WebhookSourceVerificationCall {
#[serde(deserialize_with = "connector_deser")]
pub connectors_with_webhook_source_verification_call: HashSet<api_models::enums::Connector>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct ApplePayDecryptConifg {
pub apple_pay_ppc: String,

View File

@ -4,7 +4,7 @@ use std::fmt::Debug;
use base64::Engine;
use common_utils::ext_traits::ByteSliceExt;
use diesel_models::enums;
use error_stack::ResultExt;
use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface;
use transformers as paypal;
@ -28,8 +28,8 @@ use crate::{
},
types::{
self,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt},
domain, ErrorResponse, Response,
api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource},
ErrorResponse, Response,
},
utils::{self, BytesExt},
};
@ -50,6 +50,7 @@ impl api::PaymentVoid for Paypal {}
impl api::Refund for Paypal {}
impl api::RefundExecute for Paypal {}
impl api::RefundSync for Paypal {}
impl api::ConnectorVerifyWebhookSource for Paypal {}
impl Paypal {
pub fn get_order_error_response(
@ -569,9 +570,6 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
data: &types::PaymentsSyncRouterData,
res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
match data.payment_method {
diesel_models::enums::PaymentMethod::Wallet
| diesel_models::enums::PaymentMethod::BankRedirect => {
let response: paypal::PaypalSyncResponse = res
.response
.parse_struct("paypal SyncResponse")
@ -582,19 +580,6 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
http_code: res.status_code,
})
}
_ => {
let response: paypal::PaypalPaymentsSyncResponse = res
.response
.parse_struct("paypal PaymentsSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
}
}
fn get_error_response(
&self,
@ -909,18 +894,123 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
async fn verify_webhook_source(
impl
ConnectorIntegration<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> for Paypal
{
fn get_headers(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_account: &domain::MerchantAccount,
_merchant_connector_account: domain::MerchantConnectorAccount,
_connector_label: &str,
) -> CustomResult<bool, errors::ConnectorError> {
Ok(false) // Verify webhook source is not implemented for Paypal it requires additional apicall this function needs to be modified once we have a way to verify webhook source
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth: paypal::PaypalAuthType = (&req.connector_auth_type)
.try_into()
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let auth_id = auth
.key1
.zip(auth.api_key)
.map(|(key1, api_key)| format!("{}:{}", key1, api_key));
let auth_val = format!("Basic {}", consts::BASE64_ENGINE.encode(auth_id.peek()));
Ok(vec![
(
headers::CONTENT_TYPE.to_string(),
types::VerifyWebhookSourceType::get_content_type(self)
.to_string()
.into(),
),
(headers::AUTHORIZATION.to_string(), auth_val.into_masked()),
])
}
fn get_content_type(&self) -> &'static str {
self.common_get_content_type()
}
fn get_url(
&self,
_req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
Ok(format!(
"{}v1/notifications/verify-webhook-signature",
self.base_url(connectors)
))
}
fn build_request(
&self,
req: &types::VerifyWebhookSourceRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
let request = services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::VerifyWebhookSourceType::get_url(
self, req, connectors,
)?)
.headers(types::VerifyWebhookSourceType::get_headers(
self, req, connectors,
)?)
.body(types::VerifyWebhookSourceType::get_request_body(self, req)?)
.build();
Ok(Some(request))
}
fn get_request_body(
&self,
req: &types::RouterData<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
let req_obj = paypal::PaypalSourceVerificationRequest::try_from(&req.request)?;
let paypal_req = types::RequestBody::log_and_get_request_body(
&req_obj,
utils::Encode::<paypal::PaypalSourceVerificationRequest>::encode_to_string_of_json,
)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(paypal_req))
}
fn handle_response(
&self,
data: &types::VerifyWebhookSourceRouterData,
res: Response,
) -> CustomResult<types::VerifyWebhookSourceRouterData, errors::ConnectorError> {
let response: paypal::PaypalSourceVerificationResponse = res
.response
.parse_struct("paypal PaypalSourceVerificationResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
}
fn get_error_response(
&self,
res: Response,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res)
}
}
#[async_trait::async_trait]
impl api::IncomingWebhook for Paypal {
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
@ -972,13 +1062,29 @@ impl api::IncomingWebhook for Paypal {
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
let details: paypal::PaypalWebhooksBody = request
let details: paypal::PaypalWebhooksBody =
request
.body
.parse_struct("PaypalWebooksEventType")
.parse_struct("PaypalWebhooksBody")
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let res_json = utils::Encode::<transformers::PaypalWebhooksBody>::encode_to_value(&details)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(res_json)
let sync_payload = match details.resource {
paypal::PaypalResource::PaypalCardWebhooks(resource) => serde_json::to_value(
paypal::PaypalPaymentsSyncResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
paypal::PaypalResource::PaypalRedirectsWebhooks(resource) => serde_json::to_value(
paypal::PaypalOrdersResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
paypal::PaypalResource::PaypalRefundWebhooks(resource) => serde_json::to_value(
paypal::RefundSyncResponse::try_from((*resource, details.event_type))?,
)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?,
};
Ok(sync_payload)
}
}

View File

@ -1,5 +1,6 @@
use api_models::payments::BankRedirectData;
use common_utils::errors::CustomResult;
use error_stack::{IntoReport, ResultExt};
use masking::Secret;
use serde::{Deserialize, Serialize};
use url::Url;
@ -11,9 +12,20 @@ use crate::{
},
core::errors,
services,
types::{self, api, storage::enums as storage_enums, transformers::ForeignFrom},
types::{
self, api, storage::enums as storage_enums, transformers::ForeignFrom,
VerifyWebhookSourceResponseData,
},
};
mod webhook_headers {
pub const PAYPAL_TRANSMISSION_ID: &str = "paypal-transmission-id";
pub const PAYPAL_TRANSMISSION_TIME: &str = "paypal-transmission-time";
pub const PAYPAL_TRANSMISSION_SIG: &str = "paypal-transmission-sig";
pub const PAYPAL_CERT_URL: &str = "paypal-cert-url";
pub const PAYPAL_AUTH_ALGO: &str = "paypal-auth-algo";
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum PaypalPaymentIntent {
@ -590,8 +602,8 @@ pub struct PaymentsCollection {
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PurchaseUnitItem {
reference_id: String,
payments: PaymentsCollection,
pub reference_id: String,
pub payments: PaymentsCollection,
}
#[derive(Debug, Serialize, Deserialize)]
@ -621,6 +633,7 @@ pub struct PaypalRedirectResponse {
pub enum PaypalSyncResponse {
PaypalOrdersSyncResponse(PaypalOrdersResponse),
PaypalRedirectSyncResponse(PaypalRedirectResponse),
PaypalPaymentsSyncResponse(PaypalPaymentsSyncResponse),
}
#[derive(Debug, Serialize, Deserialize)]
@ -771,6 +784,13 @@ impl<F, T> TryFrom<types::ResponseRouterData<F, PaypalSyncResponse, T, types::Pa
http_code: item.http_code,
})
}
PaypalSyncResponse::PaypalPaymentsSyncResponse(response) => {
Self::try_from(types::ResponseRouterData {
response,
data: item.data,
http_code: item.http_code,
})
}
}
}
}
@ -1050,7 +1070,7 @@ impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RefundSyncResponse {
id: String,
status: RefundStatus,
@ -1167,7 +1187,10 @@ pub struct PaypalCardWebhooks {
#[derive(Deserialize, Debug, Serialize)]
pub struct PaypalRedirectsWebhooks {
pub purchase_units: Vec<PaypalWebhooksPurchaseUnits>,
pub purchase_units: Vec<PurchaseUnitItem>,
pub links: Vec<PaypalLinks>,
pub id: String,
pub intent: PaypalPaymentIntent,
}
#[derive(Deserialize, Debug, Serialize)]
@ -1196,13 +1219,222 @@ impl From<PaypalWebhookEventType> for api::IncomingWebhookEvent {
PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::CheckoutOrderCompleted => Self::PaymentIntentSuccess,
PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::CheckoutOrderProcessed => Self::PaymentIntentProcessing,
PaypalWebhookEventType::PaymentCaptureDeclined => Self::PaymentIntentFailure,
PaypalWebhookEventType::PaymentCaptureRefunded => Self::RefundSuccess,
PaypalWebhookEventType::Unknown
| PaypalWebhookEventType::PaymentAuthorizationCreated
| PaypalWebhookEventType::PaymentAuthorizationVoided => Self::EventNotSupported,
PaypalWebhookEventType::PaymentAuthorizationCreated
| PaypalWebhookEventType::PaymentAuthorizationVoided
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::Unknown => Self::EventNotSupported,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct PaypalSourceVerificationRequest {
pub transmission_id: String,
pub transmission_time: String,
pub cert_url: String,
pub transmission_sig: String,
pub auth_algo: String,
pub webhook_id: String,
pub webhook_event: serde_json::Value,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct PaypalSourceVerificationResponse {
pub verification_status: PaypalSourceVerificationStatus,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PaypalSourceVerificationStatus {
Success,
Failure,
}
impl
TryFrom<
types::ResponseRouterData<
api::VerifyWebhookSource,
PaypalSourceVerificationResponse,
types::VerifyWebhookSourceRequestData,
VerifyWebhookSourceResponseData,
>,
> for types::VerifyWebhookSourceRouterData
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
api::VerifyWebhookSource,
PaypalSourceVerificationResponse,
types::VerifyWebhookSourceRequestData,
VerifyWebhookSourceResponseData,
>,
) -> Result<Self, Self::Error> {
Ok(Self {
response: Ok(VerifyWebhookSourceResponseData {
verify_webhook_status: types::VerifyWebhookStatus::from(
item.response.verification_status,
),
}),
..item.data
})
}
}
impl From<PaypalSourceVerificationStatus> for types::VerifyWebhookStatus {
fn from(item: PaypalSourceVerificationStatus) -> Self {
match item {
PaypalSourceVerificationStatus::Success => Self::SourceVerified,
PaypalSourceVerificationStatus::Failure => Self::SourceNotVerified,
}
}
}
impl TryFrom<(PaypalCardWebhooks, PaypalWebhookEventType)> for PaypalPaymentsSyncResponse {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(webhook_body, webhook_event): (PaypalCardWebhooks, PaypalWebhookEventType),
) -> Result<Self, Self::Error> {
Ok(Self {
id: webhook_body.supplementary_data.related_ids.order_id.clone(),
status: PaypalPaymentStatus::try_from(webhook_event)?,
amount: webhook_body.amount,
supplementary_data: webhook_body.supplementary_data,
})
}
}
impl TryFrom<(PaypalRedirectsWebhooks, PaypalWebhookEventType)> for PaypalOrdersResponse {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(webhook_body, webhook_event): (PaypalRedirectsWebhooks, PaypalWebhookEventType),
) -> Result<Self, Self::Error> {
Ok(Self {
id: webhook_body.id,
intent: webhook_body.intent,
status: PaypalOrderStatus::try_from(webhook_event)?,
purchase_units: webhook_body.purchase_units,
})
}
}
impl TryFrom<(PaypalRefundWebhooks, PaypalWebhookEventType)> for RefundSyncResponse {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(webhook_body, webhook_event): (PaypalRefundWebhooks, PaypalWebhookEventType),
) -> Result<Self, Self::Error> {
Ok(Self {
id: webhook_body.id,
status: RefundStatus::try_from(webhook_event)
.attach_printable("Could not find suitable webhook event")?,
})
}
}
impl TryFrom<PaypalWebhookEventType> for PaypalPaymentStatus {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
match event {
PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::CheckoutOrderCompleted => Ok(Self::Completed),
PaypalWebhookEventType::PaymentAuthorizationVoided => Ok(Self::Voided),
PaypalWebhookEventType::PaymentCaptureDeclined => Ok(Self::Declined),
PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending),
PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created),
PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Refunded),
PaypalWebhookEventType::Unknown => {
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
}
}
}
}
impl TryFrom<PaypalWebhookEventType> for RefundStatus {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
match event {
PaypalWebhookEventType::PaymentCaptureRefunded => Ok(Self::Completed),
PaypalWebhookEventType::PaymentAuthorizationCreated
| PaypalWebhookEventType::PaymentAuthorizationVoided
| PaypalWebhookEventType::PaymentCaptureDeclined
| PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::CheckoutOrderCompleted
| PaypalWebhookEventType::CheckoutOrderProcessed
| PaypalWebhookEventType::Unknown => {
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
}
}
}
}
impl TryFrom<PaypalWebhookEventType> for PaypalOrderStatus {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(event: PaypalWebhookEventType) -> Result<Self, Self::Error> {
match event {
PaypalWebhookEventType::PaymentCaptureCompleted
| PaypalWebhookEventType::CheckoutOrderCompleted => Ok(Self::Completed),
PaypalWebhookEventType::PaymentAuthorizationVoided => Ok(Self::Voided),
PaypalWebhookEventType::PaymentCapturePending
| PaypalWebhookEventType::CheckoutOrderProcessed => Ok(Self::Pending),
PaypalWebhookEventType::PaymentAuthorizationCreated => Ok(Self::Created),
PaypalWebhookEventType::CheckoutOrderApproved
| PaypalWebhookEventType::PaymentCaptureDeclined
| PaypalWebhookEventType::PaymentCaptureRefunded
| PaypalWebhookEventType::Unknown => {
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
}
}
}
}
impl TryFrom<&types::VerifyWebhookSourceRequestData> for PaypalSourceVerificationRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(req: &types::VerifyWebhookSourceRequestData) -> Result<Self, Self::Error> {
let req_body = serde_json::from_slice(&req.webhook_body)
.into_report()
.change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?;
Ok(Self {
transmission_id: get_headers(
&req.webhook_headers,
webhook_headers::PAYPAL_TRANSMISSION_ID,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?,
transmission_time: get_headers(
&req.webhook_headers,
webhook_headers::PAYPAL_TRANSMISSION_TIME,
)?,
cert_url: get_headers(&req.webhook_headers, webhook_headers::PAYPAL_CERT_URL)?,
transmission_sig: get_headers(
&req.webhook_headers,
webhook_headers::PAYPAL_TRANSMISSION_SIG,
)?,
auth_algo: get_headers(&req.webhook_headers, webhook_headers::PAYPAL_AUTH_ALGO)?,
webhook_id: String::from_utf8(req.merchant_secret.secret.to_vec())
.into_report()
.change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)
.attach_printable("Could not convert secret to UTF-8")?,
webhook_event: req_body,
})
}
}
fn get_headers(
header: &actix_web::http::header::HeaderMap,
key: &'static str,
) -> CustomResult<String, errors::ConnectorError> {
let header_value = header
.get(key.clone())
.map(|value| value.to_str())
.ok_or(errors::ConnectorError::MissingRequiredField { field_name: key })?
.into_report()
.change_context(errors::ConnectorError::InvalidDataFormat { field_name: key })?
.to_owned();
Ok(header_value)
}

View File

@ -178,6 +178,80 @@ default_imp_for_complete_authorize!(
connector::Worldpay,
connector::Zen
);
macro_rules! default_imp_for_webhook_source_verification {
($($path:ident::$connector:ident),*) => {
$(
impl api::ConnectorVerifyWebhookSource for $path::$connector {}
impl
services::ConnectorIntegration<
api::VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> for $path::$connector
{}
)*
};
}
#[cfg(feature = "dummy_connector")]
impl<const T: u8> api::ConnectorVerifyWebhookSource for connector::DummyConnector<T> {}
#[cfg(feature = "dummy_connector")]
impl<const T: u8>
services::ConnectorIntegration<
api::VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> for connector::DummyConnector<T>
{
}
default_imp_for_webhook_source_verification!(
connector::Aci,
connector::Adyen,
connector::Airwallex,
connector::Authorizedotnet,
connector::Bambora,
connector::Bitpay,
connector::Bluesnap,
connector::Braintree,
connector::Boku,
connector::Cashtocode,
connector::Checkout,
connector::Coinbase,
connector::Cryptopay,
connector::Cybersource,
connector::Dlocal,
connector::Fiserv,
connector::Forte,
connector::Globalpay,
connector::Globepay,
connector::Gocardless,
connector::Helcim,
connector::Iatapay,
connector::Klarna,
connector::Mollie,
connector::Multisafepay,
connector::Nexinets,
connector::Nmi,
connector::Noon,
connector::Nuvei,
connector::Opayo,
connector::Opennode,
connector::Payeezy,
connector::Payme,
connector::Payu,
connector::Powertranz,
connector::Rapyd,
connector::Shift4,
connector::Square,
connector::Stax,
connector::Stripe,
connector::Trustpay,
connector::Tsys,
connector::Wise,
connector::Worldline,
connector::Worldpay,
connector::Zen
);
macro_rules! default_imp_for_create_customer {
($($path:ident::$connector:ident),*) => {

View File

@ -1,6 +1,8 @@
pub mod types;
pub mod utils;
use std::str::FromStr;
use api_models::payments::HeaderPayload;
use common_utils::errors::ReportSwitchExt;
use error_stack::{report, IntoReport, ResultExt};
@ -806,8 +808,40 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
.get_webhook_object_reference_id(&request_details)
.switch()
.attach_printable("Could not find object reference id in incoming webhook body")?;
let connector_enum = api_models::enums::Connector::from_str(&connector_name)
.into_report()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "connector",
})
.attach_printable_lazy(|| {
format!("unable to parse connector name {connector_name:?}")
})?;
let connectors_with_source_verification_call = &state.conf.webhook_source_verification_call;
let source_verified = connector
let source_verified = if connectors_with_source_verification_call
.connectors_with_webhook_source_verification_call
.contains(&connector_enum)
{
connector
.verify_webhook_source_verification_call(
&state,
&merchant_account,
merchant_connector_account.clone(),
&connector_name,
&request_details,
)
.await
.or_else(|error| match error.current_context() {
errors::ConnectorError::WebhookSourceVerificationFailed => {
logger::error!(?error, "Source Verification Failed");
Ok(false)
}
_ => Err(error),
})
.switch()
.attach_printable("There was an issue in incoming webhook source verification")?
} else {
connector
.verify_webhook_source(
&request_details,
&merchant_account,
@ -823,7 +857,8 @@ pub async fn webhooks_core<W: types::OutgoingWebhookType>(
_ => Err(error),
})
.switch()
.attach_printable("There was an issue in incoming webhook source verification")?;
.attach_printable("There was an issue in incoming webhook source verification")?
};
if source_verified {
metrics::WEBHOOK_SOURCE_VERIFIED_COUNT.add(

View File

@ -1,6 +1,15 @@
use std::marker::PhantomData;
use common_utils::{errors::CustomResult, ext_traits::ValueExt};
use error_stack::ResultExt;
use crate::{
core::{
errors::{self},
payments::helpers,
},
db::{get_and_deserialize_key, StorageInterface},
types::api,
types::{self, api, domain, PaymentAddress},
};
fn default_webhook_config() -> api::MerchantWebhookConfig {
@ -13,6 +22,13 @@ fn default_webhook_config() -> api::MerchantWebhookConfig {
])
}
const IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
"irrelevant_payment_id_in_source_verification_flow";
const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
"irrelevant_attempt_id_in_source_verification_flow";
const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW: &str =
"irrelevant_connector_request_reference_id_in_source_verification_flow";
pub async fn lookup_webhook_event(
db: &dyn StorageInterface,
connector_id: &str,
@ -45,3 +61,60 @@ pub async fn lookup_webhook_event(
}
}
}
pub async fn construct_webhook_router_data<'a>(
connector_name: &str,
merchant_connector_account: domain::MerchantConnectorAccount,
merchant_account: &domain::MerchantAccount,
connector_wh_secrets: &api_models::webhooks::ConnectorWebhookSecrets,
request_details: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<types::VerifyWebhookSourceRouterData, errors::ApiErrorResponse> {
let auth_type: types::ConnectorAuthType =
helpers::MerchantConnectorAccountType::DbVal(merchant_connector_account.clone())
.get_connector_account_details()
.parse_value("ConnectorAuthType")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.merchant_id.clone(),
connector: connector_name.to_string(),
customer_id: None,
payment_id: IRRELEVANT_PAYMENT_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
attempt_id: IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
status: diesel_models::enums::AttemptStatus::default(),
payment_method: diesel_models::enums::PaymentMethod::default(),
connector_auth_type: auth_type,
description: None,
return_url: None,
payment_method_id: None,
address: PaymentAddress::default(),
auth_type: diesel_models::enums::AuthenticationType::default(),
connector_meta_data: None,
amount_captured: None,
request: types::VerifyWebhookSourceRequestData {
webhook_headers: request_details.headers.clone(),
webhook_body: request_details.body.to_vec().clone(),
merchant_secret: connector_wh_secrets.to_owned(),
},
response: Err(types::ErrorResponse::default()),
access_token: None,
session_token: None,
reference_id: None,
payment_method_token: None,
connector_customer: None,
recurring_mandate_payment_data: None,
preprocessing_id: None,
connector_request_reference_id:
IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_SOURCE_VERIFICATION_FLOW.to_string(),
#[cfg(feature = "payouts")]
payout_method_data: None,
#[cfg(feature = "payouts")]
quote_id: None,
test_mode: None,
payment_method_balance: None,
connector_api_version: None,
connector_http_status_code: None,
};
Ok(router_data)
}

View File

@ -177,6 +177,11 @@ pub type AcceptDisputeType = dyn services::ConnectorIntegration<
AcceptDisputeRequestData,
AcceptDisputeResponse,
>;
pub type VerifyWebhookSourceType = dyn services::ConnectorIntegration<
api::VerifyWebhookSource,
VerifyWebhookSourceRequestData,
VerifyWebhookSourceResponseData,
>;
pub type SubmitEvidenceType = dyn services::ConnectorIntegration<
api::Evidence,
@ -204,6 +209,12 @@ pub type VerifyRouterData = RouterData<api::Verify, VerifyRequestData, PaymentsR
pub type AcceptDisputeRouterData =
RouterData<api::Accept, AcceptDisputeRequestData, AcceptDisputeResponse>;
pub type VerifyWebhookSourceRouterData = RouterData<
api::VerifyWebhookSource,
VerifyWebhookSourceRequestData,
VerifyWebhookSourceResponseData,
>;
pub type SubmitEvidenceRouterData =
RouterData<api::Evidence, SubmitEvidenceRequestData, SubmitEvidenceResponse>;
@ -720,6 +731,24 @@ pub enum Redirection {
NoRedirect,
}
#[derive(Debug, Clone)]
pub struct VerifyWebhookSourceRequestData {
pub webhook_headers: actix_web::http::header::HeaderMap,
pub webhook_body: Vec<u8>,
pub merchant_secret: api_models::webhooks::ConnectorWebhookSecrets,
}
#[derive(Debug, Clone)]
pub struct VerifyWebhookSourceResponseData {
pub verify_webhook_status: VerifyWebhookStatus,
}
#[derive(Debug, Clone)]
pub enum VerifyWebhookStatus {
SourceVerified,
SourceNotVerified,
}
#[derive(Default, Debug, Clone)]
pub struct AcceptDisputeRequestData {
pub dispute_id: String,

View File

@ -38,6 +38,18 @@ pub trait ConnectorAccessToken:
{
}
#[derive(Clone, Debug)]
pub struct VerifyWebhookSource;
pub trait ConnectorVerifyWebhookSource:
ConnectorIntegration<
VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
>
{
}
pub trait ConnectorTransactionId: ConnectorCommon + Sync {
fn connector_transaction_id(
&self,
@ -123,6 +135,7 @@ pub trait Connector:
+ FileUpload
+ ConnectorTransactionId
+ Payouts
+ ConnectorVerifyWebhookSource
{
}
@ -141,7 +154,8 @@ impl<
+ Dispute
+ FileUpload
+ ConnectorTransactionId
+ Payouts,
+ Payouts
+ ConnectorVerifyWebhookSource,
> Connector for T
{
}

View File

@ -9,10 +9,14 @@ use masking::ExposeInterface;
use super::ConnectorCommon;
use crate::{
core::errors::{self, CustomResult},
core::{
errors::{self, CustomResult},
payments,
webhooks::utils::construct_webhook_router_data,
},
db::StorageInterface,
services,
types::domain,
services::{self},
types::{self, domain},
utils::crypto,
};
@ -139,12 +143,71 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
Ok(Vec::new())
}
async fn verify_webhook_source_verification_call(
&self,
state: &crate::routes::AppState,
merchant_account: &domain::MerchantAccount,
merchant_connector_account: domain::MerchantConnectorAccount,
connector_name: &str,
request_details: &IncomingWebhookRequestDetails<'_>,
) -> CustomResult<bool, errors::ConnectorError> {
let connector_data = types::api::ConnectorData::get_connector_by_name(
&state.conf.connectors,
connector_name,
types::api::GetToken::Connector,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("invalid connector name received in payment attempt")?;
let connector_integration: services::BoxedConnectorIntegration<
'_,
types::api::VerifyWebhookSource,
types::VerifyWebhookSourceRequestData,
types::VerifyWebhookSourceResponseData,
> = connector_data.connector.get_connector_integration();
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
merchant_account,
connector_name,
merchant_connector_account.clone(),
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let router_data = construct_webhook_router_data(
connector_name,
merchant_connector_account,
merchant_account,
&connector_webhook_secrets,
request_details,
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Failed while constructing webhook router data")?;
let response = services::execute_connector_processing_step(
state,
connector_integration,
&router_data,
payments::CallConnectorAction::Trigger,
None,
)
.await?;
let verification_result = response
.response
.map(|response| response.verify_webhook_status);
match verification_result {
Ok(types::VerifyWebhookStatus::SourceVerified) => Ok(true),
_ => Ok(false),
}
}
async fn verify_webhook_source(
&self,
request: &IncomingWebhookRequestDetails<'_>,
merchant_account: &domain::MerchantAccount,
merchant_connector_account: domain::MerchantConnectorAccount,
connector_label: &str,
connector_name: &str,
) -> CustomResult<bool, errors::ConnectorError> {
let algorithm = self
.get_webhook_source_verification_algorithm(request)
@ -156,7 +219,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
merchant_account,
connector_label,
connector_name,
merchant_connector_account,
)
.await