diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index c0939c70fc..8a6dde9c4a 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -684,6 +684,8 @@ impl api::IncomingWebhook for Adyen { &self, _headers: &actix_web::http::header::HeaderMap, body: &[u8], + _merchant_id: &str, + _secret: &[u8], ) -> CustomResult, errors::ConnectorError> { let notif = get_webhook_object_from_body(body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index d383f1bacc..ec6687913e 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -2,7 +2,7 @@ mod transformers; use std::fmt::Debug; use base64::Engine; -use common_utils::date_time; +use common_utils::{date_time, ext_traits::StringExt}; use error_stack::{IntoReport, ResultExt}; use rand::distributions::{Alphanumeric, DistString}; use ring::hmac; @@ -10,18 +10,20 @@ use transformers as rapyd; use crate::{ configs::settings, + connector::utils as conn_utils, consts, core::{ errors::{self, CustomResult}, payments, }, - headers, logger, services, + db::StorageInterface, + headers, services, types::{ self, api::{self, ConnectorCommon}, ErrorResponse, }, - utils::{self, BytesExt}, + utils::{self, crypto, ByteSliceExt, BytesExt}, }; #[derive(Debug, Clone)] @@ -70,6 +72,22 @@ impl ConnectorCommon for Rapyd { ) -> CustomResult, errors::ConnectorError> { Ok(vec![]) } + + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: rapyd::RapydPaymentsResponse = res + .response + .parse_struct("Rapyd ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + code: response.status.error_code, + message: response.status.status.unwrap_or_default(), + reason: response.status.message, + }) + } } impl api::ConnectorAccessToken for Rapyd {} @@ -171,7 +189,6 @@ impl .response .parse_struct("Rapyd PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(rapydpayments_create_response=?response); types::ResponseRouterData { response, data: data.clone(), @@ -185,16 +202,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: rapyd::RapydPaymentsResponse = res - .response - .parse_struct("Rapyd ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.error_code, - message: response.status.status, - reason: response.status.message, - }) + self.build_error_response(res) } } @@ -283,7 +291,6 @@ impl .response .parse_struct("Rapyd PaymentResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - logger::debug!(rapydpayments_create_response=?response); types::ResponseRouterData { response, data: data.clone(), @@ -297,16 +304,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: rapyd::RapydPaymentsResponse = res - .response - .parse_struct("Rapyd ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.error_code, - message: response.status.status, - reason: response.status.message, - }) + self.build_error_response(res) } } @@ -320,7 +318,10 @@ impl _req: &types::PaymentsSyncRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(vec![]) + Ok(vec![( + headers::CONTENT_TYPE.to_string(), + types::PaymentsSyncType::get_content_type(self).to_string(), + )]) } fn get_content_type(&self) -> &'static str { @@ -329,33 +330,74 @@ impl fn get_url( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into()) + let id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/v1/payments/{}", + self.base_url(connectors), + id.get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? + )) } fn build_request( &self, - _req: &types::PaymentsSyncRouterData, - _connectors: &settings::Connectors, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(None) + let timestamp = date_time::now_unix_timestamp(); + let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12); + + let auth: rapyd::RapydAuthType = rapyd::RapydAuthType::try_from(&req.connector_auth_type)?; + let response_id = req.request.connector_transaction_id.clone(); + let url_path = format!( + "/v1/payments/{}", + response_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? + ); + let signature = self.generate_signature(&auth, "get", &url_path, "", ×tamp, &salt)?; + + let headers = vec![ + ("access_key".to_string(), auth.access_key), + ("salt".to_string(), salt), + ("timestamp".to_string(), timestamp.to_string()), + ("signature".to_string(), signature), + ]; + let request = services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .headers(headers) + .build(); + Ok(Some(request)) } fn get_error_response( &self, - _res: types::Response, + res: types::Response, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into()) + self.build_error_response(res) } fn handle_response( &self, - _data: &types::PaymentsSyncRouterData, - _res: types::Response, + data: &types::PaymentsSyncRouterData, + res: types::Response, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("PSync".to_string()).into()) + let response: rapyd::RapydPaymentsResponse = res + .response + .parse_struct("Rapyd PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + } + .try_into() + .change_context(errors::ConnectorError::ResponseHandlingFailed) } } @@ -461,16 +503,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: rapyd::RapydPaymentsResponse = res - .response - .parse_struct("Rapyd ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.error_code, - message: response.status.status, - reason: response.status.message, - }) + self.build_error_response(res) } } @@ -559,7 +592,6 @@ impl services::ConnectorIntegration, res: types::Response, ) -> CustomResult, errors::ConnectorError> { - logger::debug!(target: "router::connector::rapyd", response=?res); let response: rapyd::RefundResponse = res .response .parse_struct("rapyd RefundResponse") @@ -577,16 +609,7 @@ impl services::ConnectorIntegration CustomResult { - let response: rapyd::RapydPaymentsResponse = res - .response - .parse_struct("Rapyd ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.error_code, - message: response.status.status, - reason: response.status.message, - }) + self.build_error_response(res) } } @@ -618,7 +641,6 @@ impl services::ConnectorIntegration CustomResult { - logger::debug!(target: "router::connector::rapyd", response=?res); let response: rapyd::RefundResponse = res .response .parse_struct("rapyd RefundResponse") @@ -642,25 +664,145 @@ impl services::ConnectorIntegration CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + headers: &actix_web::http::header::HeaderMap, + _body: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let base64_signature = conn_utils::get_header_key_value("signature", headers)?; + let signature = consts::BASE64_ENGINE_URL_SAFE + .decode(base64_signature.as_bytes()) + .into_report() + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + Ok(signature) + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = format!("wh_mer_sec_verification_{}_{}", self.id(), merchant_id); + let secret = db + .get_key(&key) + .await + .change_context(errors::ConnectorError::WebhookVerificationSecretNotFound)?; + + Ok(secret) + } + + fn get_webhook_source_verification_message( + &self, + headers: &actix_web::http::header::HeaderMap, + body: &[u8], + merchant_id: &str, + secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let host = conn_utils::get_header_key_value("host", headers)?; + let connector = self.id(); + let url_path = format!("https://{host}/webhooks/{merchant_id}/{connector}"); + let salt = conn_utils::get_header_key_value("salt", headers)?; + let timestamp = conn_utils::get_header_key_value("timestamp", headers)?; + let stringify_auth = String::from_utf8(secret.to_vec()) + .into_report() + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + .attach_printable("Could not convert secret to UTF-8")?; + let auth: transformers::RapydAuthType = stringify_auth + .parse_struct("RapydAuthType") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let access_key = auth.access_key; + let secret_key = auth.secret_key; + let body_string = String::from_utf8(body.to_vec()) + .into_report() + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + .attach_printable("Could not convert body to UTF-8")?; + let to_sign = format!("{url_path}{salt}{timestamp}{access_key}{secret_key}{body_string}"); + + Ok(to_sign.into_bytes()) + } + + async fn verify_webhook_source( + &self, + db: &dyn StorageInterface, + headers: &actix_web::http::header::HeaderMap, + body: &[u8], + merchant_id: &str, + ) -> CustomResult { + let signature = self + .get_webhook_source_verification_signature(headers, body) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let secret = self + .get_webhook_source_verification_merchant_secret(db, merchant_id) + .await + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self + .get_webhook_source_verification_message(headers, body, merchant_id, &secret) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + + let stringify_auth = String::from_utf8(secret.to_vec()) + .into_report() + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) + .attach_printable("Could not convert secret to UTF-8")?; + let auth: transformers::RapydAuthType = stringify_auth + .parse_struct("RapydAuthType") + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let secret_key = auth.secret_key; + let key = hmac::Key::new(hmac::HMAC_SHA256, secret_key.as_bytes()); + let tag = hmac::sign(&key, &message); + let hmac_sign = hex::encode(tag); + Ok(hmac_sign.as_bytes().eq(&signature)) + } + fn get_webhook_object_reference_id( &self, - _body: &[u8], + body: &[u8], ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let webhook: transformers::RapydIncomingWebhook = body + .parse_struct("RapydIncomingWebhook") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + Ok(match webhook.data { + transformers::WebhookData::PaymentData(payment_data) => payment_data.id, + transformers::WebhookData::RefundData(refund_data) => refund_data.id, + }) } fn get_webhook_event_type( &self, - _body: &[u8], + body: &[u8], ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let webhook: transformers::RapydIncomingWebhook = body + .parse_struct("RapydIncomingWebhook") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + webhook.webhook_type.try_into() } fn get_webhook_resource_object( &self, - _body: &[u8], + body: &[u8], ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let webhook: transformers::RapydIncomingWebhook = body + .parse_struct("RapydIncomingWebhook") + .change_context(errors::ConnectorError::WebhookEventTypeNotFound)?; + let response = match webhook.data { + transformers::WebhookData::PaymentData(payment_data) => { + let rapyd_response: transformers::RapydPaymentsResponse = payment_data.into(); + Ok(rapyd_response) + } + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound), + }?; + let res_json = + utils::Encode::::encode_to_value(&response) + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(res_json) } } diff --git a/crates/router/src/connector/rapyd/transformers.rs b/crates/router/src/connector/rapyd/transformers.rs index a89a7c7bc9..a0b6df812c 100644 --- a/crates/router/src/connector/rapyd/transformers.rs +++ b/crates/router/src/connector/rapyd/transformers.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use url::Url; use crate::{ + consts, core::errors, pii::{self, Secret}, services, @@ -138,6 +139,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for RapydPaymentsRequest { } } +#[derive(Debug, Deserialize)] pub struct RapydAuthType { pub access_key: String, pub secret_key: String, @@ -194,9 +196,10 @@ impl From> } } RapydPaymentStatus::CanceledByClientOrBank - | RapydPaymentStatus::Error | RapydPaymentStatus::Expired - | RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Failure, + | RapydPaymentStatus::ReversedByRapyd => enums::AttemptStatus::Voided, + RapydPaymentStatus::Error => enums::AttemptStatus::Failure, + RapydPaymentStatus::New => enums::AttemptStatus::Authorizing, } .into() @@ -212,10 +215,10 @@ pub struct RapydPaymentsResponse { #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Status { pub error_code: String, - pub status: String, + pub status: Option, pub message: Option, pub response_code: Option, - pub operation_id: String, + pub operation_id: Option, } #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -236,83 +239,6 @@ pub struct ResponseData { pub failure_message: Option, } -impl TryFrom> - for types::PaymentsAuthorizeRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::PaymentsResponseRouterData, - ) -> Result { - let (status, response) = match item.response.status.status.as_str() { - "SUCCESS" => match item.response.data { - Some(data) => { - let redirection_data = match (data.next_action.as_str(), data.redirect_url) { - ("3d_verification", Some(url)) => { - let url = Url::parse(&url) - .into_report() - .change_context(errors::ConnectorError::ResponseHandlingFailed)?; - let mut base_url = url.clone(); - base_url.set_query(None); - Some(services::RedirectForm { - url: base_url.to_string(), - method: services::Method::Get, - form_fields: std::collections::HashMap::from_iter( - url.query_pairs() - .map(|(k, v)| (k.to_string(), v.to_string())), - ), - }) - } - (_, _) => None, - }; - ( - enums::AttemptStatus::foreign_from((data.status, data.next_action)), - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund - redirect: redirection_data.is_some(), - redirection_data, - mandate_reference: None, - connector_metadata: None, - }), - ) - } - None => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - }, - "ERROR" => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - _ => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - }; - - Ok(Self { - status, - response, - ..item.data - }) - } -} - #[derive(Default, Debug, Serialize)] pub struct RapydRefundRequest { pub payment: String, @@ -435,51 +361,75 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CaptureRequest { } } -impl TryFrom> - for types::PaymentsCaptureRouterData +impl + TryFrom> + for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::PaymentsCaptureResponseRouterData, + item: types::ResponseRouterData, ) -> Result { - let (status, response) = match item.response.status.status.as_str() { - "SUCCESS" => match item.response.data { - Some(data) => ( - enums::AttemptStatus::foreign_from((data.status, data.next_action)), - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund - redirection_data: None, - redirect: false, - mandate_reference: None, - connector_metadata: None, - }), - ), - None => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - message: item.response.status.status, - reason: item.response.status.message, - status_code: item.http_code, - }), - ), - }, - "ERROR" => ( + let (status, response) = match &item.response.data { + Some(data) => { + let attempt_status = enums::AttemptStatus::foreign_from(( + data.status.to_owned(), + data.next_action.to_owned(), + )); + match attempt_status { + storage_models::enums::AttemptStatus::Failure => ( + enums::AttemptStatus::Failure, + Err(types::ErrorResponse { + code: data + .failure_code + .to_owned() + .unwrap_or(item.response.status.error_code), + status_code: item.http_code, + message: item.response.status.status.unwrap_or_default(), + reason: data.failure_message.to_owned(), + }), + ), + _ => { + let redirection_data = + match (data.next_action.as_str(), data.redirect_url.to_owned()) { + ("3d_verification", Some(url)) => { + let url = Url::parse(&url).into_report().change_context( + errors::ConnectorError::ResponseHandlingFailed, + )?; + let mut base_url = url.clone(); + base_url.set_query(None); + Some(services::RedirectForm { + url: base_url.to_string(), + method: services::Method::Get, + form_fields: std::collections::HashMap::from_iter( + url.query_pairs() + .map(|(k, v)| (k.to_string(), v.to_string())), + ), + }) + } + (_, _) => None, + }; + ( + attempt_status, + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId( + data.id.to_owned(), + ), //transaction_id is also the field but this id is used to initiate a refund + redirect: redirection_data.is_some(), + redirection_data, + mandate_reference: None, + connector_metadata: None, + }), + ) + } + } + } + None => ( enums::AttemptStatus::Failure, Err(types::ErrorResponse { code: item.response.status.error_code, - message: item.response.status.status, - reason: item.response.status.message, status_code: item.http_code, - }), - ), - _ => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - message: item.response.status.status, + message: item.response.status.status.unwrap_or_default(), reason: item.response.status.message, - status_code: item.http_code, }), ), }; @@ -492,59 +442,73 @@ impl TryFrom> } } -impl TryFrom> - for types::PaymentsCancelRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::PaymentsCancelResponseRouterData, - ) -> Result { - let (status, response) = match item.response.status.status.as_str() { - "SUCCESS" => match item.response.data { - Some(data) => ( - enums::AttemptStatus::foreign_from((data.status, data.next_action)), - Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(data.id), //transaction_id is also the field but this id is used to initiate a refund - redirection_data: None, - redirect: false, - mandate_reference: None, - connector_metadata: None, - }), - ), - None => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - }, - "ERROR" => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - _ => ( - enums::AttemptStatus::Failure, - Err(types::ErrorResponse { - code: item.response.status.error_code, - status_code: item.http_code, - message: item.response.status.status, - reason: item.response.status.message, - }), - ), - }; +#[derive(Debug, Deserialize)] +pub struct RapydIncomingWebhook { + pub id: String, + #[serde(rename = "type")] + pub webhook_type: RapydWebhookObjectEventType, + pub data: WebhookData, + pub trigger_operation_id: Option, + pub status: String, + pub created_at: i64, +} - Ok(Self { - status, - response, - ..item.data - }) +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RapydWebhookObjectEventType { + PaymentCompleted, + PaymentCaptured, + PaymentFailed, + RefundCompleted, + PaymentRefundRejected, + PaymentRefundFailed, +} + +impl TryFrom for api::IncomingWebhookEvent { + type Error = error_stack::Report; + fn try_from(value: RapydWebhookObjectEventType) -> Result { + match value { + RapydWebhookObjectEventType::PaymentCompleted => Ok(Self::PaymentIntentSuccess), + RapydWebhookObjectEventType::PaymentCaptured => Ok(Self::PaymentIntentSuccess), + RapydWebhookObjectEventType::PaymentFailed => Ok(Self::PaymentIntentFailure), + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound).into_report()?, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum WebhookData { + PaymentData(ResponseData), + RefundData(RefundResponseData), +} + +impl From for RapydPaymentsResponse { + fn from(value: ResponseData) -> Self { + Self { + status: Status { + error_code: consts::NO_ERROR_CODE.to_owned(), + status: None, + message: None, + response_code: None, + operation_id: None, + }, + data: Some(value), + } + } +} + +impl From for RefundResponse { + fn from(value: RefundResponseData) -> Self { + Self { + status: Status { + error_code: consts::NO_ERROR_CODE.to_owned(), + status: None, + message: None, + response_code: None, + operation_id: None, + }, + data: Some(value), + } } } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index fecedb31f1..15959b945e 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -907,6 +907,8 @@ impl api::IncomingWebhook for Stripe { &self, headers: &actix_web::http::header::HeaderMap, body: &[u8], + _merchant_id: &str, + _secret: &[u8], ) -> CustomResult, errors::ConnectorError> { let mut security_header_kvs = get_signature_elements_from_header(headers)?; diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 20238844ea..1e7c350ce2 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1,8 +1,8 @@ -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use crate::{ - core::errors, + core::errors::{self, CustomResult}, pii::PeekInterface, types::{self, api}, utils::OptionExt, @@ -190,3 +190,20 @@ impl AddressDetailsData for api::AddressDetails { .ok_or_else(missing_field_err("address.country")) } } + +pub fn get_header_key_value<'a>( + key: &str, + headers: &'a actix_web::http::header::HeaderMap, +) -> CustomResult<&'a str, errors::ConnectorError> { + headers + .get(key) + .map(|header_value| { + header_value + .to_str() + .into_report() + .change_context(errors::ConnectorError::WebhookSignatureNotFound) + }) + .ok_or(report!( + errors::ConnectorError::WebhookSourceVerificationFailed + ))? +} diff --git a/crates/router/src/types/api/webhooks.rs b/crates/router/src/types/api/webhooks.rs index 813b380fca..36a9f87624 100644 --- a/crates/router/src/types/api/webhooks.rs +++ b/crates/router/src/types/api/webhooks.rs @@ -88,6 +88,8 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { &self, _headers: &actix_web::http::header::HeaderMap, _body: &[u8], + _merchant_id: &str, + _secret: &[u8], ) -> CustomResult, errors::ConnectorError> { Ok(Vec::new()) } @@ -106,13 +108,13 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { let signature = self .get_webhook_source_verification_signature(headers, body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; - let message = self - .get_webhook_source_verification_message(headers, body) - .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let secret = self .get_webhook_source_verification_merchant_secret(db, merchant_id) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; + let message = self + .get_webhook_source_verification_message(headers, body, merchant_id, &secret) + .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; algorithm .verify_signature(&secret, &signature, &message)