feat(webhooks): Webhook source verification (#2069)

This commit is contained in:
Sangamesh Kulkarni
2023-09-01 14:50:33 +05:30
committed by GitHub
parent f8410b5b2a
commit 8b22f38dd6
14 changed files with 145 additions and 41 deletions

View File

@ -641,6 +641,8 @@ pub struct MerchantConnectorCreate {
pub struct MerchantConnectorWebhookDetails { pub struct MerchantConnectorWebhookDetails {
#[schema(value_type = String, example = "12345678900987654321")] #[schema(value_type = String, example = "12345678900987654321")]
pub merchant_secret: Secret<String>, pub merchant_secret: Secret<String>,
#[schema(value_type = String, example = "12345678900987654321")]
pub additional_secret: Option<Secret<String>>,
} }
/// Response of creating a new Merchant Connector for the merchant account." /// Response of creating a new Merchant Connector for the merchant account."

View File

@ -2474,6 +2474,8 @@ pub struct ApplepaySessionTokenResponse {
pub connector_reference_id: Option<String>, pub connector_reference_id: Option<String>,
/// The public key id is to invoke third party sdk /// The public key id is to invoke third party sdk
pub connector_sdk_public_key: Option<String>, pub connector_sdk_public_key: Option<String>,
/// The connector merchant id
pub connector_merchant_id: Option<String>,
} }
#[derive(Debug, Eq, PartialEq, serde::Serialize, Clone, ToSchema)] #[derive(Debug, Eq, PartialEq, serde::Serialize, Clone, ToSchema)]

View File

@ -114,3 +114,8 @@ pub enum OutgoingWebhookContent {
#[schema(value_type = DisputeResponse)] #[schema(value_type = DisputeResponse)]
DisputeDetails(Box<disputes::DisputeResponse>), DisputeDetails(Box<disputes::DisputeResponse>),
} }
pub struct ConnectorWebhookSecrets {
pub secret: Vec<u8>,
pub additional_secret: Option<masking::Secret<String>>,
}

View File

@ -1408,7 +1408,7 @@ impl api::IncomingWebhook for Adyen {
let signature = self let signature = self
.get_webhook_source_verification_signature(request) .get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret( .get_webhook_source_verification_merchant_secret(
db, db,
merchant_account, merchant_account,
@ -1422,11 +1422,11 @@ impl api::IncomingWebhook for Adyen {
.get_webhook_source_verification_message( .get_webhook_source_verification_message(
request, request,
&merchant_account.merchant_id, &merchant_account.merchant_id,
&secret, &connector_webhook_secrets.secret,
) )
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let raw_key = hex::decode(secret) let raw_key = hex::decode(connector_webhook_secrets.secret)
.into_report() .into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?; .change_context(errors::ConnectorError::WebhookSignatureNotFound)?;

View File

@ -427,6 +427,7 @@ impl TryFrom<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenRespons
}, },
connector_reference_id: None, connector_reference_id: None,
connector_sdk_public_key: None, connector_sdk_public_key: None,
connector_merchant_id: None,
}, },
)), )),
}), }),

View File

@ -341,7 +341,7 @@ impl api::IncomingWebhook for Cashtocode {
let signature = self let signature = self
.get_webhook_source_verification_signature(request) .get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret( .get_webhook_source_verification_merchant_secret(
db, db,
merchant_account, merchant_account,
@ -351,7 +351,7 @@ impl api::IncomingWebhook for Cashtocode {
) )
.await .await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret_auth = String::from_utf8(secret.to_vec()) let secret_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec())
.into_report() .into_report()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Could not convert secret to UTF-8")?; .attach_printable("Could not convert secret to UTF-8")?;

View File

@ -12,7 +12,7 @@ use crate::{
configs::settings, configs::settings,
connector::utils as connector_utils, connector::utils as connector_utils,
core::errors::{self, CustomResult}, core::errors::{self, CustomResult},
headers, db, headers,
services::{self, request, ConnectorIntegration, ConnectorValidation}, services::{self, request, ConnectorIntegration, ConnectorValidation},
types::{ types::{
self, self,
@ -722,6 +722,62 @@ impl api::IncomingWebhook for Payme {
.to_vec()) .to_vec())
} }
async fn verify_webhook_source(
&self,
db: &dyn db::StorageInterface,
request: &api::IncomingWebhookRequestDetails<'_>,
merchant_account: &types::domain::MerchantAccount,
connector_label: &str,
key_store: &types::domain::MerchantKeyStore,
object_reference_id: api_models::webhooks::ObjectReferenceId,
) -> CustomResult<bool, errors::ConnectorError> {
let algorithm = self
.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(
db,
merchant_account,
connector_label,
key_store,
object_reference_id,
)
.await
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let mut message = self
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&connector_webhook_secrets.secret,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let mut message_to_verify = connector_webhook_secrets
.additional_secret
.ok_or(errors::ConnectorError::WebhookSourceVerificationFailed)
.into_report()
.attach_printable("Failed to get additional secrets")?
.expose()
.as_bytes()
.to_vec();
message_to_verify.append(&mut message);
let signature_to_verify = hex::decode(signature)
.into_report()
.change_context(errors::ConnectorError::WebhookResponseEncodingFailed)?;
algorithm
.verify_signature(
&connector_webhook_secrets.secret,
&signature_to_verify,
&message_to_verify,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
}
fn get_webhook_object_reference_id( fn get_webhook_object_reference_id(
&self, &self,
request: &api::IncomingWebhookRequestDetails<'_>, request: &api::IncomingWebhookRequestDetails<'_>,

View File

@ -290,7 +290,7 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod {
match item { match item {
PaymentMethodData::Card(_) => Ok(Self::CreditCard), PaymentMethodData::Card(_) => Ok(Self::CreditCard),
PaymentMethodData::Wallet(wallet_data) => match wallet_data { PaymentMethodData::Wallet(wallet_data) => match wallet_data {
api_models::payments::WalletData::ApplePay(_) => Ok(Self::ApplePay), api_models::payments::WalletData::ApplePayThirdPartySdk(_) => Ok(Self::ApplePay),
api_models::payments::WalletData::AliPayQr(_) api_models::payments::WalletData::AliPayQr(_)
| api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayRedirect(_)
| api_models::payments::WalletData::AliPayHkRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_)
@ -299,7 +299,6 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod {
| api_models::payments::WalletData::GoPayRedirect(_) | api_models::payments::WalletData::GoPayRedirect(_)
| api_models::payments::WalletData::GcashRedirect(_) | api_models::payments::WalletData::GcashRedirect(_)
| api_models::payments::WalletData::ApplePayRedirect(_) | api_models::payments::WalletData::ApplePayRedirect(_)
| api_models::payments::WalletData::ApplePayThirdPartySdk(_)
| api_models::payments::WalletData::DanaRedirect {} | api_models::payments::WalletData::DanaRedirect {}
| api_models::payments::WalletData::GooglePay(_) | api_models::payments::WalletData::GooglePay(_)
| api_models::payments::WalletData::GooglePayRedirect(_) | api_models::payments::WalletData::GooglePayRedirect(_)
@ -315,6 +314,7 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod {
| api_models::payments::WalletData::WeChatPayRedirect(_) | api_models::payments::WalletData::WeChatPayRedirect(_)
| api_models::payments::WalletData::WeChatPayQr(_) | api_models::payments::WalletData::WeChatPayQr(_)
| api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::CashappQr(_)
| api_models::payments::WalletData::ApplePay(_)
| api_models::payments::WalletData::SwishQr(_) => { | api_models::payments::WalletData::SwishQr(_) => {
Err(errors::ConnectorError::NotSupported { Err(errors::ConnectorError::NotSupported {
message: "Wallet".to_string(), message: "Wallet".to_string(),
@ -401,6 +401,7 @@ impl<F>
let amount = item.data.request.get_amount()?; let amount = item.data.request.get_amount()?;
let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?; let amount_in_base_unit = utils::to_currency_base_unit(amount, currency_code)?;
let pmd = item.data.request.payment_method_data.to_owned(); let pmd = item.data.request.payment_method_data.to_owned();
let payme_auth_type = PaymeAuthType::try_from(&item.data.connector_auth_type)?;
let session_token = match pmd { let session_token = match pmd {
Some(PaymentMethodData::Wallet( Some(PaymentMethodData::Wallet(
@ -427,11 +428,10 @@ impl<F>
next_action: api_models::payments::NextActionCall::Sync, next_action: api_models::payments::NextActionCall::Sync,
}, },
connector_reference_id: Some(item.response.payme_sale_id.to_owned()), connector_reference_id: Some(item.response.payme_sale_id.to_owned()),
connector_sdk_public_key: Some( connector_sdk_public_key: Some(payme_auth_type.payme_public_key.expose()),
PaymeAuthType::try_from(&item.data.connector_auth_type)? connector_merchant_id: payme_auth_type
.payme_public_key .payme_merchant_id
.expose(), .map(|mid| mid.expose()),
),
}, },
))), ))),
_ => None, _ => None,
@ -513,6 +513,7 @@ pub struct PaymeAuthType {
#[allow(dead_code)] #[allow(dead_code)]
pub(super) payme_public_key: Secret<String>, pub(super) payme_public_key: Secret<String>,
pub(super) seller_payme_id: Secret<String>, pub(super) seller_payme_id: Secret<String>,
pub(super) payme_merchant_id: Option<Secret<String>>,
} }
impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType { impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType {
@ -522,6 +523,16 @@ impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType {
types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
seller_payme_id: api_key.to_owned(), seller_payme_id: api_key.to_owned(),
payme_public_key: key1.to_owned(), payme_public_key: key1.to_owned(),
payme_merchant_id: None,
}),
types::ConnectorAuthType::SignatureKey {
api_key,
key1,
api_secret,
} => Ok(Self {
seller_payme_id: api_key.to_owned(),
payme_public_key: key1.to_owned(),
payme_merchant_id: Some(api_secret.to_owned()),
}), }),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
} }

View File

@ -772,7 +772,7 @@ impl api::IncomingWebhook for Rapyd {
let signature = self let signature = self
.get_webhook_source_verification_signature(request) .get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret( .get_webhook_source_verification_merchant_secret(
db, db,
merchant_account, merchant_account,
@ -786,11 +786,11 @@ impl api::IncomingWebhook for Rapyd {
.get_webhook_source_verification_message( .get_webhook_source_verification_message(
request, request,
&merchant_account.merchant_id, &merchant_account.merchant_id,
&secret, &connector_webhook_secrets.secret,
) )
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let stringify_auth = String::from_utf8(secret.to_vec()) let stringify_auth = String::from_utf8(connector_webhook_secrets.secret.to_vec())
.into_report() .into_report()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Could not convert secret to UTF-8")?; .attach_printable("Could not convert secret to UTF-8")?;

View File

@ -1068,6 +1068,7 @@ pub fn get_apple_pay_session<F, T>(
}, },
connector_reference_id: None, connector_reference_id: None,
connector_sdk_public_key: None, connector_sdk_public_key: None,
connector_merchant_id: None,
}, },
))), ))),
connector_response_reference_id: None, connector_response_reference_id: None,

View File

@ -573,7 +573,7 @@ impl api::IncomingWebhook for Zen {
let algorithm = self.get_webhook_source_verification_algorithm(request)?; let algorithm = self.get_webhook_source_verification_algorithm(request)?;
let signature = self.get_webhook_source_verification_signature(request)?; let signature = self.get_webhook_source_verification_signature(request)?;
let mut secret = self let mut connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret( .get_webhook_source_verification_merchant_secret(
db, db,
merchant_account, merchant_account,
@ -585,11 +585,11 @@ impl api::IncomingWebhook for Zen {
let mut message = self.get_webhook_source_verification_message( let mut message = self.get_webhook_source_verification_message(
request, request,
&merchant_account.merchant_id, &merchant_account.merchant_id,
&secret, &connector_webhook_secrets.secret,
)?; )?;
message.append(&mut secret); message.append(&mut connector_webhook_secrets.secret);
algorithm algorithm
.verify_signature(&secret, &signature, &message) .verify_signature(&connector_webhook_secrets.secret, &signature, &message)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
} }

View File

@ -273,6 +273,7 @@ fn create_apple_pay_session_response(
sdk_next_action: { payment_types::SdkNextAction { next_action } }, sdk_next_action: { payment_types::SdkNextAction { next_action } },
connector_reference_id: None, connector_reference_id: None,
connector_sdk_public_key: None, connector_sdk_public_key: None,
connector_merchant_id: None,
}, },
)), )),
}), }),

View File

@ -82,7 +82,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
connector_name: &str, connector_name: &str,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,
object_reference_id: ObjectReferenceId, object_reference_id: ObjectReferenceId,
) -> CustomResult<Vec<u8>, errors::ConnectorError> { ) -> CustomResult<api_models::webhooks::ConnectorWebhookSecrets, errors::ConnectorError> {
let merchant_id = merchant_account.merchant_id.as_str(); let merchant_id = merchant_account.merchant_id.as_str();
let debug_suffix = format!( let debug_suffix = format!(
"For merchant_id: {}, and connector_name: {}", "For merchant_id: {}, and connector_name: {}",
@ -106,22 +106,34 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
) )
.await; .await;
let merchant_secret = match merchant_connector_account_result { let connector_webhook_secrets = match merchant_connector_account_result {
Ok(mca) => match mca.connector_webhook_details { Ok(mca) => match mca.connector_webhook_details {
Some(merchant_connector_webhook_details) => merchant_connector_webhook_details Some(merchant_connector_webhook_details) => {
let connector_webhook_details = merchant_connector_webhook_details
.parse_value::<MerchantConnectorWebhookDetails>( .parse_value::<MerchantConnectorWebhookDetails>(
"MerchantConnectorWebhookDetails", "MerchantConnectorWebhookDetails",
) )
.change_context_lazy(|| errors::ConnectorError::WebhookSourceVerificationFailed) .change_context_lazy(|| {
errors::ConnectorError::WebhookSourceVerificationFailed
})
.attach_printable_lazy(|| { .attach_printable_lazy(|| {
format!( format!(
"Deserializing MerchantConnectorWebhookDetails failed {}", "Deserializing MerchantConnectorWebhookDetails failed {}",
debug_suffix debug_suffix
) )
})? })?;
api_models::webhooks::ConnectorWebhookSecrets {
secret: connector_webhook_details
.merchant_secret .merchant_secret
.expose(), .expose()
None => default_secret, .into_bytes(),
additional_secret: connector_webhook_details.additional_secret,
}
}
None => api_models::webhooks::ConnectorWebhookSecrets {
secret: default_secret.into_bytes(),
additional_secret: None,
},
}, },
Err(err) => { Err(err) => {
logger::error!( logger::error!(
@ -129,7 +141,10 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
debug_suffix debug_suffix
); );
logger::error!("DB error = {:?}", err); logger::error!("DB error = {:?}", err);
default_secret api_models::webhooks::ConnectorWebhookSecrets {
secret: default_secret.into_bytes(),
additional_secret: None,
}
} }
}; };
@ -137,7 +152,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
//If merchant has not set the secret for webhook source verification, "default_secret" is returned. //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. //So it will fail during verification step and goes to psync flow.
Ok(merchant_secret.into_bytes()) Ok(connector_webhook_secrets)
} }
fn get_webhook_source_verification_signature( fn get_webhook_source_verification_signature(
@ -172,7 +187,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
let signature = self let signature = self
.get_webhook_source_verification_signature(request) .get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret( .get_webhook_source_verification_merchant_secret(
db, db,
merchant_account, merchant_account,
@ -186,11 +201,11 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
.get_webhook_source_verification_message( .get_webhook_source_verification_message(
request, request,
&merchant_account.merchant_id, &merchant_account.merchant_id,
&secret, &connector_webhook_secrets.secret,
) )
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
algorithm algorithm
.verify_signature(&secret, &signature, &message) .verify_signature(&connector_webhook_secrets.secret, &signature, &message)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
} }

View File

@ -2370,6 +2370,11 @@
"type": "string", "type": "string",
"description": "The public key id is to invoke third party sdk", "description": "The public key id is to invoke third party sdk",
"nullable": true "nullable": true
},
"connector_merchant_id": {
"type": "string",
"description": "The connector merchant id",
"nullable": true
} }
} }
}, },
@ -6815,12 +6820,17 @@
"MerchantConnectorWebhookDetails": { "MerchantConnectorWebhookDetails": {
"type": "object", "type": "object",
"required": [ "required": [
"merchant_secret" "merchant_secret",
"additional_secret"
], ],
"properties": { "properties": {
"merchant_secret": { "merchant_secret": {
"type": "string", "type": "string",
"example": "12345678900987654321" "example": "12345678900987654321"
},
"additional_secret": {
"type": "string",
"example": "12345678900987654321"
} }
} }
}, },