diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index f0dbe28192..22dbcb1af1 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -303,7 +303,9 @@ pub(crate) enum StripeErrorType { impl From for ErrorCode { fn from(value: ApiErrorResponse) -> Self { match value { - ApiErrorResponse::Unauthorized => ErrorCode::Unauthorized, + ApiErrorResponse::Unauthorized | ApiErrorResponse::InvalidEphermeralKey => { + ErrorCode::Unauthorized + } ApiErrorResponse::InvalidRequestUrl | ApiErrorResponse::InvalidHttpMethod => { ErrorCode::InvalidRequestUrl } diff --git a/crates/router/src/connector/aci.rs b/crates/router/src/connector/aci.rs index 72f9a0df01..a8567cf2ff 100644 --- a/crates/router/src/connector/aci.rs +++ b/crates/router/src/connector/aci.rs @@ -109,7 +109,10 @@ impl "{}{}{}{}{}", self.base_url(connectors), "v1/payments/", - req.request.connector_transaction_id, + req.request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?, "?entityId=", auth.entity_id )) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 7cdf63ab74..cf8c71da1a 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -130,7 +130,7 @@ pub struct AdyenRedirectionAction { method: String, #[serde(rename = "type")] type_of_response: String, - data: HashMap, + data: Option>, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -491,12 +491,21 @@ pub fn get_redirection_response( .change_context(errors::ParsingError) .attach_printable("Failed to parse redirection url")?; + let form_field_for_redirection = match response.action.data { + Some(data) => data, + None => std::collections::HashMap::from_iter( + redirection_url_response + .query_pairs() + .map(|(k, v)| (k.to_string(), v.to_string())), + ), + }; + let redirection_data = services::RedirectForm { url: redirection_url_response.to_string(), method: services::Method::from_str(&response.action.method) .into_report() .change_context(errors::ParsingError)?, - form_fields: response.action.data, + form_fields: form_field_for_redirection, }; // We don't get connector transaction id for redirections in Adyen. diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 798d29b84b..8d591700aa 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -108,7 +108,11 @@ impl ) -> CustomResult { let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - let connector_payment_id = req.request.connector_transaction_id.clone(); + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; Ok(format!( "{}/merchants/{}/transactions/{}", self.base_url(connectors), diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index 64d20f9659..f1df92b941 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -107,7 +107,10 @@ impl "{}{}{}", self.base_url(connectors), "payments/", - req.request.connector_transaction_id + req.request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? )) } diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 7109fb153f..b682933a09 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -202,7 +202,8 @@ impl "{}{}/{}", self.base_url(connectors), "v1/payment_intents", - id + id.get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)? )) } diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 6aa621d406..96e44e5c63 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -286,6 +286,8 @@ pub enum ConnectorError { FailedToObtainAuthType, #[error("This step has not been implemented for: {0}")] NotImplemented(String), + #[error("Missing connector transaction ID")] + MissingConnectorTransactionID, #[error("Webhooks not implemented for this connector")] WebhooksNotImplemented, #[error("Failed to decode webhook event body")] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 9a2377ae42..c79dcbe73a 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -28,6 +28,8 @@ pub enum ApiErrorResponse { the Dashboard Settings section." )] BadCredentials, + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_10", message = "Invalid Ephemeral Key for the customer")] + InvalidEphermeralKey, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_03", message = "Unrecognized request URL.")] InvalidRequestUrl, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_04", message = "The HTTP method is not applicable for this API.")] @@ -137,9 +139,9 @@ impl actix_web::ResponseError for ApiErrorResponse { use reqwest::StatusCode; match self { - ApiErrorResponse::Unauthorized | ApiErrorResponse::BadCredentials => { - StatusCode::UNAUTHORIZED - } // 401 + ApiErrorResponse::Unauthorized + | ApiErrorResponse::BadCredentials + | ApiErrorResponse::InvalidEphermeralKey => StatusCode::UNAUTHORIZED, // 401 ApiErrorResponse::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404 ApiErrorResponse::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405 ApiErrorResponse::MissingRequiredField { .. } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index beccb70bfa..97d802b2e1 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -12,6 +12,7 @@ use super::{ }; use crate::{ configs::settings::Server, + consts, core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::cards, @@ -21,7 +22,7 @@ use crate::{ services, types::{ api::{self, enums as api_enums}, - storage::{self, enums as storage_enums}, + storage::{self, enums as storage_enums, ephemeral_key}, }, utils::{ self, @@ -571,7 +572,7 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R>( pub async fn make_pm_data<'a, F: Clone, R>( operation: BoxedOperation<'a, F, R>, state: &'a AppState, - payment_method: Option, + payment_method_type: Option, txn_id: &str, _payment_attempt: &storage::PaymentAttempt, request: &Option, @@ -579,7 +580,7 @@ pub async fn make_pm_data<'a, F: Clone, R>( ) -> RouterResult<(BoxedOperation<'a, F, R>, Option)> { let payment_method = match (request, token) { (_, Some(token)) => Ok::<_, error_stack::Report>( - if payment_method == Some(storage_enums::PaymentMethodType::Card) { + if payment_method_type == Some(storage_enums::PaymentMethodType::Card) { // TODO: Handle token expiry Vault::get_payment_method_data_from_locker(state, token).await? } else { @@ -598,7 +599,13 @@ pub async fn make_pm_data<'a, F: Clone, R>( let payment_method = match payment_method { Some(pm) => Some(pm), - None => Vault::get_payment_method_data_from_locker(state, txn_id).await?, + None => { + if payment_method_type == Some(storage_enums::PaymentMethodType::Card) { + Vault::get_payment_method_data_from_locker(state, txn_id).await? + } else { + None + } + } }; Ok((operation, payment_method)) @@ -897,6 +904,40 @@ pub fn make_merchant_url_with_response( Ok(merchant_url_with_response.to_string()) } +pub async fn make_ephemeral_key( + state: &AppState, + customer_id: String, + merchant_id: String, +) -> errors::RouterResponse { + let store = &state.store; + let id = utils::generate_id(consts::ID_LENGTH, "eki"); + let secret = format!("epk_{}", &Uuid::new_v4().simple().to_string()); + let ek = ephemeral_key::EphemeralKeyNew { + id, + customer_id, + merchant_id, + secret, + }; + let ek = store + .create_ephemeral_key(ek, state.conf.eph_key.validity) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + Ok(services::BachResponse::Json(ek)) +} + +pub async fn delete_ephemeral_key( + store: &dyn StorageInterface, + ek_id: String, +) -> errors::RouterResponse { + let ek = store + .delete_ephemeral_key(&ek_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to delete ephemeral key")?; + Ok(services::BachResponse::Json(ek)) +} + pub fn make_pg_redirect_response( payment_id: String, response: &api::PaymentsResponse, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index e14e13e9e3..bb4535ffe7 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -154,12 +154,15 @@ async fn payment_response_ut( storage::PaymentAttemptUpdate::ResponseUpdate { status: router_data.status, - connector_transaction_id: Some( - response - .resource_id - .get_connector_transaction_id() - .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, - ), + connector_transaction_id: match response.resource_id { + types::ResponseId::NoResponseId => None, + _ => Some( + response + .resource_id + .get_connector_transaction_id() + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, + ), + }, authentication_type: None, payment_method_id: Some(router_data.payment_method_id), redirect: Some(response.redirect), @@ -187,12 +190,15 @@ async fn payment_response_ut( .attach_printable("Could not parse the connector response")?; let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate { - connector_transaction_id: Some( - connector_response - .resource_id - .get_connector_transaction_id() - .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, - ), + connector_transaction_id: match connector_response.resource_id { + types::ResponseId::NoResponseId => None, + _ => Some( + connector_response + .resource_id + .get_connector_transaction_id() + .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, + ), + }, authentication_data, encoded_data: payment_data.connector_response.encoded_data.clone(), }; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index da9c686301..df63eb59e5 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -376,10 +376,12 @@ impl TryFrom> for types::PaymentsSyncData { fn try_from(payment_data: PaymentData) -> Result { Ok(Self { - connector_transaction_id: payment_data - .payment_attempt - .connector_transaction_id - .ok_or(errors::ApiErrorResponse::SuccessfulPaymentNotFound)?, + connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id { + Some(connector_txn_id) => { + types::ResponseId::ConnectorTransactionId(connector_txn_id) + } + None => types::ResponseId::NoResponseId, + }, encoded_data: payment_data.connector_response.encoded_data, }) } diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index b1db0c0d5b..81a99a16d0 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -49,8 +49,8 @@ mod storage { let expires = created_at.saturating_add(validity.hours()); let created_ek = EphemeralKey { id: new.id, - created_at, - expires, + created_at: created_at.assume_utc().unix_timestamp(), + expires: expires.assume_utc().unix_timestamp(), customer_id: new.customer_id, merchant_id: new.merchant_id, secret: new.secret, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index c6553349f9..c918af20f9 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -105,6 +105,7 @@ pub fn mk_app( .service(routes::PaymentMethods::server(state.clone())) .service(routes::MerchantAccount::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) + .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())); #[cfg(feature = "stripe")] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ffc5bdcf6f..854a949044 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,6 +1,7 @@ mod admin; mod app; mod customers; +mod ephemeral_key; mod health; mod mandates; mod metrics; @@ -11,7 +12,7 @@ mod refunds; mod webhooks; pub use self::app::{ - AppState, Customers, Health, Mandates, MerchantAccount, MerchantConnectorAccount, + AppState, Customers, EphemeralKey, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks, }; #[cfg(feature = "stripe")] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 554c8e1ce5..8310f428f4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1,8 +1,8 @@ use actix_web::{web, Scope}; use super::{ - admin::*, customers::*, health::*, mandates::*, payment_methods::*, payments::*, payouts::*, - refunds::*, webhooks::*, + admin::*, customers::*, ephemeral_key::*, health::*, mandates::*, payment_methods::*, + payments::*, payouts::*, refunds::*, webhooks::*, }; use crate::{ configs::settings::Settings, @@ -189,6 +189,17 @@ impl MerchantConnectorAccount { } } +pub struct EphemeralKey; + +impl EphemeralKey { + pub fn server(config: AppState) -> Scope { + web::scope("/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; impl Mandates { diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index ecd7a298ad..b60cb00712 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -5,7 +5,11 @@ use router_env::{ }; use super::app::AppState; -use crate::{core::customers::*, services::api, types::api::customers}; +use crate::{ + core::customers::*, + services::{self, api}, + types::api::customers, +}; #[instrument(skip_all, fields(flow = ?Flow::CustomersCreate))] // #[post("")] @@ -35,12 +39,22 @@ pub async fn customers_retrieve( customer_id: path.into_inner(), }) .into_inner(); + let auth_type = match services::authenticate_eph_key( + &req, + &*state.store, + payload.customer_id.clone(), + ) + .await + { + Ok(auth_type) => auth_type, + Err(err) => return api::log_and_return_error_response(err), + }; api::server_wrap( &state, &req, payload, |state, merchant_account, req| retrieve_customer(&*state.store, merchant_account, req), - api::MerchantAuthentication::ApiKey, + auth_type, ) .await } diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs new file mode 100644 index 0000000000..30a580a0f5 --- /dev/null +++ b/crates/router/src/routes/ephemeral_key.rs @@ -0,0 +1,44 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use router_env::{ + tracing::{self, instrument}, + Flow, +}; + +use super::AppState; +use crate::{core::payments::helpers, services::api, types::api::customers}; + +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] +pub async fn ephemeral_key_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let payload = json_payload.into_inner(); + api::server_wrap( + &state, + &req, + payload, + |state, merchant_account, req| { + helpers::make_ephemeral_key(state, req.customer_id, merchant_account.merchant_id) + }, + api::MerchantAuthentication::ApiKey, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] +pub async fn ephemeral_key_delete( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let payload = path.into_inner(); + api::server_wrap( + &state, + &req, + payload, + |state, _, req| helpers::delete_ephemeral_key(&*state.store, req), + api::MerchantAuthentication::ApiKey, + ) + .await +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index d94bd3bacf..1ef2da29c3 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -31,7 +31,7 @@ use crate::{ storage::{self, enums}, ErrorResponse, Response, }, - utils::OptionExt, + utils::{self, OptionExt}, }; pub type BoxedConnectorIntegration<'a, T, Req, Resp> = @@ -596,6 +596,29 @@ pub(crate) fn get_auth_type_and_check_client_secret( )) } +pub(crate) async fn authenticate_eph_key<'a>( + req: &'a actix_web::HttpRequest, + store: &dyn StorageInterface, + customer_id: String, +) -> RouterResult> { + let api_key = get_api_key(req)?; + if api_key.starts_with("epk") { + let ek = store + .get_ephemeral_key(api_key) + .await + .change_context(errors::ApiErrorResponse::BadCredentials)?; + utils::when( + ek.customer_id.ne(&customer_id), + Err(report!(errors::ApiErrorResponse::InvalidEphermeralKey)), + )?; + Ok(MerchantAuthentication::MerchantId(Cow::Owned( + ek.merchant_id, + ))) + } else { + Ok(MerchantAuthentication::ApiKey) + } +} + fn get_api_key(req: &HttpRequest) -> RouterResult<&str> { req.headers() .get("api-key") diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index be1abcaf8a..0c904a16bd 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -104,7 +104,7 @@ pub struct PaymentsCaptureData { #[derive(Debug, Clone)] pub struct PaymentsSyncData { //TODO : add fields based on the connector requirements - pub connector_transaction_id: String, + pub connector_transaction_id: ResponseId, pub encoded_data: Option, } diff --git a/crates/router/src/types/storage/ephemeral_key.rs b/crates/router/src/types/storage/ephemeral_key.rs index d3afbbdcfa..44226d50fd 100644 --- a/crates/router/src/types/storage/ephemeral_key.rs +++ b/crates/router/src/types/storage/ephemeral_key.rs @@ -10,7 +10,7 @@ pub struct EphemeralKey { pub id: String, pub merchant_id: String, pub customer_id: String, - pub created_at: time::PrimitiveDateTime, - pub expires: time::PrimitiveDateTime, + pub created_at: i64, + pub expires: i64, pub secret: String, } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 29b3e7b722..bc9f52a686 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -80,6 +80,10 @@ pub enum Flow { CustomersDelete, /// Customers get mandates flow. CustomersGetMandates, + /// Create an Ephemeral Key. + EphemeralKeyCreate, + /// Delete an Ephemeral Key. + EphemeralKeyDelete, /// Mandates retrieve flow. MandatesRetrieve, /// Mandates revoke flow.