//! Webhooks interface use common_utils::{crypto, errors::CustomResult, ext_traits::ValueExt}; use error_stack::ResultExt; use hyperswitch_domain_models::{ api::ApplicationResponse, errors::api_error_response::ApiErrorResponse, }; use masking::{ExposeInterface, Secret}; use crate::{api::ConnectorCommon, errors}; /// struct IncomingWebhookRequestDetails #[derive(Debug)] pub struct IncomingWebhookRequestDetails<'a> { /// method pub method: http::Method, /// uri pub uri: http::Uri, /// headers pub headers: &'a actix_web::http::header::HeaderMap, /// body pub body: &'a [u8], /// query_params pub query_params: String, } /// IncomingWebhookFlowError enum defining the error type for incoming webhook #[derive(Debug)] pub enum IncomingWebhookFlowError { /// Resource not found for the webhook ResourceNotFound, /// Internal error for the webhook InternalError, } impl From<&ApiErrorResponse> for IncomingWebhookFlowError { fn from(api_error_response: &ApiErrorResponse) -> Self { match api_error_response { ApiErrorResponse::WebhookResourceNotFound | ApiErrorResponse::DisputeNotFound { .. } | ApiErrorResponse::PayoutNotFound | ApiErrorResponse::MandateNotFound | ApiErrorResponse::PaymentNotFound | ApiErrorResponse::RefundNotFound | ApiErrorResponse::AuthenticationNotFound { .. } => Self::ResourceNotFound, _ => Self::InternalError, } } } /// Trait defining incoming webhook #[async_trait::async_trait] pub trait IncomingWebhook: ConnectorCommon + Sync { /// fn get_webhook_body_decoding_algorithm fn get_webhook_body_decoding_algorithm( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { Ok(Box::new(crypto::NoAlgorithm)) } /// fn get_webhook_body_decoding_message fn get_webhook_body_decoding_message( &self, request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { Ok(request.body.to_vec()) } /// fn decode_webhook_body async fn decode_webhook_body( &self, request: &IncomingWebhookRequestDetails<'_>, merchant_id: &common_utils::id_type::MerchantId, connector_webhook_details: Option, connector_name: &str, ) -> CustomResult, errors::ConnectorError> { let algorithm = self.get_webhook_body_decoding_algorithm(request)?; let message = self .get_webhook_body_decoding_message(request) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let secret = self .get_webhook_source_verification_merchant_secret( merchant_id, connector_name, connector_webhook_details, ) .await .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; algorithm .decode_message(&secret.secret, message.into()) .change_context(errors::ConnectorError::WebhookBodyDecodingFailed) } /// fn get_webhook_source_verification_algorithm fn get_webhook_source_verification_algorithm( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { Ok(Box::new(crypto::NoAlgorithm)) } /// fn get_webhook_source_verification_merchant_secret async fn get_webhook_source_verification_merchant_secret( &self, merchant_id: &common_utils::id_type::MerchantId, connector_name: &str, connector_webhook_details: Option, ) -> CustomResult { let debug_suffix = format!( "For merchant_id: {:?}, and connector_name: {}", merchant_id, connector_name ); let default_secret = "default_secret".to_string(); let merchant_secret = match connector_webhook_details { Some(merchant_connector_webhook_details) => { let connector_webhook_details = merchant_connector_webhook_details .parse_value::( "MerchantConnectorWebhookDetails", ) .change_context_lazy(|| errors::ConnectorError::WebhookSourceVerificationFailed) .attach_printable_lazy(|| { format!( "Deserializing MerchantConnectorWebhookDetails failed {}", debug_suffix ) })?; api_models::webhooks::ConnectorWebhookSecrets { secret: connector_webhook_details .merchant_secret .expose() .into_bytes(), additional_secret: connector_webhook_details.additional_secret, } } None => api_models::webhooks::ConnectorWebhookSecrets { secret: default_secret.into_bytes(), additional_secret: None, }, }; //need to fetch merchant secret from config table with caching in future for enhanced performance //If merchant has not set the secret for webhook source verification, "default_secret" is returned. //So it will fail during verification step and goes to psync flow. Ok(merchant_secret) } /// fn get_webhook_source_verification_signature fn get_webhook_source_verification_signature( &self, _request: &IncomingWebhookRequestDetails<'_>, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(Vec::new()) } /// fn get_webhook_source_verification_message fn get_webhook_source_verification_message( &self, _request: &IncomingWebhookRequestDetails<'_>, _merchant_id: &common_utils::id_type::MerchantId, _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, ) -> CustomResult, errors::ConnectorError> { Ok(Vec::new()) } /// fn verify_webhook_source async fn verify_webhook_source( &self, request: &IncomingWebhookRequestDetails<'_>, merchant_id: &common_utils::id_type::MerchantId, connector_webhook_details: Option, _connector_account_details: crypto::Encryptable>, connector_name: &str, ) -> CustomResult { let algorithm = self .get_webhook_source_verification_algorithm(request) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let connector_webhook_secrets = self .get_webhook_source_verification_merchant_secret( merchant_id, connector_name, connector_webhook_details, ) .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_id, &connector_webhook_secrets, ) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; algorithm .verify_signature(&connector_webhook_secrets.secret, &signature, &message) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed) } /// fn get_webhook_object_reference_id fn get_webhook_object_reference_id( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult; /// fn get_webhook_event_type fn get_webhook_event_type( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult; /// fn get_webhook_resource_object fn get_webhook_resource_object( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError>; /// fn get_webhook_api_response fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::StatusOk) } /// fn get_dispute_details fn get_dispute_details( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("get_dispute_details method".to_string()).into()) } /// fn get_external_authentication_details fn get_external_authentication_details( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult { Err(errors::ConnectorError::NotImplemented( "get_external_authentication_details method".to_string(), ) .into()) } /// fn get_mandate_details fn get_mandate_details( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult< Option, errors::ConnectorError, > { Ok(None) } /// fn get_network_txn_id fn get_network_txn_id( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult< Option, errors::ConnectorError, > { Ok(None) } #[cfg(all(feature = "revenue_recovery", feature = "v2"))] /// get revenue recovery invoice details fn get_revenue_recovery_attempt_details( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult< hyperswitch_domain_models::revenue_recovery::RevenueRecoveryAttemptData, errors::ConnectorError, > { Err(errors::ConnectorError::NotImplemented( "get_revenue_recovery_attempt_details method".to_string(), ) .into()) } #[cfg(all(feature = "revenue_recovery", feature = "v2"))] /// get revenue recovery transaction details fn get_revenue_recovery_invoice_details( &self, _request: &IncomingWebhookRequestDetails<'_>, ) -> CustomResult< hyperswitch_domain_models::revenue_recovery::RevenueRecoveryInvoiceData, errors::ConnectorError, > { Err(errors::ConnectorError::NotImplemented( "get_revenue_recovery_invoice_details method".to_string(), ) .into()) } }