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 {
#[schema(value_type = String, example = "12345678900987654321")]
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."

View File

@ -2474,6 +2474,8 @@ pub struct ApplepaySessionTokenResponse {
pub connector_reference_id: Option<String>,
/// The public key id is to invoke third party sdk
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)]

View File

@ -114,3 +114,8 @@ pub enum OutgoingWebhookContent {
#[schema(value_type = 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
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
@ -1422,11 +1422,11 @@ impl api::IncomingWebhook for Adyen {
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&secret,
&connector_webhook_secrets.secret,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let raw_key = hex::decode(secret)
let raw_key = hex::decode(connector_webhook_secrets.secret)
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;

View File

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

View File

@ -341,7 +341,7 @@ impl api::IncomingWebhook for Cashtocode {
let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
@ -351,7 +351,7 @@ impl api::IncomingWebhook for Cashtocode {
)
.await
.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()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.attach_printable("Could not convert secret to UTF-8")?;

View File

@ -12,7 +12,7 @@ use crate::{
configs::settings,
connector::utils as connector_utils,
core::errors::{self, CustomResult},
headers,
db, headers,
services::{self, request, ConnectorIntegration, ConnectorValidation},
types::{
self,
@ -722,6 +722,62 @@ impl api::IncomingWebhook for Payme {
.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(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,

View File

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

View File

@ -772,7 +772,7 @@ impl api::IncomingWebhook for Rapyd {
let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
@ -786,11 +786,11 @@ impl api::IncomingWebhook for Rapyd {
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&secret,
&connector_webhook_secrets.secret,
)
.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()
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
.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_sdk_public_key: None,
connector_merchant_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 signature = self.get_webhook_source_verification_signature(request)?;
let mut secret = self
let mut connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
@ -585,11 +585,11 @@ impl api::IncomingWebhook for Zen {
let mut message = self.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&secret,
&connector_webhook_secrets.secret,
)?;
message.append(&mut secret);
message.append(&mut connector_webhook_secrets.secret);
algorithm
.verify_signature(&secret, &signature, &message)
.verify_signature(&connector_webhook_secrets.secret, &signature, &message)
.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 } },
connector_reference_id: None,
connector_sdk_public_key: None,
connector_merchant_id: None,
},
)),
}),

View File

@ -82,7 +82,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
connector_name: &str,
key_store: &domain::MerchantKeyStore,
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 debug_suffix = format!(
"For merchant_id: {}, and connector_name: {}",
@ -106,22 +106,34 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
)
.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 {
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>(
"MerchantConnectorWebhookDetails",
)
.change_context_lazy(|| errors::ConnectorError::WebhookSourceVerificationFailed)
.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(),
None => default_secret,
.expose()
.into_bytes(),
additional_secret: connector_webhook_details.additional_secret,
}
}
None => api_models::webhooks::ConnectorWebhookSecrets {
secret: default_secret.into_bytes(),
additional_secret: None,
},
},
Err(err) => {
logger::error!(
@ -129,7 +141,10 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
debug_suffix
);
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.
//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(
@ -172,7 +187,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
let signature = self
.get_webhook_source_verification_signature(request)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
let secret = self
let connector_webhook_secrets = self
.get_webhook_source_verification_merchant_secret(
db,
merchant_account,
@ -186,11 +201,11 @@ pub trait IncomingWebhook: ConnectorCommon + Sync {
.get_webhook_source_verification_message(
request,
&merchant_account.merchant_id,
&secret,
&connector_webhook_secrets.secret,
)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?;
algorithm
.verify_signature(&secret, &signature, &message)
.verify_signature(&connector_webhook_secrets.secret, &signature, &message)
.change_context(errors::ConnectorError::WebhookSourceVerificationFailed)
}

View File

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