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 {
fn from(value: ApiErrorResponse) -> Self {
match value {
ApiErrorResponse::Unauthorized => ErrorCode::Unauthorized,
ApiErrorResponse::Unauthorized | ApiErrorResponse::InvalidEphermeralKey => {
ErrorCode::Unauthorized
}
ApiErrorResponse::InvalidRequestUrl | ApiErrorResponse::InvalidHttpMethod => {
ErrorCode::InvalidRequestUrl
}

View File

@ -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
))

View File

@ -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.

View File

@ -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),

View File

@ -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)?
))
}

View File

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

View File

@ -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")]

View File

@ -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 { .. }

View File

@ -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,

View File

@ -154,12 +154,15 @@ async fn payment_response_ut<F: Clone, T>(
storage::PaymentAttemptUpdate::ResponseUpdate {
status: router_data.status,
connector_transaction_id: Some(
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_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(),
};

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> {
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,
})
}

View File

@ -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,

View File

@ -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")]

View File

@ -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")]

View File

@ -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 {

View File

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

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},
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")

View File

@ -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>,
}

View File

@ -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,
}

View File

@ -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.