mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 19:42:27 +08:00
feat(webhooks): Webhook source verification (#2069)
This commit is contained in:
committed by
GitHub
parent
f8410b5b2a
commit
8b22f38dd6
@ -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."
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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>>,
|
||||
}
|
||||
|
||||
@ -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)?;
|
||||
|
||||
|
||||
@ -427,6 +427,7 @@ impl TryFrom<types::PaymentsSessionResponseRouterData<BluesnapWalletTokenRespons
|
||||
},
|
||||
connector_reference_id: None,
|
||||
connector_sdk_public_key: None,
|
||||
connector_merchant_id: None,
|
||||
},
|
||||
)),
|
||||
}),
|
||||
|
||||
@ -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")?;
|
||||
|
||||
@ -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<'_>,
|
||||
|
||||
@ -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()),
|
||||
}
|
||||
|
||||
@ -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")?;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
)),
|
||||
}),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user