customers: Added ephemeral key authentication for customer retrieve (#89)

This commit is contained in:
Kartikeya Hegde
2022-12-09 17:58:04 +05:30
committed by GitHub
parent 2aef3bccfb
commit 3d7d89172c
21 changed files with 214 additions and 41 deletions

View File

@ -303,7 +303,9 @@ pub(crate) enum StripeErrorType {
impl From<ApiErrorResponse> for ErrorCode { impl From<ApiErrorResponse> for ErrorCode {
fn from(value: ApiErrorResponse) -> Self { fn from(value: ApiErrorResponse) -> Self {
match value { match value {
ApiErrorResponse::Unauthorized => ErrorCode::Unauthorized, ApiErrorResponse::Unauthorized | ApiErrorResponse::InvalidEphermeralKey => {
ErrorCode::Unauthorized
}
ApiErrorResponse::InvalidRequestUrl | ApiErrorResponse::InvalidHttpMethod => { ApiErrorResponse::InvalidRequestUrl | ApiErrorResponse::InvalidHttpMethod => {
ErrorCode::InvalidRequestUrl ErrorCode::InvalidRequestUrl
} }

View File

@ -109,7 +109,10 @@ impl
"{}{}{}{}{}", "{}{}{}{}{}",
self.base_url(connectors), self.base_url(connectors),
"v1/payments/", "v1/payments/",
req.request.connector_transaction_id, req.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
"?entityId=", "?entityId=",
auth.entity_id auth.entity_id
)) ))

View File

@ -130,7 +130,7 @@ pub struct AdyenRedirectionAction {
method: String, method: String,
#[serde(rename = "type")] #[serde(rename = "type")]
type_of_response: String, type_of_response: String,
data: HashMap<String, String>, data: Option<HashMap<String, String>>,
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
@ -491,12 +491,21 @@ pub fn get_redirection_response(
.change_context(errors::ParsingError) .change_context(errors::ParsingError)
.attach_printable("Failed to parse redirection url")?; .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 { let redirection_data = services::RedirectForm {
url: redirection_url_response.to_string(), url: redirection_url_response.to_string(),
method: services::Method::from_str(&response.action.method) method: services::Method::from_str(&response.action.method)
.into_report() .into_report()
.change_context(errors::ParsingError)?, .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. // We don't get connector transaction id for redirections in Adyen.

View File

@ -108,7 +108,11 @@ impl
) -> CustomResult<String, errors::ConnectorError> { ) -> CustomResult<String, errors::ConnectorError> {
let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type) let auth_type = braintree::BraintreeAuthType::try_from(&req.connector_auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?; .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!( Ok(format!(
"{}/merchants/{}/transactions/{}", "{}/merchants/{}/transactions/{}",
self.base_url(connectors), self.base_url(connectors),

View File

@ -107,7 +107,10 @@ impl
"{}{}{}", "{}{}{}",
self.base_url(connectors), self.base_url(connectors),
"payments/", "payments/",
req.request.connector_transaction_id req.request
.connector_transaction_id
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
)) ))
} }

View File

@ -202,7 +202,8 @@ impl
"{}{}/{}", "{}{}/{}",
self.base_url(connectors), self.base_url(connectors),
"v1/payment_intents", "v1/payment_intents",
id id.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
)) ))
} }

View File

@ -286,6 +286,8 @@ pub enum ConnectorError {
FailedToObtainAuthType, FailedToObtainAuthType,
#[error("This step has not been implemented for: {0}")] #[error("This step has not been implemented for: {0}")]
NotImplemented(String), NotImplemented(String),
#[error("Missing connector transaction ID")]
MissingConnectorTransactionID,
#[error("Webhooks not implemented for this connector")] #[error("Webhooks not implemented for this connector")]
WebhooksNotImplemented, WebhooksNotImplemented,
#[error("Failed to decode webhook event body")] #[error("Failed to decode webhook event body")]

View File

@ -28,6 +28,8 @@ pub enum ApiErrorResponse {
the Dashboard Settings section." the Dashboard Settings section."
)] )]
BadCredentials, 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.")] #[error(error_type = ErrorType::InvalidRequestError, code = "IR_03", message = "Unrecognized request URL.")]
InvalidRequestUrl, InvalidRequestUrl,
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_04", message = "The HTTP method is not applicable for this API.")] #[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; use reqwest::StatusCode;
match self { match self {
ApiErrorResponse::Unauthorized | ApiErrorResponse::BadCredentials => { ApiErrorResponse::Unauthorized
StatusCode::UNAUTHORIZED | ApiErrorResponse::BadCredentials
} // 401 | ApiErrorResponse::InvalidEphermeralKey => StatusCode::UNAUTHORIZED, // 401
ApiErrorResponse::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404 ApiErrorResponse::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
ApiErrorResponse::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405 ApiErrorResponse::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
ApiErrorResponse::MissingRequiredField { .. } ApiErrorResponse::MissingRequiredField { .. }

View File

@ -12,6 +12,7 @@ use super::{
}; };
use crate::{ use crate::{
configs::settings::Server, configs::settings::Server,
consts,
core::{ core::{
errors::{self, CustomResult, RouterResult, StorageErrorExt}, errors::{self, CustomResult, RouterResult, StorageErrorExt},
payment_methods::cards, payment_methods::cards,
@ -21,7 +22,7 @@ use crate::{
services, services,
types::{ types::{
api::{self, enums as api_enums}, api::{self, enums as api_enums},
storage::{self, enums as storage_enums}, storage::{self, enums as storage_enums, ephemeral_key},
}, },
utils::{ utils::{
self, 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>( pub async fn make_pm_data<'a, F: Clone, R>(
operation: BoxedOperation<'a, F, R>, operation: BoxedOperation<'a, F, R>,
state: &'a AppState, state: &'a AppState,
payment_method: Option<storage_enums::PaymentMethodType>, payment_method_type: Option<storage_enums::PaymentMethodType>,
txn_id: &str, txn_id: &str,
_payment_attempt: &storage::PaymentAttempt, _payment_attempt: &storage::PaymentAttempt,
request: &Option<api::PaymentMethod>, 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>)> { ) -> RouterResult<(BoxedOperation<'a, F, R>, Option<api::PaymentMethod>)> {
let payment_method = match (request, token) { let payment_method = match (request, token) {
(_, Some(token)) => Ok::<_, error_stack::Report<errors::ApiErrorResponse>>( (_, 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 // TODO: Handle token expiry
Vault::get_payment_method_data_from_locker(state, token).await? Vault::get_payment_method_data_from_locker(state, token).await?
} else { } else {
@ -598,7 +599,13 @@ pub async fn make_pm_data<'a, F: Clone, R>(
let payment_method = match payment_method { let payment_method = match payment_method {
Some(pm) => Some(pm), 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)) Ok((operation, payment_method))
@ -897,6 +904,40 @@ pub fn make_merchant_url_with_response(
Ok(merchant_url_with_response.to_string()) 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( pub fn make_pg_redirect_response(
payment_id: String, payment_id: String,
response: &api::PaymentsResponse, response: &api::PaymentsResponse,

View File

@ -154,12 +154,15 @@ async fn payment_response_ut<F: Clone, T>(
storage::PaymentAttemptUpdate::ResponseUpdate { storage::PaymentAttemptUpdate::ResponseUpdate {
status: router_data.status, status: router_data.status,
connector_transaction_id: Some( connector_transaction_id: match response.resource_id {
response types::ResponseId::NoResponseId => None,
.resource_id _ => Some(
.get_connector_transaction_id() response
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, .resource_id
), .get_connector_transaction_id()
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?,
),
},
authentication_type: None, authentication_type: None,
payment_method_id: Some(router_data.payment_method_id), payment_method_id: Some(router_data.payment_method_id),
redirect: Some(response.redirect), redirect: Some(response.redirect),
@ -187,12 +190,15 @@ async fn payment_response_ut<F: Clone, T>(
.attach_printable("Could not parse the connector response")?; .attach_printable("Could not parse the connector response")?;
let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate { let connector_response_update = storage::ConnectorResponseUpdate::ResponseUpdate {
connector_transaction_id: Some( connector_transaction_id: match connector_response.resource_id {
connector_response types::ResponseId::NoResponseId => None,
.resource_id _ => Some(
.get_connector_transaction_id() connector_response
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?, .resource_id
), .get_connector_transaction_id()
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?,
),
},
authentication_data, authentication_data,
encoded_data: payment_data.connector_response.encoded_data.clone(), encoded_data: payment_data.connector_response.encoded_data.clone(),
}; };

View File

@ -376,10 +376,12 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsSyncData {
fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> { fn try_from(payment_data: PaymentData<F>) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
connector_transaction_id: payment_data connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id {
.payment_attempt Some(connector_txn_id) => {
.connector_transaction_id types::ResponseId::ConnectorTransactionId(connector_txn_id)
.ok_or(errors::ApiErrorResponse::SuccessfulPaymentNotFound)?, }
None => types::ResponseId::NoResponseId,
},
encoded_data: payment_data.connector_response.encoded_data, encoded_data: payment_data.connector_response.encoded_data,
}) })
} }

View File

@ -49,8 +49,8 @@ mod storage {
let expires = created_at.saturating_add(validity.hours()); let expires = created_at.saturating_add(validity.hours());
let created_ek = EphemeralKey { let created_ek = EphemeralKey {
id: new.id, id: new.id,
created_at, created_at: created_at.assume_utc().unix_timestamp(),
expires, expires: expires.assume_utc().unix_timestamp(),
customer_id: new.customer_id, customer_id: new.customer_id,
merchant_id: new.merchant_id, merchant_id: new.merchant_id,
secret: new.secret, secret: new.secret,

View File

@ -105,6 +105,7 @@ pub fn mk_app(
.service(routes::PaymentMethods::server(state.clone())) .service(routes::PaymentMethods::server(state.clone()))
.service(routes::MerchantAccount::server(state.clone())) .service(routes::MerchantAccount::server(state.clone()))
.service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone()))
.service(routes::EphemeralKey::server(state.clone()))
.service(routes::Webhooks::server(state.clone())); .service(routes::Webhooks::server(state.clone()));
#[cfg(feature = "stripe")] #[cfg(feature = "stripe")]

View File

@ -1,6 +1,7 @@
mod admin; mod admin;
mod app; mod app;
mod customers; mod customers;
mod ephemeral_key;
mod health; mod health;
mod mandates; mod mandates;
mod metrics; mod metrics;
@ -11,7 +12,7 @@ mod refunds;
mod webhooks; mod webhooks;
pub use self::app::{ pub use self::app::{
AppState, Customers, Health, Mandates, MerchantAccount, MerchantConnectorAccount, AppState, Customers, EphemeralKey, Health, Mandates, MerchantAccount, MerchantConnectorAccount,
PaymentMethods, Payments, Payouts, Refunds, Webhooks, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
}; };
#[cfg(feature = "stripe")] #[cfg(feature = "stripe")]

View File

@ -1,8 +1,8 @@
use actix_web::{web, Scope}; use actix_web::{web, Scope};
use super::{ use super::{
admin::*, customers::*, health::*, mandates::*, payment_methods::*, payments::*, payouts::*, admin::*, customers::*, ephemeral_key::*, health::*, mandates::*, payment_methods::*,
refunds::*, webhooks::*, payments::*, payouts::*, refunds::*, webhooks::*,
}; };
use crate::{ use crate::{
configs::settings::Settings, 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; pub struct Mandates;
impl Mandates { impl Mandates {

View File

@ -5,7 +5,11 @@ use router_env::{
}; };
use super::app::AppState; 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))] #[instrument(skip_all, fields(flow = ?Flow::CustomersCreate))]
// #[post("")] // #[post("")]
@ -35,12 +39,22 @@ pub async fn customers_retrieve(
customer_id: path.into_inner(), customer_id: path.into_inner(),
}) })
.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( api::server_wrap(
&state, &state,
&req, &req,
payload, payload,
|state, merchant_account, req| retrieve_customer(&*state.store, merchant_account, req), |state, merchant_account, req| retrieve_customer(&*state.store, merchant_account, req),
api::MerchantAuthentication::ApiKey, auth_type,
) )
.await .await
} }

View 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
}

View File

@ -31,7 +31,7 @@ use crate::{
storage::{self, enums}, storage::{self, enums},
ErrorResponse, Response, ErrorResponse, Response,
}, },
utils::OptionExt, utils::{self, OptionExt},
}; };
pub type BoxedConnectorIntegration<'a, T, Req, Resp> = 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> { fn get_api_key(req: &HttpRequest) -> RouterResult<&str> {
req.headers() req.headers()
.get("api-key") .get("api-key")

View File

@ -104,7 +104,7 @@ pub struct PaymentsCaptureData {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PaymentsSyncData { pub struct PaymentsSyncData {
//TODO : add fields based on the connector requirements //TODO : add fields based on the connector requirements
pub connector_transaction_id: String, pub connector_transaction_id: ResponseId,
pub encoded_data: Option<String>, pub encoded_data: Option<String>,
} }

View File

@ -10,7 +10,7 @@ pub struct EphemeralKey {
pub id: String, pub id: String,
pub merchant_id: String, pub merchant_id: String,
pub customer_id: String, pub customer_id: String,
pub created_at: time::PrimitiveDateTime, pub created_at: i64,
pub expires: time::PrimitiveDateTime, pub expires: i64,
pub secret: String, pub secret: String,
} }

View File

@ -80,6 +80,10 @@ pub enum Flow {
CustomersDelete, CustomersDelete,
/// Customers get mandates flow. /// Customers get mandates flow.
CustomersGetMandates, CustomersGetMandates,
/// Create an Ephemeral Key.
EphemeralKeyCreate,
/// Delete an Ephemeral Key.
EphemeralKeyDelete,
/// Mandates retrieve flow. /// Mandates retrieve flow.
MandatesRetrieve, MandatesRetrieve,
/// Mandates revoke flow. /// Mandates revoke flow.