feat(core): add support to generate session token response from both connector_wallets_details and metadata (#7140)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sakil Mostak
2025-02-13 13:10:28 +05:30
committed by GitHub
parent 6aac16e0c9
commit 66d9c731f5
11 changed files with 297 additions and 163 deletions

View File

@ -199,24 +199,6 @@ impl SecretsHandler for settings::PazeDecryptConfig {
}
}
#[async_trait::async_trait]
impl SecretsHandler for settings::GooglePayDecryptConfig {
async fn convert_to_raw_secret(
value: SecretStateContainer<Self, SecuredSecret>,
secret_management_client: &dyn SecretManagementInterface,
) -> CustomResult<SecretStateContainer<Self, RawSecret>, SecretsManagementError> {
let google_pay_decrypt_keys = value.get_inner();
let google_pay_root_signing_keys = secret_management_client
.get_secret(google_pay_decrypt_keys.google_pay_root_signing_keys.clone())
.await?;
Ok(value.transition_state(|_| Self {
google_pay_root_signing_keys,
}))
}
}
#[async_trait::async_trait]
impl SecretsHandler for settings::ApplepayMerchantConfigs {
async fn convert_to_raw_secret(
@ -438,20 +420,6 @@ pub(crate) async fn fetch_raw_secrets(
None
};
#[allow(clippy::expect_used)]
let google_pay_decrypt_keys = if let Some(google_pay_keys) = conf.google_pay_decrypt_keys {
Some(
settings::GooglePayDecryptConfig::convert_to_raw_secret(
google_pay_keys,
secret_management_client,
)
.await
.expect("Failed to decrypt google pay decrypt configs"),
)
} else {
None
};
#[allow(clippy::expect_used)]
let applepay_merchant_configs = settings::ApplepayMerchantConfigs::convert_to_raw_secret(
conf.applepay_merchant_configs,
@ -544,7 +512,7 @@ pub(crate) async fn fetch_raw_secrets(
payouts: conf.payouts,
applepay_decrypt_keys,
paze_decrypt_keys,
google_pay_decrypt_keys,
google_pay_decrypt_keys: conf.google_pay_decrypt_keys,
multiple_api_version_supported_connectors: conf.multiple_api_version_supported_connectors,
applepay_merchant_configs,
lock_settings: conf.lock_settings,

View File

@ -99,7 +99,7 @@ pub struct Settings<S: SecretState> {
pub payout_method_filters: ConnectorFilters,
pub applepay_decrypt_keys: SecretStateContainer<ApplePayDecryptConfig, S>,
pub paze_decrypt_keys: Option<SecretStateContainer<PazeDecryptConfig, S>>,
pub google_pay_decrypt_keys: Option<SecretStateContainer<GooglePayDecryptConfig, S>>,
pub google_pay_decrypt_keys: Option<GooglePayDecryptConfig>,
pub multiple_api_version_supported_connectors: MultipleApiVersionSupportedConnectors,
pub applepay_merchant_configs: SecretStateContainer<ApplepayMerchantConfigs, S>,
pub lock_settings: LockSettings,
@ -919,7 +919,7 @@ impl Settings<SecuredSecret> {
self.google_pay_decrypt_keys
.as_ref()
.map(|x| x.get_inner().validate())
.map(|x| x.validate())
.transpose()?;
self.key_manager.get_inner().validate()?;

View File

@ -1364,10 +1364,12 @@ impl From<GpayTokenizationSpecification> for api_models::payments::GpayTokenizat
impl From<GpayTokenParameters> for api_models::payments::GpayTokenParameters {
fn from(value: GpayTokenParameters) -> Self {
Self {
gateway: value.gateway,
gateway: Some(value.gateway),
gateway_merchant_id: Some(value.gateway_merchant_id.expose()),
stripe_version: None,
stripe_publishable_key: None,
public_key: None,
protocol_version: None,
}
}
}

View File

@ -220,3 +220,9 @@ pub const DEFAULT_PAYMENT_METHOD_SESSION_EXPIRY: u32 = 15 * 60; // 15 minutes
/// Authorize flow identifier used for performing GSM operations
pub const AUTHORIZE_FLOW_STR: &str = "Authorize";
/// Protocol Version for encrypted Google Pay Token
pub(crate) const PROTOCOL: &str = "ECv2";
/// Sender ID for Google Pay Decryption
pub(crate) const SENDER_ID: &[u8] = b"Google";

View File

@ -246,8 +246,6 @@ pub enum PazeDecryptionError {
#[derive(Debug, thiserror::Error)]
pub enum GooglePayDecryptionError {
#[error("Recipient ID not found")]
RecipientIdNotFound,
#[error("Invalid expiration time")]
InvalidExpirationTime,
#[error("Failed to base64 decode input data")]

View File

@ -4381,22 +4381,13 @@ fn get_google_pay_connector_wallet_details(
state: &SessionState,
merchant_connector_account: &helpers::MerchantConnectorAccountType,
) -> Option<GooglePayPaymentProcessingDetails> {
let google_pay_root_signing_keys =
state
.conf
.google_pay_decrypt_keys
.as_ref()
.map(|google_pay_keys| {
google_pay_keys
.get_inner()
.google_pay_root_signing_keys
.clone()
});
match (
google_pay_root_signing_keys,
merchant_connector_account.get_connector_wallets_details(),
) {
(Some(google_pay_root_signing_keys), Some(wallet_details)) => {
let google_pay_root_signing_keys = state
.conf
.google_pay_decrypt_keys
.as_ref()
.map(|google_pay_keys| google_pay_keys.google_pay_root_signing_keys.clone());
match merchant_connector_account.get_connector_wallets_details() {
Some(wallet_details) => {
let google_pay_wallet_details = wallet_details
.parse_value::<api_models::payments::GooglePayWalletDetails>(
"GooglePayWalletDetails",
@ -4407,31 +4398,43 @@ fn get_google_pay_connector_wallet_details(
google_pay_wallet_details
.ok()
.map(
.and_then(
|google_pay_wallet_details| {
match google_pay_wallet_details
.google_pay
.provider_details {
api_models::payments::GooglePayProviderDetails::GooglePayMerchantDetails(merchant_details) => {
GooglePayPaymentProcessingDetails {
google_pay_private_key: merchant_details
match (
merchant_details
.merchant_info
.tokenization_specification
.parameters
.private_key,
google_pay_root_signing_keys,
google_pay_recipient_id: merchant_details
merchant_details
.merchant_info
.tokenization_specification
.parameters
.recipient_id,
}
) {
(Some(google_pay_private_key), Some(google_pay_root_signing_keys), Some(google_pay_recipient_id)) => {
Some(GooglePayPaymentProcessingDetails {
google_pay_private_key,
google_pay_root_signing_keys,
google_pay_recipient_id
})
}
_ => {
logger::warn!("One or more of the following fields are missing in GooglePayMerchantDetails: google_pay_private_key, google_pay_root_signing_keys, google_pay_recipient_id");
None
}
}
}
}
}
)
}
_ => None,
None => None,
}
}
@ -4557,7 +4560,7 @@ pub struct PazePaymentProcessingDetails {
pub struct GooglePayPaymentProcessingDetails {
pub google_pay_private_key: Secret<String>,
pub google_pay_root_signing_keys: Secret<String>,
pub google_pay_recipient_id: Option<Secret<String>>,
pub google_pay_recipient_id: Secret<String>,
}
#[derive(Clone, Debug)]

View File

@ -1,4 +1,4 @@
use api_models::payments as payment_types;
use api_models::{admin as admin_types, payments as payment_types};
use async_trait::async_trait;
use common_utils::{
ext_traits::ByteSliceExt,
@ -8,10 +8,11 @@ use common_utils::{
use error_stack::{Report, ResultExt};
#[cfg(feature = "v2")]
use hyperswitch_domain_models::payments::PaymentIntentData;
use masking::ExposeInterface;
use masking::{ExposeInterface, ExposeOptionInterface};
use super::{ConstructFlowSpecificData, Feature};
use crate::{
consts::PROTOCOL,
core::{
errors::{self, ConnectorErrorExt, RouterResult},
payments::{self, access_token, helpers, transformers, PaymentData},
@ -828,6 +829,21 @@ fn create_gpay_session_token(
connector: &api::ConnectorData,
business_profile: &domain::Profile,
) -> RouterResult<types::PaymentsSessionRouterData> {
// connector_wallet_details is being parse into admin types to check specifically if google_pay field is present
// this is being done because apple_pay details from metadata is also being filled into connector_wallets_details
let connector_wallets_details = router_data
.connector_wallets_details
.clone()
.parse_value::<admin_types::ConnectorWalletDetails>("ConnectorWalletDetails")
.change_context(errors::ConnectorError::NoConnectorWalletDetails)
.attach_printable(format!(
"cannot parse connector_wallets_details from the given value {:?}",
router_data.connector_wallets_details
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_wallets_details".to_string(),
expected_format: "admin_types_connector_wallets_details_format".to_string(),
})?;
let connector_metadata = router_data.connector_meta_data.clone();
let delayed_response = is_session_response_delayed(state, connector);
@ -849,18 +865,6 @@ fn create_gpay_session_token(
..router_data.clone()
})
} else {
let gpay_data = connector_metadata
.clone()
.parse_value::<payment_types::GpaySessionTokenData>("GpaySessionTokenData")
.change_context(errors::ConnectorError::NoConnectorMetaData)
.attach_printable(format!(
"cannot parse gpay metadata from the given value {connector_metadata:?}"
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_metadata".to_string(),
expected_format: "gpay_metadata_format".to_string(),
})?;
let always_collect_billing_details_from_wallet_connector = business_profile
.always_collect_billing_details_from_wallet_connector
.unwrap_or(false);
@ -883,27 +887,6 @@ fn create_gpay_session_token(
false
};
let billing_address_parameters =
is_billing_details_required.then_some(payment_types::GpayBillingAddressParameters {
phone_number_required: is_billing_details_required,
format: payment_types::GpayBillingAddressFormat::FULL,
});
let gpay_allowed_payment_methods = gpay_data
.data
.allowed_payment_methods
.into_iter()
.map(
|allowed_payment_methods| payment_types::GpayAllowedPaymentMethods {
parameters: payment_types::GpayAllowedMethodsParameters {
billing_address_required: Some(is_billing_details_required),
billing_address_parameters: billing_address_parameters.clone(),
..allowed_payment_methods.parameters
},
..allowed_payment_methods
},
)
.collect();
let required_amount_type = StringMajorUnitForConnector;
let google_pay_amount = required_amount_type
.convert(
@ -945,39 +928,186 @@ fn create_gpay_session_token(
false
};
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse::GooglePaySession(
payment_types::GooglePaySessionResponse {
merchant_info: gpay_data.data.merchant_info,
allowed_payment_methods: gpay_allowed_payment_methods,
transaction_info,
connector: connector.connector_name.to_string(),
sdk_next_action: payment_types::SdkNextAction {
next_action: payment_types::NextActionCall::Confirm,
},
delayed_session_token: false,
secrets: None,
shipping_address_required: required_shipping_contact_fields,
// We pass Email as a required field irrespective of
// collect_billing_details_from_wallet_connector or
// collect_shipping_details_from_wallet_connector as it is common to both.
email_required: required_shipping_contact_fields
|| is_billing_details_required,
shipping_address_parameters:
api_models::payments::GpayShippingAddressParameters {
phone_number_required: required_shipping_contact_fields,
if connector_wallets_details.google_pay.is_some() {
let gpay_data = router_data
.connector_wallets_details
.clone()
.parse_value::<payment_types::GooglePayWalletDetails>("GooglePayWalletDetails")
.change_context(errors::ConnectorError::NoConnectorWalletDetails)
.attach_printable(format!(
"cannot parse gpay connector_wallets_details from the given value {:?}",
router_data.connector_wallets_details
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_wallets_details".to_string(),
expected_format: "gpay_connector_wallets_details_format".to_string(),
})?;
let payment_types::GooglePayProviderDetails::GooglePayMerchantDetails(gpay_info) =
gpay_data.google_pay.provider_details.clone();
let gpay_allowed_payment_methods = get_allowed_payment_methods_from_cards(
gpay_data,
&gpay_info.merchant_info.tokenization_specification,
is_billing_details_required,
)?;
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse::GooglePaySession(
payment_types::GooglePaySessionResponse {
merchant_info: payment_types::GpayMerchantInfo {
merchant_name: gpay_info.merchant_info.merchant_name,
merchant_id: gpay_info.merchant_info.merchant_id,
},
allowed_payment_methods: vec![gpay_allowed_payment_methods],
transaction_info,
connector: connector.connector_name.to_string(),
sdk_next_action: payment_types::SdkNextAction {
next_action: payment_types::NextActionCall::Confirm,
},
delayed_session_token: false,
secrets: None,
shipping_address_required: required_shipping_contact_fields,
// We pass Email as a required field irrespective of
// collect_billing_details_from_wallet_connector or
// collect_shipping_details_from_wallet_connector as it is common to both.
email_required: required_shipping_contact_fields
|| is_billing_details_required,
shipping_address_parameters:
api_models::payments::GpayShippingAddressParameters {
phone_number_required: required_shipping_contact_fields,
},
},
),
)),
}),
..router_data.clone()
})
} else {
let billing_address_parameters = is_billing_details_required.then_some(
payment_types::GpayBillingAddressParameters {
phone_number_required: is_billing_details_required,
format: payment_types::GpayBillingAddressFormat::FULL,
},
);
let gpay_data = connector_metadata
.clone()
.parse_value::<payment_types::GpaySessionTokenData>("GpaySessionTokenData")
.change_context(errors::ConnectorError::NoConnectorMetaData)
.attach_printable(format!(
"cannot parse gpay metadata from the given value {connector_metadata:?}"
))
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "connector_metadata".to_string(),
expected_format: "gpay_metadata_format".to_string(),
})?;
let gpay_allowed_payment_methods = gpay_data
.data
.allowed_payment_methods
.into_iter()
.map(
|allowed_payment_methods| payment_types::GpayAllowedPaymentMethods {
parameters: payment_types::GpayAllowedMethodsParameters {
billing_address_required: Some(is_billing_details_required),
billing_address_parameters: billing_address_parameters.clone(),
..allowed_payment_methods.parameters
},
),
)),
}),
..router_data.clone()
})
..allowed_payment_methods
},
)
.collect();
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::GooglePay(Box::new(
payment_types::GpaySessionTokenResponse::GooglePaySession(
payment_types::GooglePaySessionResponse {
merchant_info: gpay_data.data.merchant_info,
allowed_payment_methods: gpay_allowed_payment_methods,
transaction_info,
connector: connector.connector_name.to_string(),
sdk_next_action: payment_types::SdkNextAction {
next_action: payment_types::NextActionCall::Confirm,
},
delayed_session_token: false,
secrets: None,
shipping_address_required: required_shipping_contact_fields,
// We pass Email as a required field irrespective of
// collect_billing_details_from_wallet_connector or
// collect_shipping_details_from_wallet_connector as it is common to both.
email_required: required_shipping_contact_fields
|| is_billing_details_required,
shipping_address_parameters:
api_models::payments::GpayShippingAddressParameters {
phone_number_required: required_shipping_contact_fields,
},
},
),
)),
}),
..router_data.clone()
})
}
}
}
/// Card Type for Google Pay Allowerd Payment Methods
pub(crate) const CARD: &str = "CARD";
fn get_allowed_payment_methods_from_cards(
gpay_info: payment_types::GooglePayWalletDetails,
gpay_token_specific_data: &payment_types::GooglePayTokenizationSpecification,
is_billing_details_required: bool,
) -> RouterResult<payment_types::GpayAllowedPaymentMethods> {
let billing_address_parameters =
is_billing_details_required.then_some(payment_types::GpayBillingAddressParameters {
phone_number_required: is_billing_details_required,
format: payment_types::GpayBillingAddressFormat::FULL,
});
let protocol_version: Option<String> = gpay_token_specific_data
.parameters
.public_key
.as_ref()
.map(|_| PROTOCOL.to_string());
Ok(payment_types::GpayAllowedPaymentMethods {
parameters: payment_types::GpayAllowedMethodsParameters {
billing_address_required: Some(is_billing_details_required),
billing_address_parameters: billing_address_parameters.clone(),
..gpay_info.google_pay.cards
},
payment_method_type: CARD.to_string(),
tokenization_specification: payment_types::GpayTokenizationSpecification {
token_specification_type: gpay_token_specific_data.tokenization_type.to_string(),
parameters: payment_types::GpayTokenParameters {
protocol_version,
public_key: gpay_token_specific_data.parameters.public_key.clone(),
gateway: gpay_token_specific_data.parameters.gateway.clone(),
gateway_merchant_id: gpay_token_specific_data
.parameters
.gateway_merchant_id
.clone()
.expose_option(),
stripe_publishable_key: gpay_token_specific_data
.parameters
.stripe_publishable_key
.clone()
.expose_option(),
stripe_version: gpay_token_specific_data
.parameters
.stripe_version
.clone()
.expose_option(),
},
},
})
}
fn is_session_response_delayed(
state: &routes::SessionState,
connector: &api::ConnectorData,

View File

@ -47,8 +47,8 @@ use openssl::{
};
#[cfg(feature = "v2")]
use redis_interface::errors::RedisError;
use ring::hmac;
use router_env::{instrument, logger, tracing};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use x509_parser::parse_x509_certificate;
@ -5267,7 +5267,7 @@ where
Ok(connector_data_list)
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ApplePayData {
version: masking::Secret<String>,
data: masking::Secret<String>,
@ -5275,7 +5275,7 @@ pub struct ApplePayData {
header: ApplePayHeader,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApplePayHeader {
ephemeral_public_key: masking::Secret<String>,
@ -5430,13 +5430,10 @@ impl ApplePayData {
}
}
pub(crate) const SENDER_ID: &[u8] = b"Google";
pub(crate) const PROTOCOL: &str = "ECv2";
// Structs for keys and the main decryptor
pub struct GooglePayTokenDecryptor {
root_signing_keys: Vec<GooglePayRootSigningKey>,
recipient_id: Option<masking::Secret<String>>,
recipient_id: masking::Secret<String>,
private_key: PKey<openssl::pkey::Private>,
}
@ -5543,21 +5540,24 @@ fn filter_root_signing_keys(
impl GooglePayTokenDecryptor {
pub fn new(
root_keys: masking::Secret<String>,
recipient_id: Option<masking::Secret<String>>,
recipient_id: masking::Secret<String>,
private_key: masking::Secret<String>,
) -> CustomResult<Self, errors::GooglePayDecryptionError> {
// base64 decode the private key
let decoded_key = BASE64_ENGINE
.decode(private_key.expose())
.change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?;
// base64 decode the root signing keys
let decoded_root_signing_keys = BASE64_ENGINE
.decode(root_keys.expose())
.change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?;
// create a private key from the decoded key
let private_key = PKey::private_key_from_pkcs8(&decoded_key)
.change_context(errors::GooglePayDecryptionError::KeyDeserializationFailed)
.attach_printable("cannot convert private key from decode_key")?;
// parse the root signing keys
let root_keys_vector: Vec<GooglePayRootSigningKey> = root_keys
.expose()
let root_keys_vector: Vec<GooglePayRootSigningKey> = decoded_root_signing_keys
.parse_struct("GooglePayRootSigningKey")
.change_context(errors::GooglePayDecryptionError::DeserializationFailed)?;
@ -5663,13 +5663,13 @@ impl GooglePayTokenDecryptor {
}
// get the sender id i.e. Google
let sender_id = String::from_utf8(SENDER_ID.to_vec())
let sender_id = String::from_utf8(consts::SENDER_ID.to_vec())
.change_context(errors::GooglePayDecryptionError::DeserializationFailed)?;
// construct the signed data
let signed_data = self.construct_signed_data_for_intermediate_signing_key_verification(
&sender_id,
PROTOCOL,
consts::PROTOCOL,
encrypted_data.intermediate_signing_key.signed_key.peek(),
)?;
@ -5770,7 +5770,7 @@ impl GooglePayTokenDecryptor {
.change_context(errors::GooglePayDecryptionError::DerivingEcKeyFailed)?;
// get the sender id i.e. Google
let sender_id = String::from_utf8(SENDER_ID.to_vec())
let sender_id = String::from_utf8(consts::SENDER_ID.to_vec())
.change_context(errors::GooglePayDecryptionError::DeserializationFailed)?;
// serialize the signed message to string
@ -5780,7 +5780,7 @@ impl GooglePayTokenDecryptor {
// construct the signed data
let signed_data = self.construct_signed_data_for_signature_verification(
&sender_id,
PROTOCOL,
consts::PROTOCOL,
&signed_message,
)?;
@ -5827,11 +5827,7 @@ impl GooglePayTokenDecryptor {
protocol_version: &str,
signed_key: &str,
) -> CustomResult<Vec<u8>, errors::GooglePayDecryptionError> {
let recipient_id = self
.recipient_id
.clone()
.ok_or(errors::GooglePayDecryptionError::RecipientIdNotFound)?
.expose();
let recipient_id = self.recipient_id.clone().expose();
let length_of_sender_id = u32::try_from(sender_id.len())
.change_context(errors::GooglePayDecryptionError::ParsingFailed)?;
let length_of_recipient_id = u32::try_from(recipient_id.len())
@ -5911,13 +5907,14 @@ impl GooglePayTokenDecryptor {
// derive 64 bytes for the output key (symmetric encryption + MAC key)
let mut output_key = vec![0u8; 64];
hkdf.expand(SENDER_ID, &mut output_key).map_err(|err| {
logger::error!(
hkdf.expand(consts::SENDER_ID, &mut output_key)
.map_err(|err| {
logger::error!(
"Failed to derive the shared ephemeral key for Google Pay decryption flow: {:?}",
err
);
report!(errors::GooglePayDecryptionError::DerivingSharedEphemeralKeyFailed)
})?;
report!(errors::GooglePayDecryptionError::DerivingSharedEphemeralKeyFailed)
})?;
Ok(output_key)
}
@ -5930,8 +5927,8 @@ impl GooglePayTokenDecryptor {
tag: &[u8],
encrypted_message: &[u8],
) -> CustomResult<(), errors::GooglePayDecryptionError> {
let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, mac_key);
hmac::verify(&hmac_key, encrypted_message, tag)
let hmac_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, mac_key);
ring::hmac::verify(&hmac_key, encrypted_message, tag)
.change_context(errors::GooglePayDecryptionError::HmacVerificationFailed)
}
@ -6024,7 +6021,7 @@ pub fn decrypt_paze_token(
Ok(parsed_decrypted)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JwsBody {
pub payload_id: String,