From 24401bc16f9677ce0f5fa70d739e5e6885c7e907 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:04:45 +0530 Subject: [PATCH] feat(payment_methods_v2): Added Ephemeral auth for v2 (#6813) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 15 +- crates/api_models/src/ephemeral_key.rs | 43 +++++ crates/api_models/src/payment_methods.rs | 14 +- crates/common_utils/src/events.rs | 3 + crates/common_utils/src/id_type.rs | 2 + .../common_utils/src/id_type/ephemeral_key.rs | 31 +++ crates/diesel_models/src/ephemeral_key.rs | 48 +++++ crates/diesel_models/src/payment_method.rs | 13 +- .../hyperswitch_domain_models/src/payments.rs | 5 +- crates/router/src/consts.rs | 8 +- crates/router/src/core/payment_methods.rs | 127 ++++++------ .../src/core/payment_methods/transformers.rs | 3 +- .../router/src/core/payment_methods/vault.rs | 9 +- crates/router/src/core/payments/helpers.rs | 108 ++++++++++- crates/router/src/db/ephemeral_key.rs | 180 ++++++++++++++++++ crates/router/src/db/kafka_store.rs | 32 ++++ crates/router/src/lib.rs | 9 +- crates/router/src/routes/app.rs | 23 ++- crates/router/src/routes/ephemeral_key.rs | 34 +++- crates/router/src/routes/payment_methods.rs | 12 +- crates/router/src/services/authentication.rs | 66 ++++++- crates/router/src/types/payment_methods.rs | 12 +- .../router/src/types/storage/ephemeral_key.rs | 17 ++ 23 files changed, 688 insertions(+), 126 deletions(-) create mode 100644 crates/common_utils/src/id_type/ephemeral_key.rs diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index b1c33c253af2..14e624233c36 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -13870,16 +13870,11 @@ "PaymentMethodIntentConfirm": { "type": "object", "required": [ - "client_secret", "payment_method_data", "payment_method_type", "payment_method_subtype" ], "properties": { - "client_secret": { - "type": "string", - "description": "For SDK based calls, client_secret would be required" - }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -14165,7 +14160,7 @@ "example": "2024-02-24T11:04:09.922Z", "nullable": true }, - "client_secret": { + "ephemeral_key": { "type": "string", "description": "For Client based calls", "nullable": true @@ -14314,14 +14309,6 @@ "properties": { "payment_method_data": { "$ref": "#/components/schemas/PaymentMethodUpdateData" - }, - "client_secret": { - "type": "string", - "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", - "example": "secret_k2uj3he2893eiu2d", - "nullable": true, - "maxLength": 30, - "minLength": 30 } }, "additionalProperties": false diff --git a/crates/api_models/src/ephemeral_key.rs b/crates/api_models/src/ephemeral_key.rs index d06490d6bac2..fa61642e353a 100644 --- a/crates/api_models/src/ephemeral_key.rs +++ b/crates/api_models/src/ephemeral_key.rs @@ -1,7 +1,10 @@ use common_utils::id_type; +#[cfg(feature = "v2")] +use masking::Secret; use serde; use utoipa::ToSchema; +#[cfg(feature = "v1")] /// Information required to create an ephemeral key. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct EphemeralKeyCreateRequest { @@ -15,12 +18,52 @@ pub struct EphemeralKeyCreateRequest { pub customer_id: id_type::CustomerId, } +#[cfg(feature = "v2")] +/// Information required to create an ephemeral key. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct EphemeralKeyCreateRequest { + /// Customer ID for which an ephemeral key must be created + #[schema( + min_length = 32, + max_length = 64, + value_type = String, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8" + )] + pub customer_id: id_type::GlobalCustomerId, +} + +#[cfg(feature = "v2")] +/// ephemeral_key for the customer_id mentioned +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] +pub struct EphemeralKeyResponse { + /// Ephemeral key id + #[schema(value_type = String, max_length = 32, min_length = 1)] + pub id: id_type::EphemeralKeyId, + /// customer_id to which this ephemeral key belongs to + #[schema(value_type = String, max_length = 64, min_length = 32, example = "12345_cus_01926c58bc6e77c09e809964e72af8c8")] + pub customer_id: id_type::GlobalCustomerId, + /// time at which this ephemeral key was created + pub created_at: time::PrimitiveDateTime, + /// time at which this ephemeral key would expire + pub expires: time::PrimitiveDateTime, + #[schema(value_type=String)] + /// ephemeral key + pub secret: Secret, +} + impl common_utils::events::ApiEventMetric for EphemeralKeyCreateRequest { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric for EphemeralKeyResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + /// ephemeral_key for the customer_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] pub struct EphemeralKeyCreateResponse { diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c821feaff575..fe5cb2933f9e 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -163,9 +163,6 @@ pub struct PaymentMethodIntentCreate { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodIntentConfirm { - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -211,9 +208,6 @@ pub struct PaymentMethodIntentConfirmInternal { #[schema(value_type = PaymentMethodType,example = "credit")] pub payment_method_subtype: api_enums::PaymentMethodType, - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -226,7 +220,6 @@ pub struct PaymentMethodIntentConfirmInternal { impl From for PaymentMethodIntentConfirm { fn from(item: PaymentMethodIntentConfirmInternal) -> Self { Self { - client_secret: item.client_secret, payment_method_type: item.payment_method_type, payment_method_subtype: item.payment_method_subtype, customer_id: item.customer_id, @@ -408,10 +401,6 @@ pub struct PaymentMethodUpdate { pub struct PaymentMethodUpdate { /// payment method data to be passed pub payment_method_data: PaymentMethodUpdateData, - - /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK - #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")] - pub client_secret: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -826,7 +815,8 @@ pub struct PaymentMethodResponse { pub last_used_at: Option, /// For Client based calls - pub client_secret: Option, + #[schema(value_type=Option)] + pub ephemeral_key: Option>, pub payment_method_data: Option, } diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index b9af92786f58..9494b8209e75 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -102,6 +102,9 @@ pub enum ApiEventsType { poll_id: String, }, Analytics, + EphemeralKey { + key_id: id_type::EphemeralKeyId, + }, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index c7393155226b..ea90c00121e8 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -3,6 +3,7 @@ mod api_key; mod customer; +mod ephemeral_key; #[cfg(feature = "v2")] mod global_id; mod merchant; @@ -38,6 +39,7 @@ pub use self::global_id::{ pub use self::{ api_key::ApiKeyId, customer::CustomerId, + ephemeral_key::EphemeralKeyId, merchant::MerchantId, merchant_connector_account::MerchantConnectorAccountId, organization::OrganizationId, diff --git a/crates/common_utils/src/id_type/ephemeral_key.rs b/crates/common_utils/src/id_type/ephemeral_key.rs new file mode 100644 index 000000000000..071980fc6a46 --- /dev/null +++ b/crates/common_utils/src/id_type/ephemeral_key.rs @@ -0,0 +1,31 @@ +crate::id_type!( + EphemeralKeyId, + "A type for key_id that can be used for Ephemeral key IDs" +); +crate::impl_id_type_methods!(EphemeralKeyId, "key_id"); + +// This is to display the `EphemeralKeyId` as EphemeralKeyId(abcd) +crate::impl_debug_id_type!(EphemeralKeyId); +crate::impl_try_from_cow_str_id_type!(EphemeralKeyId, "key_id"); + +crate::impl_generate_id_id_type!(EphemeralKeyId, "eki"); +crate::impl_serializable_secret_id_type!(EphemeralKeyId); +crate::impl_queryable_id_type!(EphemeralKeyId); +crate::impl_to_sql_from_sql_id_type!(EphemeralKeyId); + +impl crate::events::ApiEventMetric for EphemeralKeyId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::EphemeralKey { + key_id: self.clone(), + }) + } +} + +crate::impl_default_id_type!(EphemeralKeyId, "key"); + +impl EphemeralKeyId { + /// Generate a key for redis + pub fn generate_redis_key(&self) -> String { + format!("epkey_{}", self.get_string_repr()) + } +} diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index d398ecdf784a..c7fc103ed09b 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -1,3 +1,33 @@ +#[cfg(feature = "v2")] +use masking::{PeekInterface, Secret}; +#[cfg(feature = "v2")] +pub struct EphemeralKeyTypeNew { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub secret: Secret, + pub resource_type: ResourceType, +} + +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EphemeralKeyType { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub resource_type: ResourceType, + pub created_at: time::PrimitiveDateTime, + pub expires: time::PrimitiveDateTime, + pub secret: Secret, +} + +#[cfg(feature = "v2")] +impl EphemeralKeyType { + pub fn generate_secret_key(&self) -> String { + format!("epkey_{}", self.secret.peek()) + } +} + pub struct EphemeralKeyNew { pub id: String, pub merchant_id: common_utils::id_type::MerchantId, @@ -20,3 +50,21 @@ impl common_utils::events::ApiEventMetric for EphemeralKey { Some(common_utils::events::ApiEventsType::Miscellaneous) } } + +#[derive( + Clone, + Copy, + Debug, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + PartialEq, + Eq, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ResourceType { + Payment, + PaymentMethod, +} diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 8ae34a6f89cf..76613984363d 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -290,6 +290,7 @@ pub enum PaymentMethodUpdate { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -324,6 +325,7 @@ pub struct PaymentMethodUpdateInternal { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -343,6 +345,7 @@ impl PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } = self; PaymentMethod { @@ -361,7 +364,7 @@ impl PaymentMethodUpdateInternal { client_secret: source.client_secret, payment_method_billing_address: source.payment_method_billing_address, updated_by: updated_by.or(source.updated_by), - locker_fingerprint_id: source.locker_fingerprint_id, + locker_fingerprint_id: locker_fingerprint_id.or(source.locker_fingerprint_id), payment_method_type_v2: payment_method_type_v2.or(source.payment_method_type_v2), payment_method_subtype: payment_method_subtype.or(source.payment_method_subtype), id: source.id, @@ -710,6 +713,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { payment_method_data: None, @@ -725,6 +729,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::UpdatePaymentMethodDataAndLastUsed { payment_method_data, @@ -744,6 +749,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -762,6 +768,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { payment_method_data: None, @@ -777,6 +784,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, @@ -787,6 +795,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } => Self { payment_method_data, last_used_at: None, @@ -801,6 +810,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -818,6 +828,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 40ab88663c9e..1c88b83d087e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -29,7 +29,10 @@ pub mod payment_intent; use common_enums as storage_enums; #[cfg(feature = "v2")] -use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; +use diesel_models::{ + ephemeral_key, + types::{FeatureMetadata, OrderDetailsWithAmount}, +}; use self::payment_attempt::PaymentAttempt; #[cfg(feature = "v1")] diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 823f0a4bf04f..e2784b968ad1 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -171,19 +171,19 @@ pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; /// Vault Add request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add"; +pub const ADD_VAULT_REQUEST_URL: &str = "/api/v2/vault/add"; /// Vault Get Fingerprint request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/fingerprint"; +pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/api/v2/vault/fingerprint"; /// Vault Retrieve request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/vault/retrieve"; +pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/api/v2/vault/retrieve"; /// Vault Delete request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_DELETE_REQUEST_URL: &str = "/vault/delete"; +pub const VAULT_DELETE_REQUEST_URL: &str = "/api/v2/vault/delete"; /// Vault Header content type #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 61dc70ac4799..6cdadf07320c 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -62,7 +62,7 @@ use crate::{ types::{ api::{self, payment_methods::PaymentMethodCreateExt}, payment_methods as pm_types, - storage::PaymentMethodListContext, + storage::{ephemeral_key, PaymentMethodListContext}, }, utils::ext_traits::OptionExt, }; @@ -851,21 +851,23 @@ pub async fn create_payment_method( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -877,7 +879,7 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -902,14 +904,15 @@ pub async fn create_payment_method( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -926,7 +929,10 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response( + &payment_method, + Some(ephemeral_key), + )?; Ok(resp) } @@ -964,21 +970,23 @@ pub async fn payment_method_intent_create( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -991,7 +999,7 @@ pub async fn payment_method_intent_create( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -1004,7 +1012,8 @@ pub async fn payment_method_intent_create( .await .attach_printable("Failed to add Payment method to DB")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = + pm_transforms::generate_payment_method_response(&payment_method, Some(ephemeral_key))?; Ok(services::ApplicationResponse::Json(resp)) } @@ -1018,10 +1027,10 @@ pub async fn payment_method_intent_confirm( key_store: &domain::MerchantKeyStore, pm_id: String, ) -> RouterResponse { + let key_manager_state = &(state).into(); req.validate()?; let db = &*state.store; - let client_secret = req.client_secret.clone(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm_id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; @@ -1037,11 +1046,6 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) .attach_printable("Unable to find payment method")?; - when( - cards::authenticate_pm_client_secret_and_check_expiry(&client_secret, &payment_method)?, - || Err(errors::ApiErrorResponse::ClientSecretExpired), - )?; - when( payment_method.status != enums::PaymentMethodStatus::AwaitingData, || { @@ -1054,7 +1058,7 @@ pub async fn payment_method_intent_confirm( let customer_id = payment_method.customer_id.to_owned(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, @@ -1075,14 +1079,15 @@ pub async fn payment_method_intent_confirm( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1099,7 +1104,7 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response(&payment_method, None)?; Ok(resp) } @@ -1153,7 +1158,6 @@ pub async fn create_payment_method_in_db( card_scheme: Option, ) -> errors::CustomResult { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); let current_time = common_utils::date_time::now(); let response = db @@ -1170,7 +1174,7 @@ pub async fn create_payment_method_in_db( payment_method_data, connector_mandate_details, customer_acceptance, - client_secret: Some(client_secret), + client_secret: None, status: status.unwrap_or(enums::PaymentMethodStatus::Active), network_transaction_id: network_transaction_id.to_owned(), created_at: current_time, @@ -1205,9 +1209,18 @@ pub async fn create_payment_method_for_intent( key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, -) -> errors::CustomResult { +) -> errors::CustomResult<(domain::PaymentMethod, Secret), errors::ApiErrorResponse> { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); + let ephemeral_key = payment_helpers::create_ephemeral_key( + state, + customer_id, + merchant_id, + ephemeral_key::ResourceType::PaymentMethod, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to create ephemeral_key")?; + let current_time = common_utils::date_time::now(); let response = db @@ -1224,7 +1237,7 @@ pub async fn create_payment_method_for_intent( payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, - client_secret: Some(client_secret), + client_secret: None, status: enums::PaymentMethodStatus::AwaitingData, network_transaction_id: None, created_at: current_time, @@ -1244,7 +1257,7 @@ pub async fn create_payment_method_for_intent( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; - Ok(response) + Ok((response, ephemeral_key.secret)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1255,15 +1268,16 @@ pub async fn create_pm_additional_data_update( vault_id: Option, payment_method_type: Option, payment_method_subtype: Option, + vault_fingerprint_id: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { api::PaymentMethodsData::Card(card.clone().into()) } }; - let key_manager_state = state.into(); + let key_manager_state = &(state).into(); let pmd: Encryptable> = - cards::create_encrypted_data(&key_manager_state, key_store, card) + cards::create_encrypted_data(key_manager_state, key_store, card) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method data")?; @@ -1277,6 +1291,7 @@ pub async fn create_pm_additional_data_update( network_token_requestor_reference_id: None, network_token_locker_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: vault_fingerprint_id, }; Ok(pm_update) @@ -1290,7 +1305,7 @@ pub async fn vault_payment_method( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, existing_vault_id: Option, -) -> RouterResult { +) -> RouterResult<(pm_types::AddVaultResponse, String)> { let db = &*state.store; // get fingerprint_id from vault @@ -1301,17 +1316,16 @@ pub async fn vault_payment_method( // throw back error if payment method is duplicated when( - Some( - db.find_payment_method_by_fingerprint_id( - &(state.into()), - key_store, - &fingerprint_id_from_vault, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to find payment method by fingerprint_id")?, + db.find_payment_method_by_fingerprint_id( + &(state.into()), + key_store, + &fingerprint_id_from_vault, ) - .is_some(), + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to find payment method by fingerprint_id") + .inspect_err(|e| logger::error!("Vault Fingerprint_id error: {:?}", e)) + .is_ok(), || { Err(report!(errors::ApiErrorResponse::DuplicatePaymentMethod) .attach_printable("Cannot vault duplicate payment method")) @@ -1324,7 +1338,7 @@ pub async fn vault_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in vault")?; - Ok(resp_from_vault) + Ok((resp_from_vault, fingerprint_id_from_vault)) } #[cfg(all( @@ -1754,6 +1768,11 @@ pub async fn retrieve_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let pmd = payment_method .payment_method_data .clone() @@ -1774,7 +1793,7 @@ pub async fn retrieve_payment_method( created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), - client_secret: payment_method.client_secret.clone(), + ephemeral_key: None, payment_method_data: pmd, }; @@ -1822,16 +1841,12 @@ pub async fn update_payment_method( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to retrieve payment method from vault")? - .data - .expose() - .parse_struct("PaymentMethodVaultingData") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse PaymentMethodVaultingData")?; + .data; let vault_request_data = pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data); - let vaulting_response = vault_payment_method( + let (vaulting_response, fingerprint_id) = vault_payment_method( &state, &vault_request_data, &merchant_account, @@ -1848,6 +1863,7 @@ pub async fn update_payment_method( Some(vaulting_response.vault_id.get_string_repr().clone()), payment_method.get_payment_method_type(), payment_method.get_payment_method_subtype(), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1864,7 +1880,7 @@ pub async fn update_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let response = pm_transforms::generate_payment_method_response(&payment_method)?; + let response = pm_transforms::generate_payment_method_response(&payment_method, None)?; // Add a PT task to handle payment_method delete from vault @@ -1896,6 +1912,11 @@ pub async fn delete_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let vault_id = payment_method .locker_id .clone() diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index c3fbfd8afbff..ce95096a0fb3 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -551,6 +551,7 @@ pub fn generate_pm_vaulting_req_from_update_request( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn generate_payment_method_response( pm: &domain::PaymentMethod, + ephemeral_key: Option>, ) -> errors::RouterResult { let pmd = pm .payment_method_data @@ -572,7 +573,7 @@ pub fn generate_payment_method_response( created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), - client_secret: pm.client_secret.clone(), + ephemeral_key, payment_method_data: pmd, }; diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index fcff711f6c9a..65ef16cb4496 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1186,6 +1186,7 @@ async fn create_vault_request( jwekey: &settings::Jwekey, locker: &settings::Locker, payload: Vec, + tenant_id: id_type::TenantId, ) -> CustomResult { let private_key = jwekey.vault_private_key.peek().as_bytes(); @@ -1206,6 +1207,10 @@ async fn create_vault_request( headers::CONTENT_TYPE, consts::VAULT_HEADER_CONTENT_TYPE.into(), ); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); request.set_body(request::RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -1219,7 +1224,9 @@ pub async fn call_to_vault( let locker = &state.conf.locker; let jwekey = state.conf.jwekey.get_inner(); - let request = create_vault_request::(jwekey, locker, payload).await?; + let request = + create_vault_request::(jwekey, locker, payload, state.tenant.tenant_id.to_owned()) + .await?; let response = services::call_connector_api(state, request, V::get_vaulting_flow_name()) .await .change_context(errors::VaultError::VaultAPIError); diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 78cfdda33b49..c243686a76cd 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,11 +1,15 @@ use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "v2")] +use api_models::ephemeral_key::EphemeralKeyResponse; use api_models::{ mandates::RecurringDetails, payments::{additional_info as payment_additional_types, RequestSurchargeDetails}, }; use base64::Engine; use common_enums::ConnectorType; +#[cfg(feature = "v2")] +use common_utils::id_type::GenerateId; use common_utils::{ crypto::Encryptable, ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt}, @@ -40,6 +44,8 @@ use openssl::{ pkey::PKey, symm::{decrypt_aead, Cipher}, }; +#[cfg(feature = "v2")] +use redis_interface::errors::RedisError; use router_env::{instrument, logger, tracing}; use uuid::Uuid; use x509_parser::parse_x509_certificate; @@ -48,8 +54,6 @@ use super::{ operations::{BoxedOperation, Operation, PaymentResponse}, CustomerDetails, PaymentData, }; -#[cfg(feature = "v2")] -use crate::core::admin as core_admin; use crate::{ configs::settings::{ConnectorRequestReferenceIdConfig, TempLockerEnableConfig}, connector, @@ -84,6 +88,8 @@ use crate::{ OptionExt, StringExt, }, }; +#[cfg(feature = "v2")] +use crate::{core::admin as core_admin, headers}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::payment_methods::cards::create_encrypted_data, types::storage::CustomerUpdate::Update, @@ -3021,6 +3027,7 @@ pub fn make_merchant_url_with_response( Ok(merchant_url_with_response.to_string()) } +#[cfg(feature = "v1")] pub async fn make_ephemeral_key( state: SessionState, customer_id: id_type::CustomerId, @@ -3043,6 +3050,80 @@ pub async fn make_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn make_ephemeral_key( + state: SessionState, + customer_id: id_type::GlobalCustomerId, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + headers: &actix_web::http::header::HeaderMap, +) -> errors::RouterResponse { + let db = &state.store; + let key_manager_state = &((&state).into()); + db.find_customer_by_global_id( + key_manager_state, + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let resource_type = services::authentication::get_header_value_by_key( + headers::X_RESOURCE_TYPE.to_string(), + headers, + )? + .map(ephemeral_key::ResourceType::from_str) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + })? + .get_required_value("ResourceType") + .attach_printable("Failed to convert ResourceType from string")?; + + let ephemeral_key = create_ephemeral_key( + &state, + &customer_id, + merchant_account.get_id(), + resource_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(feature = "v2")] +pub async fn create_ephemeral_key( + state: &SessionState, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + resource_type: ephemeral_key::ResourceType, +) -> RouterResult { + use common_utils::generate_time_ordered_id; + + let store = &state.store; + let id = id_type::EphemeralKeyId::generate(); + let secret = masking::Secret::new(generate_time_ordered_id("epk")); + let ephemeral_key = ephemeral_key::EphemeralKeyTypeNew { + id, + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + secret, + resource_type, + }; + let ephemeral_key = store + .create_ephemeral_key(ephemeral_key, state.conf.eph_key.validity) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + Ok(ephemeral_key) +} + +#[cfg(feature = "v1")] pub async fn delete_ephemeral_key( state: SessionState, ek_id: String, @@ -3056,6 +3137,29 @@ pub async fn delete_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn delete_ephemeral_key( + state: SessionState, + ephemeral_key_id: String, +) -> errors::RouterResponse { + let db = state.store.as_ref(); + let ephemeral_key = db + .delete_ephemeral_key(&ephemeral_key_id) + .await + .map_err(|err| match err.current_context() { + errors::StorageError::ValueNotFound(_) => { + err.change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Ephemeral Key not found".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InternalServerError), + }) + .attach_printable("Unable to delete ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + pub fn make_pg_redirect_response( payment_id: id_type::PaymentId, response: &api::PaymentsResponse, diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index a1d43e83b871..a77995e4e58e 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "v2")] +use common_utils::id_type; use time::ext::NumericalDuration; +#[cfg(feature = "v2")] +use crate::types::storage::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; use crate::{ core::errors::{self, CustomResult}, db::MockDb, @@ -8,30 +12,64 @@ use crate::{ #[async_trait::async_trait] pub trait EphemeralKeyInterface { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, _ek: EphemeralKeyNew, _validity: i64, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + _ek: EphemeralKeyTypeNew, + _validity: i64, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, _key: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + _key: &str, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, _id: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + _id: &str, + ) -> CustomResult; } mod storage { use common_utils::date_time; + #[cfg(feature = "v2")] + use common_utils::id_type; use error_stack::ResultExt; + #[cfg(feature = "v2")] + use masking::PeekInterface; + #[cfg(feature = "v2")] + use redis_interface::errors::RedisError; use redis_interface::HsetnxReply; use router_env::{instrument, tracing}; use storage_impl::redis::kv_store::RedisConnInterface; use time::ext::NumericalDuration; use super::EphemeralKeyInterface; + #[cfg(feature = "v2")] + use crate::types::storage::ephemeral_key::{ + EphemeralKeyType, EphemeralKeyTypeNew, ResourceType, + }; use crate::{ core::errors::{self, CustomResult}, services::Store, @@ -40,6 +78,7 @@ mod storage { #[async_trait::async_trait] impl EphemeralKeyInterface for Store { + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn create_ephemeral_key( &self, @@ -94,6 +133,68 @@ mod storage { Err(er) => Err(er).change_context(errors::StorageError::KVError), } } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn create_ephemeral_key( + &self, + new: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + let created_at = date_time::now(); + let expires = created_at.saturating_add(validity.hours()); + let id_key = new.id.generate_redis_key(); + + let created_ephemeral_key = EphemeralKeyType { + id: new.id, + created_at, + expires, + customer_id: new.customer_id, + merchant_id: new.merchant_id, + secret: new.secret, + resource_type: new.resource_type, + }; + let secret_key = created_ephemeral_key.generate_secret_key(); + + match self + .get_redis_conn() + .map_err(Into::::into)? + .serialize_and_set_multiple_hash_field_if_not_exist( + &[ + (&secret_key, &created_ephemeral_key), + (&id_key, &created_ephemeral_key), + ], + "ephkey", + None, + ) + .await + { + Ok(v) if v.contains(&HsetnxReply::KeyNotSet) => { + Err(errors::StorageError::DuplicateValue { + entity: "ephemeral key", + key: None, + } + .into()) + } + Ok(_) => { + let expire_at = expires.assume_utc().unix_timestamp(); + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&secret_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&id_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + Ok(created_ephemeral_key) + } + Err(er) => Err(er).change_context(errors::StorageError::KVError), + } + } + + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn get_ephemeral_key( &self, @@ -106,6 +207,22 @@ mod storage { .await .change_context(errors::StorageError::KVError) } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + let key = format!("epkey_{key}"); + self.get_redis_conn() + .map_err(Into::::into)? + .get_hash_field_and_deserialize(&key, "ephkey", "EphemeralKeyType") + .await + .change_context(errors::StorageError::KVError) + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -125,11 +242,45 @@ mod storage { .change_context(errors::StorageError::KVError)?; Ok(ek) } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + let ephemeral_key = self.get_ephemeral_key(id).await?; + let redis_id_key = ephemeral_key.id.generate_redis_key(); + let secret_key = ephemeral_key.generate_secret_key(); + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&redis_id_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(redis_id_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&secret_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(secret_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + Ok(ephemeral_key) + } } } #[async_trait::async_trait] impl EphemeralKeyInterface for MockDb { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -150,6 +301,17 @@ impl EphemeralKeyInterface for MockDb { ephemeral_keys.push(ephemeral_key.clone()); Ok(ephemeral_key) } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, @@ -167,6 +329,16 @@ impl EphemeralKeyInterface for MockDb { ), } } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -181,4 +353,12 @@ impl EphemeralKeyInterface for MockDb { ); } } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + todo!() + } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index f49173b35496..4155bbf3ff9a 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,8 @@ use common_utils::{ id_type, types::{keymanager::KeyManagerState, theme::ThemeLineage}, }; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew}; use diesel_models::{ enums, enums::ProcessTrackerStatus, @@ -646,6 +648,7 @@ impl DisputeInterface for KafkaStore { #[async_trait::async_trait] impl EphemeralKeyInterface for KafkaStore { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -653,18 +656,47 @@ impl EphemeralKeyInterface for KafkaStore { ) -> CustomResult { self.diesel_store.create_ephemeral_key(ek, validity).await } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, ) -> CustomResult { self.diesel_store.get_ephemeral_key(key).await } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, ) -> CustomResult { self.diesel_store.delete_ephemeral_key(id).await } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } } #[async_trait::async_trait] diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 9100ea3ebaa5..625a4d9c95b5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -90,6 +90,7 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; + pub const X_RESOURCE_TYPE: &str = "X-Resource-Type"; } pub mod pii { @@ -156,15 +157,17 @@ pub fn mk_app( } } + #[cfg(all(feature = "oltp", any(feature = "v1", feature = "v2"),))] + { + server_app = server_app.service(routes::EphemeralKey::server(state.clone())) + } #[cfg(all( feature = "oltp", any(feature = "v1", feature = "v2"), not(feature = "customer_v2") ))] { - server_app = server_app - .service(routes::EphemeralKey::server(state.clone())) - .service(routes::Poll::server(state.clone())) + server_app = server_app.service(routes::Poll::server(state.clone())) } #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a070afe09a48..0b5d481e3da3 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -27,11 +27,7 @@ use self::settings::Tenant; use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "customer_v2"), - feature = "oltp" -))] +#[cfg(all(any(feature = "v1", feature = "v2"), feature = "oltp"))] use super::ephemeral_key::*; #[cfg(any(feature = "olap", feature = "oltp"))] use super::payment_methods::*; @@ -1155,8 +1151,11 @@ impl PaymentMethods { web::resource("/{id}/update-saved-payment-method") .route(web::patch().to(payment_method_update_api)), ) - .service(web::resource("/{id}").route(web::get().to(payment_method_retrieve_api))) - .service(web::resource("/{id}").route(web::delete().to(payment_method_delete_api))); + .service( + web::resource("/{id}") + .route(web::get().to(payment_method_retrieve_api)) + .route(web::delete().to(payment_method_delete_api)), + ); route } @@ -1430,6 +1429,16 @@ impl EphemeralKey { } } +#[cfg(feature = "v2")] +impl EphemeralKey { + pub fn server(config: AppState) -> Scope { + web::scope("/v2/ephemeral-keys") + .app_data(web::Data::new(config)) + .service(web::resource("").route(web::post().to(ephemeral_key_create))) + .service(web::resource("/{id}").route(web::delete().to(ephemeral_key_delete))) + } +} + pub struct Mandates; #[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v1"))] diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index e6a33c59192e..0330482e81a9 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -1,11 +1,7 @@ -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use actix_web::{web, HttpRequest, HttpResponse}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use router_env::{instrument, tracing, Flow}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use super::AppState; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::{api_locking, payments::helpers}, services::{api, authentication as auth}, @@ -38,7 +34,35 @@ pub async fn ephemeral_key_create( .await } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] +pub async fn ephemeral_key_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::EphemeralKeyCreate; + let payload = json_payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, payload, _| { + helpers::make_ephemeral_key( + state, + payload.customer_id.to_owned(), + auth.merchant_account, + auth.key_store, + req.headers(), + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + ) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] pub async fn ephemeral_key_delete( state: web::Data, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index c27a25677af2..729a35aa8b7a 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -141,8 +141,8 @@ pub async fn confirm_payment_method_intent_api( let pm_id = path.into_inner(); let payload = json_payload.into_inner(); - let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { - Ok((auth, _auth_flow)) => (auth, _auth_flow), + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, Err(e) => return api::log_and_return_error_response(e), }; @@ -150,7 +150,6 @@ pub async fn confirm_payment_method_intent_api( id: pm_id.clone(), payment_method_type: payload.payment_method_type, payment_method_subtype: payload.payment_method_subtype, - client_secret: payload.client_secret.clone(), customer_id: payload.customer_id.to_owned(), payment_method_data: payload.payment_method_data.clone(), }; @@ -191,6 +190,11 @@ pub async fn payment_method_update_api( let payment_method_id = path.into_inner(); let payload = json_payload.into_inner(); + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, + Err(e) => return api::log_and_return_error_response(e), + }; + Box::pin(api::server_wrap( flow, state, @@ -205,7 +209,7 @@ pub async fn payment_method_update_api( auth.key_store, ) }, - &auth::HeaderAuth(auth::ApiKeyAuth), + &*auth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 88608316640a..e5412c20fe29 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -13,7 +13,11 @@ use api_models::payouts; use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_enums::TokenPurpose; +#[cfg(feature = "v2")] +use common_utils::fp_utils; use common_utils::{date_time, id_type}; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key; use error_stack::{report, ResultExt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::PeekInterface; @@ -1340,6 +1344,7 @@ where #[derive(Debug)] pub struct EphemeralKeyAuth; +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -1363,6 +1368,45 @@ where .await } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for EphemeralKeyAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let api_key = + get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; + let ephemeral_key = state + .store() + .get_ephemeral_key(api_key) + .await + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + let resource_type = HeaderMapStruct::new(request_headers) + .get_mandatory_header_value_by_key(headers::X_RESOURCE_TYPE) + .and_then(|val| { + ephemeral_key::ResourceType::from_str(val).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + }, + ) + })?; + + fp_utils::when(resource_type != ephemeral_key.resource_type, || { + Err(errors::ApiErrorResponse::Unauthorized) + })?; + + MerchantIdAuth(ephemeral_key.merchant_id) + .authenticate_and_fetch(request_headers, state) + .await + } +} #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); @@ -2949,13 +2993,6 @@ impl ClientSecretFetch for PaymentMethodCreate { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl ClientSecretFetch for PaymentMethodIntentConfirm { - fn get_client_secret(&self) -> Option<&String> { - Some(&self.client_secret) - } -} - impl ClientSecretFetch for api_models::cards_info::CardsInfoRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -2980,6 +3017,7 @@ impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { } } +#[cfg(feature = "v1")] impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -3070,6 +3108,20 @@ where } } +pub fn is_ephemeral_or_publishible_auth( + headers: &HeaderMap, +) -> RouterResult>> { + let api_key = get_api_key(headers)?; + + if api_key.starts_with("epk") { + Ok(Box::new(EphemeralKeyAuth)) + } else if api_key.starts_with("pk_") { + Ok(Box::new(HeaderAuth(PublishableKeyAuth))) + } else { + Ok(Box::new(HeaderAuth(ApiKeyAuth))) + } +} + pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 1a6b9053dcb9..c40d6fedfe73 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -126,16 +126,6 @@ impl VaultingDataInterface for PaymentMethodVaultingData { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub struct PaymentMethodClientSecret; - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl PaymentMethodClientSecret { - pub fn generate(payment_method_id: &common_utils::id_type::GlobalPaymentMethodId) -> String { - todo!() - } -} - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub struct SavedPMLPaymentsInfo { pub payment_intent: storage::PaymentIntent, @@ -155,7 +145,7 @@ pub struct VaultRetrieveRequest { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct VaultRetrieveResponse { - pub data: Secret, + pub data: PaymentMethodVaultingData, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/types/storage/ephemeral_key.rs b/crates/router/src/types/storage/ephemeral_key.rs index 9927e16b3fad..c4b8e2ba701a 100644 --- a/crates/router/src/types/storage/ephemeral_key.rs +++ b/crates/router/src/types/storage/ephemeral_key.rs @@ -1 +1,18 @@ pub use diesel_models::ephemeral_key::{EphemeralKey, EphemeralKeyNew}; +#[cfg(feature = "v2")] +pub use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; + +#[cfg(feature = "v2")] +use crate::types::transformers::ForeignFrom; +#[cfg(feature = "v2")] +impl ForeignFrom for api_models::ephemeral_key::EphemeralKeyResponse { + fn foreign_from(from: EphemeralKeyType) -> Self { + Self { + customer_id: from.customer_id, + created_at: from.created_at, + expires: from.expires, + secret: from.secret, + id: from.id, + } + } +}