mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
customers: Added ephemeral key authentication for customer retrieve (#89)
This commit is contained in:
@ -303,7 +303,9 @@ pub(crate) enum StripeErrorType {
|
||||
impl From<ApiErrorResponse> 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
|
||||
}
|
||||
|
||||
@ -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
|
||||
))
|
||||
|
||||
@ -130,7 +130,7 @@ pub struct AdyenRedirectionAction {
|
||||
method: String,
|
||||
#[serde(rename = "type")]
|
||||
type_of_response: String,
|
||||
data: HashMap<String, String>,
|
||||
data: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
||||
@ -108,7 +108,11 @@ impl
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
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),
|
||||
|
||||
@ -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)?
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -202,7 +202,8 @@ impl
|
||||
"{}{}/{}",
|
||||
self.base_url(connectors),
|
||||
"v1/payment_intents",
|
||||
id
|
||||
id.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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 { .. }
|
||||
|
||||
@ -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<storage_enums::PaymentMethodType>,
|
||||
payment_method_type: Option<storage_enums::PaymentMethodType>,
|
||||
txn_id: &str,
|
||||
_payment_attempt: &storage::PaymentAttempt,
|
||||
request: &Option<api::PaymentMethod>,
|
||||
@ -579,7 +580,7 @@ pub async fn make_pm_data<'a, F: Clone, R>(
|
||||
) -> RouterResult<(BoxedOperation<'a, F, R>, Option<api::PaymentMethod>)> {
|
||||
let payment_method = match (request, token) {
|
||||
(_, Some(token)) => Ok::<_, error_stack::Report<errors::ApiErrorResponse>>(
|
||||
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<ephemeral_key::EphemeralKey> {
|
||||
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<ephemeral_key::EphemeralKey> {
|
||||
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,
|
||||
|
||||
@ -154,12 +154,15 @@ async fn payment_response_ut<F: Clone, T>(
|
||||
|
||||
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<F: Clone, T>(
|
||||
.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(),
|
||||
};
|
||||
|
||||
@ -376,10 +376,12 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSyncData {
|
||||
|
||||
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
44
crates/router/src/routes/ephemeral_key.rs
Normal file
44
crates/router/src/routes/ephemeral_key.rs
Normal file
@ -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<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<customers::CustomerId>,
|
||||
) -> 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<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
) -> 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
|
||||
}
|
||||
@ -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<MerchantAuthentication<'a>> {
|
||||
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")
|
||||
|
||||
@ -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<String>,
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user