diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 39e49f1ff4b7..f271c6e3629c 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -87,6 +87,8 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } openssl = "0.10.55" x509-parser = "0.15.0" +sha-1 = { version = "0.9"} +digest = "0.9" # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index 8347e43757b4..18a575b509cb 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -1436,6 +1436,7 @@ impl api::IncomingWebhook for Adyen { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let notif_item = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; @@ -1448,7 +1449,7 @@ impl api::IncomingWebhook for Adyen { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(request.body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; @@ -1475,9 +1476,6 @@ impl api::IncomingWebhook for Adyen { merchant_connector_account: domain::MerchantConnectorAccount, connector_label: &str, ) -> CustomResult { - let signature = self - .get_webhook_source_verification_signature(request) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, @@ -1486,11 +1484,16 @@ impl api::IncomingWebhook for Adyen { ) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self .get_webhook_source_verification_message( request, &merchant_account.merchant_id, - &connector_webhook_secrets.secret, + &connector_webhook_secrets, ) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; diff --git a/crates/router/src/connector/airwallex.rs b/crates/router/src/connector/airwallex.rs index de1947d81817..8a22c14dd320 100644 --- a/crates/router/src/connector/airwallex.rs +++ b/crates/router/src/connector/airwallex.rs @@ -946,6 +946,7 @@ impl api::IncomingWebhook for Airwallex { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let security_header = request .headers @@ -969,7 +970,7 @@ impl api::IncomingWebhook for Airwallex { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let timestamp = request .headers diff --git a/crates/router/src/connector/authorizedotnet.rs b/crates/router/src/connector/authorizedotnet.rs index 3f5b0a22b2c6..8e98fc584320 100644 --- a/crates/router/src/connector/authorizedotnet.rs +++ b/crates/router/src/connector/authorizedotnet.rs @@ -745,6 +745,7 @@ impl api::IncomingWebhook for Authorizedotnet { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let security_header = request .headers @@ -772,7 +773,7 @@ impl api::IncomingWebhook for Authorizedotnet { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(request.body.to_vec()) } diff --git a/crates/router/src/connector/bluesnap.rs b/crates/router/src/connector/bluesnap.rs index 00901a55ba36..7a867ed56caf 100644 --- a/crates/router/src/connector/bluesnap.rs +++ b/crates/router/src/connector/bluesnap.rs @@ -984,6 +984,7 @@ impl api::IncomingWebhook for Bluesnap { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let security_header = connector_utils::get_header_key_value("bls-signature", request.headers)?; @@ -996,11 +997,10 @@ impl api::IncomingWebhook for Bluesnap { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let timestamp = connector_utils::get_header_key_value("bls-ipn-timestamp", request.headers)?; - Ok(format!("{}{}", timestamp, String::from_utf8_lossy(request.body)).into_bytes()) } diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 6a6411082830..ff9e93c2e9ff 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1,11 +1,15 @@ pub mod braintree_graphql_transformers; pub mod transformers; +use std::{fmt::Debug, str::FromStr}; -use std::fmt::Debug; - +use api_models::webhooks::IncomingWebhookEvent; +use base64::Engine; +use common_utils::{crypto, ext_traits::XmlExt}; use diesel_models::enums; use error_stack::{IntoReport, Report, ResultExt}; -use masking::PeekInterface; +use masking::{ExposeInterface, PeekInterface}; +use ring::hmac; +use sha1::{Digest, Sha1}; use self::transformers as braintree; use super::utils::PaymentsAuthorizeRequestData; @@ -26,6 +30,8 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, + domain, + transformers::ForeignFrom, ErrorResponse, }, utils::{self, BytesExt}, @@ -1264,28 +1270,228 @@ impl ConnectorIntegration, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha1)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notif_item = get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature_pairs: Vec<(&str, &str)> = notif_item + .bt_signature + .split('&') + .collect::>() + .into_iter() + .map(|pair| pair.split_once('|').unwrap_or(("", ""))) + .collect::>(); + + let merchant_secret = connector_webhook_secrets + .additional_secret //public key + .clone() + .ok_or(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + + let signature = get_matching_webhook_signature(signature_pairs, merchant_secret.expose()) + .ok_or(errors::ConnectorError::WebhookSignatureNotFound)?; + Ok(signature.as_bytes().to_vec()) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notify = get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let message = notify.bt_payload.to_string(); + + Ok(message.into_bytes()) + } + + async fn verify_webhook_source( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + merchant_account: &domain::MerchantAccount, + merchant_connector_account: domain::MerchantConnectorAccount, + connector_label: &str, + ) -> CustomResult { + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_account, + connector_label, + merchant_connector_account, + ) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; + + let message = self + .get_webhook_source_verification_message( + request, + &merchant_account.merchant_id, + &connector_webhook_secrets, + ) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let sha1_hash_key = Sha1::digest(&connector_webhook_secrets.secret); + + let signing_key = hmac::Key::new( + hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + sha1_hash_key.as_slice(), + ); + let signed_messaged = hmac::sign(&signing_key, &message); + let payload_sign: String = hex::encode(signed_messaged); + Ok(payload_sign.as_bytes().eq(&signature)) + } + fn get_webhook_object_reference_id( &self, _request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let notif = get_webhook_object_from_body(_request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; + + match response.dispute { + Some(dispute_data) => Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + dispute_data.transaction.id, + ), + )), + None => Err(errors::ConnectorError::WebhookReferenceIdNotFound).into_report(), + } } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, - ) -> CustomResult { - Ok(api::IncomingWebhookEvent::EventNotSupported) + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let notif = get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; + + Ok(IncomingWebhookEvent::foreign_from(response.kind.as_str())) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let notif = get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; + + let res_json = serde_json::to_value(response) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(res_json) + } + + fn get_webhook_api_response( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> + { + Ok(services::api::ApplicationResponse::TextPlain( + "[accepted]".to_string(), + )) + } + + fn get_dispute_details( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + let notif = get_webhook_object_from_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let response = decode_webhook_payload(notif.bt_payload.replace('\n', "").as_bytes())?; + + match response.dispute { + Some(dispute_data) => { + let currency = diesel_models::enums::Currency::from_str( + dispute_data.currency_iso_code.as_str(), + ) + .into_report() + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api::disputes::DisputePayload { + amount: connector_utils::to_currency_lower_unit( + dispute_data.amount_disputed.to_string(), + currency, + )?, + currency: dispute_data.currency_iso_code, + dispute_stage: braintree_graphql_transformers::get_dispute_stage( + dispute_data.kind.as_str(), + )?, + connector_dispute_id: dispute_data.id, + connector_reason: dispute_data.reason, + connector_reason_code: dispute_data.reason_code, + challenge_required_by: dispute_data.reply_by_date, + connector_status: dispute_data.status, + created_at: dispute_data.created_at, + updated_at: dispute_data.updated_at, + }) + } + None => Err(errors::ConnectorError::WebhookResourceObjectNotFound)?, + } } } +fn get_matching_webhook_signature( + signature_pairs: Vec<(&str, &str)>, + secret: String, +) -> Option { + for (public_key, signature) in signature_pairs { + if *public_key == secret { + return Some(signature.to_string()); + } + } + None +} + +fn get_webhook_object_from_body( + body: &[u8], +) -> CustomResult { + serde_urlencoded::from_bytes::(body) + .into_report() + .change_context(errors::ParsingError::StructParseFailure( + "BraintreeWebhookResponse", + )) +} + +fn decode_webhook_payload( + payload: &[u8], +) -> CustomResult { + let decoded_response = consts::BASE64_ENGINE + .decode(payload) + .into_report() + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let xml_response = String::from_utf8(decoded_response) + .into_report() + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + xml_response + .parse_xml::() + .into_report() + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) +} + impl services::ConnectorRedirectResponse for Braintree { fn get_flow_type( &self, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index fc6a057606d5..8e9b15ef461a 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -1,6 +1,7 @@ use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use crate::{ connector::utils::{self, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData}, @@ -1411,3 +1412,74 @@ fn get_braintree_redirect_form( amount, }) } + +#[derive(Debug, Deserialize)] +pub struct BraintreeWebhookResponse { + pub bt_signature: String, + pub bt_payload: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Notification { + pub kind: String, // xml parse only string to fields + pub timestamp: String, + pub dispute: Option, +} +impl types::transformers::ForeignFrom<&str> for api_models::webhooks::IncomingWebhookEvent { + fn foreign_from(status: &str) -> Self { + match status { + "dispute_opened" => Self::DisputeOpened, + "dispute_lost" => Self::DisputeLost, + "dispute_won" => Self::DisputeWon, + "dispute_accepted" | "dispute_auto_accepted" => Self::DisputeAccepted, + "dispute_expired" => Self::DisputeExpired, + "dispute_disputed" => Self::DisputeChallenged, + _ => Self::EventNotSupported, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BraintreeDisputeData { + pub amount_disputed: i64, + pub amount_won: Option, + pub case_number: Option, + pub chargeback_protection_level: Option, + pub currency_iso_code: String, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub created_at: Option, + pub evidence: Option, + pub id: String, + pub kind: String, // xml parse only string to fields + pub status: String, + pub reason: Option, + pub reason_code: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub updated_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub reply_by_date: Option, + pub transaction: DisputeTransaction, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DisputeTransaction { + pub amount: String, + pub id: String, +} +#[derive(Debug, Deserialize, Serialize)] +pub struct DisputeEvidence { + pub comment: String, + pub id: Secret, + pub created_at: Option, + pub url: url::Url, +} + +pub(crate) fn get_dispute_stage(code: &str) -> Result { + match code { + "CHARGEBACK" => Ok(enums::DisputeStage::Dispute), + "PRE_ARBITATION" => Ok(enums::DisputeStage::PreArbitration), + "RETRIEVAL" => Ok(enums::DisputeStage::PreDispute), + _ => Err(errors::ConnectorError::WebhookBodyDecodingFailed), + } +} diff --git a/crates/router/src/connector/cashtocode.rs b/crates/router/src/connector/cashtocode.rs index cf30c5f83e5e..ed994dca31fc 100644 --- a/crates/router/src/connector/cashtocode.rs +++ b/crates/router/src/connector/cashtocode.rs @@ -325,6 +325,7 @@ impl api::IncomingWebhook for Cashtocode { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let base64_signature = conn_utils::get_header_key_value("authorization", request.headers)?; let signature = base64_signature.as_bytes().to_owned(); @@ -338,9 +339,6 @@ impl api::IncomingWebhook for Cashtocode { merchant_connector_account: domain::MerchantConnectorAccount, connector_label: &str, ) -> CustomResult { - let signature = self - .get_webhook_source_verification_signature(request) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, @@ -349,6 +347,11 @@ impl api::IncomingWebhook for Cashtocode { ) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let secret_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec()) .into_report() .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index b9d0e70a8cb5..fb0aa8208925 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -1173,6 +1173,7 @@ impl api::IncomingWebhook for Checkout { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let signature = conn_utils::get_header_key_value("cko-signature", request.headers) .change_context(errors::ConnectorError::WebhookSignatureNotFound)?; @@ -1184,7 +1185,7 @@ impl api::IncomingWebhook for Checkout { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(format!("{}", String::from_utf8_lossy(request.body)).into_bytes()) } diff --git a/crates/router/src/connector/coinbase.rs b/crates/router/src/connector/coinbase.rs index 17d78750afa3..d50e490cfc30 100644 --- a/crates/router/src/connector/coinbase.rs +++ b/crates/router/src/connector/coinbase.rs @@ -361,6 +361,7 @@ impl api::IncomingWebhook for Coinbase { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let base64_signature = utils::get_header_key_value("X-CC-Webhook-Signature", request.headers)?; @@ -373,7 +374,7 @@ impl api::IncomingWebhook for Coinbase { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let message = std::str::from_utf8(request.body) .into_report() diff --git a/crates/router/src/connector/cryptopay.rs b/crates/router/src/connector/cryptopay.rs index 91e62f72e7ed..7e1b334b3619 100644 --- a/crates/router/src/connector/cryptopay.rs +++ b/crates/router/src/connector/cryptopay.rs @@ -377,6 +377,7 @@ impl api::IncomingWebhook for Cryptopay { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let base64_signature = utils::get_header_key_value("X-Cryptopay-Signature", request.headers)?; @@ -389,7 +390,7 @@ impl api::IncomingWebhook for Cryptopay { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let message = std::str::from_utf8(request.body) .into_report() diff --git a/crates/router/src/connector/globalpay.rs b/crates/router/src/connector/globalpay.rs index 761d91ae0130..5b545c0d019f 100644 --- a/crates/router/src/connector/globalpay.rs +++ b/crates/router/src/connector/globalpay.rs @@ -852,6 +852,7 @@ impl api::IncomingWebhook for Globalpay { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let signature = conn_utils::get_header_key_value("x-gp-signature", request.headers)?; Ok(signature.as_bytes().to_vec()) @@ -861,13 +862,13 @@ impl api::IncomingWebhook for Globalpay { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - secret: &[u8], + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let payload: Value = request.body.parse_struct("GlobalpayWebhookBody").switch()?; let mut payload_str = serde_json::to_string(&payload) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let sec = std::str::from_utf8(secret) + let sec = std::str::from_utf8(&connector_webhook_secrets.secret) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; payload_str.push_str(sec); diff --git a/crates/router/src/connector/gocardless.rs b/crates/router/src/connector/gocardless.rs index 3e2621a0c9c5..d6e6f30ad9f4 100644 --- a/crates/router/src/connector/gocardless.rs +++ b/crates/router/src/connector/gocardless.rs @@ -692,6 +692,7 @@ impl api::IncomingWebhook for Gocardless { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let signature = request .headers @@ -715,7 +716,7 @@ impl api::IncomingWebhook for Gocardless { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(format!("{}", String::from_utf8_lossy(request.body)) .as_bytes() diff --git a/crates/router/src/connector/iatapay.rs b/crates/router/src/connector/iatapay.rs index 0106b7087159..3a813c50cf6b 100644 --- a/crates/router/src/connector/iatapay.rs +++ b/crates/router/src/connector/iatapay.rs @@ -584,6 +584,7 @@ impl api::IncomingWebhook for Iatapay { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let base64_signature = utils::get_header_key_value("Authorization", request.headers)?; let base64_signature = base64_signature.replace("IATAPAY-HMAC-SHA256 ", ""); @@ -594,7 +595,7 @@ impl api::IncomingWebhook for Iatapay { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let message = std::str::from_utf8(request.body) .into_report() diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index bf1ab53453e1..5bd7f2495408 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -640,6 +640,7 @@ impl api::IncomingWebhook for Noon { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let webhook_body: noon::NoonWebhookSignature = request .body @@ -656,7 +657,7 @@ impl api::IncomingWebhook for Noon { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let webhook_body: noon::NoonWebhookBody = request .body diff --git a/crates/router/src/connector/nuvei.rs b/crates/router/src/connector/nuvei.rs index 52388c849adc..754ba5ff7443 100644 --- a/crates/router/src/connector/nuvei.rs +++ b/crates/router/src/connector/nuvei.rs @@ -872,6 +872,7 @@ impl api::IncomingWebhook for Nuvei { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let signature = utils::get_header_key_value("advanceResponseChecksum", request.headers)?; hex::decode(signature) @@ -883,12 +884,12 @@ impl api::IncomingWebhook for Nuvei { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - secret: &[u8], + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let body = serde_urlencoded::from_str::(&request.query_params) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; - let secret_str = std::str::from_utf8(secret) + let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let status = format!("{:?}", body.status).to_uppercase(); diff --git a/crates/router/src/connector/opennode.rs b/crates/router/src/connector/opennode.rs index df2b2170d028..6fad0f472050 100644 --- a/crates/router/src/connector/opennode.rs +++ b/crates/router/src/connector/opennode.rs @@ -345,6 +345,7 @@ impl api::IncomingWebhook for Opennode { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let notif = serde_urlencoded::from_bytes::(request.body) .into_report() @@ -359,7 +360,7 @@ impl api::IncomingWebhook for Opennode { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let message = std::str::from_utf8(request.body) .into_report() diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index f3af084a97dc..92a19d4296c5 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -926,6 +926,7 @@ impl api::IncomingWebhook for Payme { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let resource = serde_urlencoded::from_bytes::(request.body) @@ -938,7 +939,7 @@ impl api::IncomingWebhook for Payme { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - secret: &[u8], + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let resource = serde_urlencoded::from_bytes::(request.body) @@ -946,7 +947,7 @@ impl api::IncomingWebhook for Payme { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(format!( "{}{}{}", - String::from_utf8_lossy(secret), + String::from_utf8_lossy(&connector_webhook_secrets.secret), resource.payme_transaction_id, resource.payme_sale_id ) @@ -965,10 +966,6 @@ impl api::IncomingWebhook for Payme { .get_webhook_source_verification_algorithm(request) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - let signature = self - .get_webhook_source_verification_signature(request) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, @@ -977,11 +974,16 @@ impl api::IncomingWebhook for Payme { ) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let mut message = self .get_webhook_source_verification_message( request, &merchant_account.merchant_id, - &connector_webhook_secrets.secret, + &connector_webhook_secrets, ) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let mut message_to_verify = connector_webhook_secrets @@ -992,6 +994,7 @@ impl api::IncomingWebhook for Payme { .expose() .as_bytes() .to_vec(); + message_to_verify.append(&mut message); let signature_to_verify = hex::decode(signature) diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 04748edd2346..292e6c55f26f 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -716,6 +716,7 @@ impl api::IncomingWebhook for Rapyd { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let base64_signature = conn_utils::get_header_key_value("signature", request.headers)?; let signature = consts::BASE64_ENGINE_URL_SAFE @@ -729,14 +730,14 @@ impl api::IncomingWebhook for Rapyd { &self, request: &api::IncomingWebhookRequestDetails<'_>, merchant_id: &str, - secret: &[u8], + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let host = conn_utils::get_header_key_value("host", request.headers)?; let connector = self.id(); let url_path = format!("https://{host}/webhooks/{merchant_id}/{connector}"); let salt = conn_utils::get_header_key_value("salt", request.headers)?; let timestamp = conn_utils::get_header_key_value("timestamp", request.headers)?; - let stringify_auth = String::from_utf8(secret.to_vec()) + let stringify_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec()) .into_report() .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) .attach_printable("Could not convert secret to UTF-8")?; @@ -765,9 +766,6 @@ impl api::IncomingWebhook for Rapyd { merchant_connector_account: domain::MerchantConnectorAccount, connector_label: &str, ) -> CustomResult { - let signature = self - .get_webhook_source_verification_signature(request) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, @@ -776,11 +774,14 @@ impl api::IncomingWebhook for Rapyd { ) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let message = self .get_webhook_source_verification_message( request, &merchant_account.merchant_id, - &connector_webhook_secrets.secret, + &connector_webhook_secrets, ) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; diff --git a/crates/router/src/connector/square.rs b/crates/router/src/connector/square.rs index aedd2f93dadd..a048b0f5433b 100644 --- a/crates/router/src/connector/square.rs +++ b/crates/router/src/connector/square.rs @@ -831,6 +831,7 @@ impl api::IncomingWebhook for Square { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let encoded_signature = super_utils::get_header_key_value("x-square-hmacsha256-signature", request.headers)?; @@ -845,7 +846,7 @@ impl api::IncomingWebhook for Square { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let header_value = request .headers diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index cc1e3be81d45..734dca3016d8 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1703,6 +1703,7 @@ impl api::IncomingWebhook for Stripe { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let mut security_header_kvs = get_signature_elements_from_header(request.headers)?; @@ -1720,7 +1721,7 @@ impl api::IncomingWebhook for Stripe { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let mut security_header_kvs = get_signature_elements_from_header(request.headers)?; diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 3d4f0a14a997..f182b444976a 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -900,6 +900,7 @@ impl api::IncomingWebhook for Trustpay { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let response: trustpay::TrustpayWebhookResponse = request .body @@ -914,7 +915,7 @@ impl api::IncomingWebhook for Trustpay { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let trustpay_response: trustpay::TrustpayWebhookResponse = request .body diff --git a/crates/router/src/connector/worldline.rs b/crates/router/src/connector/worldline.rs index 2dddb2ed9e2c..4e69fd5ca0ab 100644 --- a/crates/router/src/connector/worldline.rs +++ b/crates/router/src/connector/worldline.rs @@ -726,6 +726,7 @@ impl api::IncomingWebhook for Worldline { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let header_value = conn_utils::get_header_key_value("X-GCS-Signature", request.headers)?; let signature = consts::BASE64_ENGINE @@ -739,7 +740,7 @@ impl api::IncomingWebhook for Worldline { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(request.body.to_vec()) } diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 53e26f749fce..0c69cd981bb3 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -654,6 +654,7 @@ impl api::IncomingWebhook for Worldpay { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let event_signature = utils::get_header_key_value("Event-Signature", request.headers)?.split(','); @@ -673,9 +674,9 @@ impl api::IncomingWebhook for Worldpay { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - secret: &[u8], + connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { - let secret_str = std::str::from_utf8(secret) + let secret_str = std::str::from_utf8(&connector_webhook_secrets.secret) .into_report() .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let to_sign = format!( diff --git a/crates/router/src/connector/zen.rs b/crates/router/src/connector/zen.rs index 11b3b7076dd2..f65cb02b949d 100644 --- a/crates/router/src/connector/zen.rs +++ b/crates/router/src/connector/zen.rs @@ -548,6 +548,7 @@ impl api::IncomingWebhook for Zen { fn get_webhook_source_verification_signature( &self, request: &api::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let webhook_body: zen::ZenWebhookSignature = request .body @@ -563,7 +564,7 @@ impl api::IncomingWebhook for Zen { &self, request: &api::IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { let webhook_body: zen::ZenWebhookBody = request .body @@ -587,23 +588,25 @@ impl api::IncomingWebhook for Zen { connector_label: &str, ) -> CustomResult { let algorithm = self.get_webhook_source_verification_algorithm(request)?; - - let signature = self.get_webhook_source_verification_signature(request)?; - let mut connector_webhook_secrets = self + let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, connector_label, merchant_connector_account, ) .await?; + let signature = + self.get_webhook_source_verification_signature(request, &connector_webhook_secrets)?; + let mut message = self.get_webhook_source_verification_message( request, &merchant_account.merchant_id, - &connector_webhook_secrets.secret, + &connector_webhook_secrets, )?; - message.append(&mut connector_webhook_secrets.secret); + let mut secret = connector_webhook_secrets.secret; + message.append(&mut secret); algorithm - .verify_signature(&connector_webhook_secrets.secret, &signature, &message) + .verify_signature(&secret, &signature, &message) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) } diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 3ac461fe4660..70a40f466c83 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -130,6 +130,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_source_verification_signature( &self, _request: &IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(Vec::new()) } @@ -138,7 +139,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { &self, _request: &IncomingWebhookRequestDetails<'_>, _merchant_id: &str, - _secret: &[u8], + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(Vec::new()) } @@ -213,9 +214,6 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { .get_webhook_source_verification_algorithm(request) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - let signature = self - .get_webhook_source_verification_signature(request) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_account, @@ -225,13 +223,18 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let signature = self + .get_webhook_source_verification_signature(request, &connector_webhook_secrets) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self .get_webhook_source_verification_message( request, &merchant_account.merchant_id, - &connector_webhook_secrets.secret, + &connector_webhook_secrets, ) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + algorithm .verify_signature(&connector_webhook_secrets.secret, &signature, &message) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)