diff --git a/config/development.toml b/config/development.toml index e1b180d300..b380135e19 100644 --- a/config/development.toml +++ b/config/development.toml @@ -174,6 +174,10 @@ validity = 1 [api_keys] hash_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +checksum_auth_context = "TEST" +checksum_auth_key = "54455354" + + [connectors] aci.base_url = "https://eu-test.oppwa.com/" adyen.base_url = "https://checkout-test.adyen.com/" diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index 779781a7d8..8b8707d9b9 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -238,6 +238,43 @@ impl VerifySignature for HmacSha512 { } } +/// +/// Blake3 +#[derive(Debug)] +pub struct Blake3(String); + +impl Blake3 { + /// Create a new instance of Blake3 with a key + pub fn new(key: impl Into) -> Self { + Self(key.into()) + } +} + +impl SignMessage for Blake3 { + fn sign_message( + &self, + secret: &[u8], + msg: &[u8], + ) -> CustomResult, errors::CryptoError> { + let key = blake3::derive_key(&self.0, secret); + let output = blake3::keyed_hash(&key, msg).as_bytes().to_vec(); + Ok(output) + } +} + +impl VerifySignature for Blake3 { + fn verify_signature( + &self, + secret: &[u8], + signature: &[u8], + msg: &[u8], + ) -> CustomResult { + let key = blake3::derive_key(&self.0, secret); + let output = blake3::keyed_hash(&key, msg); + Ok(output.as_bytes() == signature) + } +} + /// Represents the GCM-AES-256 algorithm #[derive(Debug)] pub struct GcmAes256; diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index b2e9124688..a813829d63 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -4,7 +4,7 @@ use std::{fmt, marker::PhantomData}; -use crate::{strategy::Strategy, PeekInterface}; +use crate::{strategy::Strategy, PeekInterface, StrongSecret}; /// /// Secret thing. @@ -81,6 +81,14 @@ where { f(self.inner_secret).into() } + + /// Convert to [`StrongSecret`] + pub fn into_strong(self) -> StrongSecret + where + SecretValue: zeroize::DefaultIsZeroes, + { + StrongSecret::new(self.inner_secret) + } } impl PeekInterface diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 54cdb068ec..69a86a5ed3 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -38,6 +38,11 @@ merchant_account_v2 = ["api_models/merchant_account_v2", "diesel_models/merchant payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2"] merchant_connector_account_v2 = ["api_models/merchant_connector_account_v2", "kgraph_utils/merchant_connector_account_v2"] +# Partial Auth +# The feature reduces the overhead of the router authenticating the merchant for every request, and trusts on `x-merchant-id` header to be present in the request. +# This is named as partial-auth because the router will still try to authenticate if the `x-merchant-id` header is not present. +partial-auth = [] + [dependencies] actix-cors = "0.6.5" actix-http = "3.6.0" diff --git a/crates/router/src/compatibility/stripe/customers.rs b/crates/router/src/compatibility/stripe/customers.rs index d07f08ef40..eeb09b1104 100644 --- a/crates/router/src/compatibility/stripe/customers.rs +++ b/crates/router/src/compatibility/stripe/customers.rs @@ -53,7 +53,7 @@ pub async fn customer_create( |state, auth, req, _| { customers::create_customer(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -87,7 +87,7 @@ pub async fn customer_retrieve( |state, auth, req, _| { customers::retrieve_customer(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -132,7 +132,7 @@ pub async fn customer_update( |state, auth, req, _| { customers::update_customer(state, auth.merchant_account, req, auth.key_store) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -166,7 +166,7 @@ pub async fn customer_delete( |state, auth: auth::AuthenticationData, req, _| { customers::delete_customer(state, auth.merchant_account, req, auth.key_store) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -208,7 +208,7 @@ pub async fn list_customer_payment_method_api( None, ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index fa6b02e499..c9881647ed 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -1,4 +1,5 @@ pub mod types; + use actix_web::{web, HttpRequest, HttpResponse}; use api_models::payments as payment_types; use error_stack::report; @@ -71,7 +72,7 @@ pub async fn payment_intents_create( api_types::HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), locking_action, )) .await @@ -400,7 +401,7 @@ pub async fn payment_intents_capture( api_types::HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), locking_action, )) .await @@ -500,7 +501,7 @@ pub async fn payment_intent_list( |state, auth, req, _| { payments::list_payments(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/compatibility/stripe/refunds.rs b/crates/router/src/compatibility/stripe/refunds.rs index 52d5230cfc..071fdd839e 100644 --- a/crates/router/src/compatibility/stripe/refunds.rs +++ b/crates/router/src/compatibility/stripe/refunds.rs @@ -1,4 +1,5 @@ pub mod types; + use actix_web::{web, HttpRequest, HttpResponse}; use error_stack::report; use router_env::{instrument, tracing, Flow, Tag}; @@ -51,7 +52,7 @@ pub async fn refund_create( |state, auth, req, _| { refunds::refund_create_core(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -101,7 +102,7 @@ pub async fn refund_retrieve_with_gateway_creds( refunds::refund_retrieve_core, ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -143,7 +144,7 @@ pub async fn refund_retrieve( refunds::refund_retrieve_core, ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -175,7 +176,7 @@ pub async fn refund_update( &req, create_refund_update_req, |state, auth, req, _| refunds::refund_update_core(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index c7bde0dbcd..28933e74bf 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -1,4 +1,5 @@ pub mod types; + use actix_web::{web, HttpRequest, HttpResponse}; use api_models::payments as payment_types; use error_stack::report; @@ -72,7 +73,7 @@ pub async fn setup_intents_create( api_types::HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index c7125770ff..be29da50ae 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -8929,6 +8929,13 @@ impl Default for super::settings::ApiKeys { // Specifies the number of days before API key expiry when email reminders should be sent #[cfg(feature = "email")] expiry_reminder_days: vec![7, 3, 1], + + // Hex-encoded key used for calculating checksum for partial auth + #[cfg(feature = "partial-auth")] + checksum_auth_key: String::new().into(), + // context used for blake3 + #[cfg(feature = "partial-auth")] + checksum_auth_context: String::new().into(), } } } diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index de53f9c189..312a475b8b 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -114,10 +114,24 @@ impl SecretsHandler for settings::ApiKeys { #[cfg(feature = "email")] let expiry_reminder_days = api_keys.expiry_reminder_days.clone(); + #[cfg(feature = "partial-auth")] + let checksum_auth_context = secret_management_client + .get_secret(api_keys.checksum_auth_context.clone()) + .await?; + #[cfg(feature = "partial-auth")] + let checksum_auth_key = secret_management_client + .get_secret(api_keys.checksum_auth_key.clone()) + .await?; + Ok(value.transition_state(|_| Self { hash_key, #[cfg(feature = "email")] expiry_reminder_days, + + #[cfg(feature = "partial-auth")] + checksum_auth_key, + #[cfg(feature = "partial-auth")] + checksum_auth_context, })) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index a210b86baf..f42f2218bd 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -663,6 +663,12 @@ pub struct ApiKeys { // Specifies the number of days before API key expiry when email reminders should be sent #[cfg(feature = "email")] pub expiry_reminder_days: Vec, + + #[cfg(feature = "partial-auth")] + pub checksum_auth_context: Secret, + + #[cfg(feature = "partial-auth")] + pub checksum_auth_key: Secret, } #[derive(Debug, Deserialize, Clone, Default)] diff --git a/crates/router/src/core/metrics.rs b/crates/router/src/core/metrics.rs index a2f187eb3f..7cdfc6e6ea 100644 --- a/crates/router/src/core/metrics.rs +++ b/crates/router/src/core/metrics.rs @@ -84,5 +84,8 @@ counter_metric!( GLOBAL_METER ); +#[cfg(feature = "partial-auth")] +counter_metric!(PARTIAL_AUTH_FAILURE, GLOBAL_METER); + counter_metric!(API_KEY_REQUEST_INITIATED, GLOBAL_METER); counter_metric!(API_KEY_REQUEST_COMPLETED, GLOBAL_METER); diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index c9cb9dac90..41ff450274 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -730,7 +730,7 @@ pub async fn toggle_connector_agnostic_mit( json_payload.into_inner(), |state, _, req, _| connector_agnostic_mit_toggle(state, &merchant_id, &profile_id, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 08d960e669..8226e6fa42 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -5,6 +5,8 @@ use actix_web::{web, Scope}; use api_models::routing::RoutingRetrieveQuery; #[cfg(feature = "olap")] use common_enums::TransactionType; +#[cfg(feature = "partial-auth")] +use common_utils::crypto::Blake3; #[cfg(feature = "email")] use external_services::email::{ses::AwsSes, EmailService}; use external_services::file_storage::FileStorageInterface; @@ -56,6 +58,8 @@ use super::{ephemeral_key::*, webhooks::*}; pub use crate::analytics::opensearch::OpenSearchClient; #[cfg(feature = "olap")] use crate::analytics::AnalyticsProvider; +#[cfg(feature = "partial-auth")] +use crate::errors::RouterResult; #[cfg(all(feature = "frm", feature = "oltp"))] use crate::routes::fraud_check as frm_routes; #[cfg(all(feature = "recon", feature = "olap"))] @@ -116,6 +120,8 @@ pub trait SessionStateInfo { fn event_handler(&self) -> EventsHandler; fn get_request_id(&self) -> Option; fn add_request_id(&mut self, request_id: RequestId); + #[cfg(feature = "partial-auth")] + fn get_detached_auth(&self) -> RouterResult<(Blake3, &[u8])>; fn session_state(&self) -> SessionState; } @@ -137,6 +143,39 @@ impl SessionStateInfo for SessionState { self.store.add_request_id(request_id.to_string()); self.request_id.replace(request_id); } + + #[cfg(feature = "partial-auth")] + fn get_detached_auth(&self) -> RouterResult<(Blake3, &[u8])> { + use error_stack::ResultExt; + use hyperswitch_domain_models::errors::api_error_response as errors; + use masking::prelude::PeekInterface as _; + use router_env::logger; + + let output = CHECKSUM_KEY.get_or_try_init(|| { + let conf = self.conf(); + let context = conf + .api_keys + .get_inner() + .checksum_auth_context + .peek() + .clone(); + let key = conf.api_keys.get_inner().checksum_auth_key.peek(); + hex::decode(key).map(|key| { + ( + masking::StrongSecret::new(context), + masking::StrongSecret::new(key), + ) + }) + }); + + match output { + Ok((context, key)) => Ok((Blake3::new(context.peek().clone()), key.peek())), + Err(err) => { + logger::error!("Failed to get checksum key"); + Err(err).change_context(errors::ApiErrorResponse::InternalServerError) + } + } + } fn session_state(&self) -> SessionState { self.clone() } @@ -174,6 +213,12 @@ pub trait AppStateInfo { fn get_request_id(&self) -> Option; } +#[cfg(feature = "partial-auth")] +static CHECKSUM_KEY: once_cell::sync::OnceCell<( + masking::StrongSecret, + masking::StrongSecret>, +)> = once_cell::sync::OnceCell::new(); + impl AppStateInfo for AppState { fn conf(&self) -> settings::Settings { self.conf.as_ref().to_owned() diff --git a/crates/router/src/routes/blocklist.rs b/crates/router/src/routes/blocklist.rs index bd0ae9f6c6..f2a70a7b90 100644 --- a/crates/router/src/routes/blocklist.rs +++ b/crates/router/src/routes/blocklist.rs @@ -35,7 +35,7 @@ pub async fn add_entry_to_blocklist( blocklist::add_entry_to_blocklist(state, auth.merchant_account, body) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountWrite), req.headers(), ), @@ -71,7 +71,7 @@ pub async fn remove_entry_from_blocklist( blocklist::remove_entry_from_blocklist(state, auth.merchant_account, body) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountWrite), req.headers(), ), @@ -109,7 +109,7 @@ pub async fn list_blocked_payment_methods( blocklist::list_blocklist_entries(state, auth.merchant_account, query) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountRead), req.headers(), ), @@ -147,7 +147,7 @@ pub async fn toggle_blocklist_guard( blocklist::toggle_blocklist_guard(state, auth.merchant_account, query) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountWrite), req.headers(), ), diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs index 15684e6ae8..80b74b5c0c 100644 --- a/crates/router/src/routes/currency.rs +++ b/crates/router/src/routes/currency.rs @@ -16,7 +16,7 @@ pub async fn retrieve_forex(state: web::Data, req: HttpRequest) -> Htt (), |state, _auth: auth::AuthenticationData, _, _| currency::retrieve_forex(state), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::DashboardNoPermissionAuth, req.headers(), ), @@ -48,7 +48,7 @@ pub async fn convert_forex( ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::DashboardNoPermissionAuth, req.headers(), ), diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 0ccf6f9a77..fe9408dc13 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -26,7 +26,7 @@ pub async fn customers_create( json_payload.into_inner(), |state, auth, req, _| create_customer(state, auth.merchant_account, auth.key_store, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::CustomerWrite), req.headers(), ), @@ -88,7 +88,7 @@ pub async fn customers_list(state: web::Data, req: HttpRequest) -> Htt ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::CustomerRead), req.headers(), ), @@ -115,7 +115,7 @@ pub async fn customers_update( json_payload.into_inner(), |state, auth, req, _| update_customer(state, auth.merchant_account, req, auth.key_store), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::CustomerWrite), req.headers(), ), @@ -144,7 +144,7 @@ pub async fn customers_delete( payload, |state, auth, req, _| delete_customer(state, auth.merchant_account, req, auth.key_store), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::CustomerWrite), req.headers(), ), @@ -179,7 +179,7 @@ pub async fn get_customer_mandates( ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MandateRead), req.headers(), ), diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index 4c7199f2f2..1c2377952a 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -45,7 +45,7 @@ pub async fn retrieve_dispute( dispute_id, |state, auth, req, _| disputes::retrieve_dispute(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeRead), req.headers(), ), @@ -92,7 +92,7 @@ pub async fn retrieve_disputes_list( payload, |state, auth, req, _| disputes::retrieve_disputes_list(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeRead), req.headers(), ), @@ -134,7 +134,7 @@ pub async fn accept_dispute( disputes::accept_dispute(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeWrite), req.headers(), ), @@ -171,7 +171,7 @@ pub async fn submit_dispute_evidence( disputes::submit_evidence(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeWrite), req.headers(), ), @@ -216,7 +216,7 @@ pub async fn attach_dispute_evidence( disputes::attach_evidence(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeWrite), req.headers(), ), @@ -259,7 +259,7 @@ pub async fn retrieve_dispute_evidence( disputes::retrieve_dispute_evidence(state, auth.merchant_account, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeRead), req.headers(), ), @@ -297,7 +297,7 @@ pub async fn delete_dispute_evidence( json_payload.into_inner(), |state, auth, req, _| disputes::delete_evidence(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::DisputeWrite), req.headers(), ), diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index 93041bbd0a..893b2e5a09 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -28,7 +28,7 @@ pub async fn ephemeral_key_create( auth.merchant_account.get_id().to_owned(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, ) .await @@ -48,7 +48,7 @@ pub async fn ephemeral_key_delete( &req, payload, |state, _, req, _| helpers::delete_ephemeral_key(state, req), - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 92be12b2bc..028b00a6a6 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -46,7 +46,7 @@ pub async fn files_create( create_file_request, |state, auth, req, _| files_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::DashboardNoPermissionAuth, req.headers(), ), @@ -88,7 +88,7 @@ pub async fn files_delete( file_id, |state, auth, req, _| files_delete_core(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::DashboardNoPermissionAuth, req.headers(), ), @@ -132,7 +132,7 @@ pub async fn files_retrieve( files_retrieve_core(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::DashboardNoPermissionAuth, req.headers(), ), diff --git a/crates/router/src/routes/mandates.rs b/crates/router/src/routes/mandates.rs index c2ed58ae40..d2c5d5e464 100644 --- a/crates/router/src/routes/mandates.rs +++ b/crates/router/src/routes/mandates.rs @@ -44,7 +44,7 @@ pub async fn get_mandate( |state, auth, req, _| { mandate::get_mandate(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -85,7 +85,7 @@ pub async fn revoke_mandate( |state, auth, req, _| { mandate::revoke_mandate(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -130,7 +130,7 @@ pub async fn retrieve_mandates_list( mandate::retrieve_mandates_list(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MandateRead), req.headers(), ), diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index b1be709115..06b1037cb3 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -152,7 +152,7 @@ pub async fn payments_link_list( &req, payload, |state, auth, payload, _| list_payment_link(state, auth.merchant_account, payload), - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 05d56c8248..f55f6c80e7 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -56,7 +56,7 @@ pub async fn create_payment_method_api( )) .await }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -426,7 +426,7 @@ pub async fn payment_method_retrieve_api( |state, auth, pm, _| { cards::retrieve_payment_method(state, pm, auth.key_store, auth.merchant_account) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -515,7 +515,7 @@ pub async fn list_countries_currencies_for_connector_payment_method( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantConnectorAccountWrite), req.headers(), ), diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 956909f9e5..9f6c35cacd 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -137,9 +137,9 @@ pub async fn payments_create( ) }, match env::which() { - env::Env::Production => &auth::ApiKeyAuth, + env::Env::Production => &auth::HeaderAuth(auth::ApiKeyAuth), _ => auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PaymentWrite), req.headers(), ), @@ -564,7 +564,7 @@ pub async fn payments_capture( HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), locking_action, )) .await @@ -633,7 +633,7 @@ pub async fn payments_connector_session( header_payload.clone(), ) }, - &auth::PublishableKeyAuth, + &auth::HeaderAuth(auth::PublishableKeyAuth), locking_action, )) .await @@ -924,7 +924,7 @@ pub async fn payments_cancel( HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), locking_action, )) .await @@ -972,7 +972,7 @@ pub async fn payments_list( payments::list_payments(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PaymentRead), req.headers(), ), @@ -1088,9 +1088,9 @@ pub async fn payments_approve( ) }, match env::which() { - env::Env::Production => &auth::ApiKeyAuth, + env::Env::Production => &auth::HeaderAuth(auth::ApiKeyAuth), _ => auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PaymentWrite), http_req.headers(), ), @@ -1143,9 +1143,9 @@ pub async fn payments_reject( ) }, match env::which() { - env::Env::Production => &auth::ApiKeyAuth, + env::Env::Production => &auth::HeaderAuth(auth::ApiKeyAuth), _ => auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PaymentWrite), http_req.headers(), ), @@ -1287,7 +1287,7 @@ pub async fn payments_incremental_authorization( HeaderPayload::default(), ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), locking_action, )) .await @@ -1339,7 +1339,7 @@ pub async fn payments_external_authentication( req, ) }, - &auth::PublishableKeyAuth, + &auth::HeaderAuth(auth::PublishableKeyAuth), locking_action, )) .await @@ -1456,7 +1456,7 @@ pub async fn retrieve_extended_card_info( payment_id, ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 4e81b67fb3..d32aa0da3e 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -41,7 +41,7 @@ pub async fn payouts_create( |state, auth, req, _| { payouts_create_core(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -83,7 +83,7 @@ pub async fn payouts_retrieve( payouts_retrieve_core(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PayoutRead), req.headers(), ), @@ -126,7 +126,7 @@ pub async fn payouts_update( |state, auth, req, _| { payouts_update_core(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -199,7 +199,7 @@ pub async fn payouts_cancel( |state, auth, req, _| { payouts_cancel_core(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -239,7 +239,7 @@ pub async fn payouts_fulfill( |state, auth, req, _| { payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -274,7 +274,7 @@ pub async fn payouts_list( payload, |state, auth, req, _| payouts_list_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PayoutRead), req.headers(), ), @@ -314,7 +314,7 @@ pub async fn payouts_list_by_filter( payouts_filtered_list_core(state, auth.merchant_account, auth.key_store, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PayoutRead), req.headers(), ), @@ -354,7 +354,7 @@ pub async fn payouts_list_available_filters( payouts_list_available_filters_core(state, auth.merchant_account, req) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::PayoutRead), req.headers(), ), diff --git a/crates/router/src/routes/poll.rs b/crates/router/src/routes/poll.rs index e4591b4032..bbb2a9a63a 100644 --- a/crates/router/src/routes/poll.rs +++ b/crates/router/src/routes/poll.rs @@ -39,7 +39,7 @@ pub async fn retrieve_poll_status( &req, poll_id, |state, auth, req, _| poll::retrieve_poll_status(state, req, auth.merchant_account), - &auth::PublishableKeyAuth, + &auth::HeaderAuth(auth::PublishableKeyAuth), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index 46ff0dce2a..013ed06497 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -38,7 +38,7 @@ pub async fn refunds_create( json_payload.into_inner(), |state, auth, req, _| refund_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RefundWrite), req.headers(), ), @@ -98,7 +98,7 @@ pub async fn refunds_retrieve( ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RefundRead), req.headers(), ), @@ -148,7 +148,7 @@ pub async fn refunds_retrieve_with_body( refund_retrieve_core, ) }, - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -188,7 +188,7 @@ pub async fn refunds_update( &req, refund_update_req, |state, auth, req, _| refund_update_core(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, )) .await @@ -222,7 +222,7 @@ pub async fn refunds_list( payload.into_inner(), |state, auth, req, _| refund_list(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RefundRead), req.headers(), ), @@ -260,7 +260,7 @@ pub async fn refunds_filter_list( payload.into_inner(), |state, auth, req, _| refund_filter_list(state, auth.merchant_account, req), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RefundRead), req.headers(), ), @@ -293,7 +293,7 @@ pub async fn get_refunds_filters(state: web::Data, req: HttpRequest) - (), |state, auth, _, _| get_filters_for_refunds(state, auth.merchant_account), auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RefundRead), req.headers(), ), diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 788acd4ba2..7accfc7f22 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -2,6 +2,7 @@ //! //! Functions that are used to perform the api level configuration, retrieval, updation //! of Routing configs. + use actix_web::{web, HttpRequest, Responder}; use api_models::{ enums, routing as routing_types, @@ -42,7 +43,7 @@ pub async fn routing_create_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), @@ -77,7 +78,7 @@ pub async fn routing_link_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), @@ -107,7 +108,7 @@ pub async fn routing_retrieve_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), @@ -142,7 +143,7 @@ pub async fn list_routing_configs( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), @@ -177,7 +178,7 @@ pub async fn routing_unlink_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), @@ -211,7 +212,7 @@ pub async fn routing_update_default_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), @@ -239,7 +240,7 @@ pub async fn routing_retrieve_default_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), @@ -273,7 +274,7 @@ pub async fn upsert_surcharge_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), req.headers(), ), @@ -304,7 +305,7 @@ pub async fn delete_surcharge_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), req.headers(), ), @@ -335,7 +336,7 @@ pub async fn retrieve_surcharge_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), req.headers(), ), @@ -369,7 +370,7 @@ pub async fn upsert_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), req.headers(), ), @@ -401,7 +402,7 @@ pub async fn delete_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerWrite), req.headers(), ), @@ -429,7 +430,7 @@ pub async fn retrieve_decision_manager_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::SurchargeDecisionManagerRead), req.headers(), ), @@ -465,7 +466,7 @@ pub async fn routing_retrieve_linked_config( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), @@ -497,13 +498,13 @@ pub async fn routing_retrieve_default_config_for_profiles( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), #[cfg(feature = "release")] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingRead), req.headers(), ), @@ -541,7 +542,7 @@ pub async fn routing_update_default_config_for_profile( }, #[cfg(not(feature = "release"))] auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::RoutingWrite), req.headers(), ), diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 25e64e95bd..4199340879 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -30,7 +30,7 @@ pub async fn apple_pay_merchant_registration( ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountWrite), req.headers(), ), @@ -62,7 +62,7 @@ pub async fn retrieve_apple_pay_verified_domains( ) }, auth::auth_type( - &auth::ApiKeyAuth, + &auth::HeaderAuth(auth::ApiKeyAuth), &auth::JWTAuth(Permission::MerchantAccountRead), req.headers(), ), diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 0ee09913e2..df59083537 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -15,6 +15,8 @@ use router_env::logger; use serde::Serialize; use self::blacklist::BlackList; +#[cfg(feature = "partial-auth")] +use self::detached::{ExtractedPayload, GetAuthType}; use super::authorization::{self, permissions::Permission}; #[cfg(feature = "olap")] use super::jwt; @@ -26,6 +28,8 @@ use crate::configs::Settings; use crate::consts; #[cfg(feature = "olap")] use crate::core::errors::UserResult; +#[cfg(feature = "partial-auth")] +use crate::core::metrics; #[cfg(feature = "recon")] use crate::routes::SessionState; use crate::{ @@ -38,10 +42,14 @@ use crate::{ types::domain, utils::OptionExt, }; + pub mod blacklist; pub mod cookies; pub mod decision; +#[cfg(feature = "partial-auth")] +mod detached; + #[derive(Clone, Debug)] pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, @@ -248,6 +256,29 @@ pub struct ApiKeyAuth; pub struct NoAuth; +#[cfg(feature = "partial-auth")] +impl GetAuthType for ApiKeyAuth { + fn get_auth_type(&self) -> detached::PayloadType { + detached::PayloadType::ApiKey + } +} + +// +// # Header Auth +// +// Header Auth is a feature that allows you to authenticate requests using custom headers. This is +// done by checking whether the request contains the specified headers. +// - `x-merchant-id` header is used to authenticate the merchant. +// +// ## Checksum +// - `x-auth-checksum` header is used to authenticate the request. The checksum is calculated using the +// above mentioned headers is generated by hashing the headers mentioned above concatenated with `:` and then hashed with the detached authentication key. +// +// When the [`partial-auth`] feature is disabled the implementation for [`AuthenticateAndFetch`] +// changes where the authentication is done by the [`I`] implementation. +// +pub struct HeaderAuth(pub I); + #[async_trait] impl AuthenticateAndFetch<(), A> for NoAuth where @@ -356,6 +387,136 @@ where } } +#[cfg(not(feature = "partial-auth"))] +#[async_trait] +impl AuthenticateAndFetch for HeaderAuth +where + A: SessionStateInfo + Send + Sync, + I: AuthenticateAndFetch + Sync + Send, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + self.0.authenticate_and_fetch(request_headers, state).await + } +} + +#[cfg(feature = "partial-auth")] +#[async_trait] +impl AuthenticateAndFetch for HeaderAuth +where + A: SessionStateInfo + Sync, + I: AuthenticateAndFetch + GetAuthType + Sync + Send, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let report_failure = || { + metrics::PARTIAL_AUTH_FAILURE.add(&metrics::CONTEXT, 1, &[]); + }; + + let payload = ExtractedPayload::from_headers(request_headers) + .and_then(|value| { + let (algo, secret) = state.get_detached_auth()?; + + Ok(value + .verify_checksum(request_headers, algo, secret) + .then_some(value)) + }) + .map(|inner_payload| { + inner_payload.and_then(|inner| { + (inner.payload_type == self.0.get_auth_type()).then_some(inner) + }) + }); + + match payload { + Ok(Some(data)) => match data { + ExtractedPayload { + payload_type: detached::PayloadType::ApiKey, + merchant_id: Some(merchant_id), + key_id: Some(key_id), + } => { + let auth = construct_authentication_data(state, &merchant_id).await?; + Ok(( + auth.clone(), + AuthenticationType::ApiKey { + merchant_id: auth.merchant_account.get_id().clone(), + key_id, + }, + )) + } + ExtractedPayload { + payload_type: detached::PayloadType::PublishableKey, + merchant_id: Some(merchant_id), + key_id: None, + } => { + let auth = construct_authentication_data(state, &merchant_id).await?; + Ok(( + auth.clone(), + AuthenticationType::PublishableKey { + merchant_id: auth.merchant_account.get_id().clone(), + }, + )) + } + _ => { + report_failure(); + self.0.authenticate_and_fetch(request_headers, state).await + } + }, + Ok(None) => { + report_failure(); + self.0.authenticate_and_fetch(request_headers, state).await + } + Err(error) => { + logger::error!(%error, "Failed to extract payload from headers"); + report_failure(); + self.0.authenticate_and_fetch(request_headers, state).await + } + } + } +} + +#[cfg(feature = "partial-auth")] +async fn construct_authentication_data( + state: &A, + merchant_id: &id_type::MerchantId, +) -> RouterResult +where + A: SessionStateInfo, +{ + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + &(&state.session_state()).into(), + merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + &(&state.session_state()).into(), + merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + }; + + Ok(auth) +} + #[cfg(feature = "olap")] #[derive(Debug)] pub(crate) struct SinglePurposeJWTAuth(pub TokenPurpose); @@ -587,6 +748,13 @@ where #[derive(Debug)] pub struct PublishableKeyAuth; +#[cfg(feature = "partial-auth")] +impl GetAuthType for PublishableKeyAuth { + fn get_auth_type(&self) -> detached::PayloadType { + detached::PayloadType::PublishableKey + } +} + #[async_trait] impl AuthenticateAndFetch for PublishableKeyAuth where @@ -1082,7 +1250,7 @@ impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate { } } -pub fn get_auth_type_and_flow( +pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( Box>, @@ -1091,9 +1259,12 @@ pub fn get_auth_type_and_flow( let api_key = get_api_key(headers)?; if api_key.starts_with("pk_") { - return Ok((Box::new(PublishableKeyAuth), api::AuthFlow::Client)); + return Ok(( + Box::new(HeaderAuth(PublishableKeyAuth)), + api::AuthFlow::Client, + )); } - Ok((Box::new(ApiKeyAuth), api::AuthFlow::Merchant)) + Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } pub fn check_client_secret_and_get_auth( @@ -1104,7 +1275,7 @@ pub fn check_client_secret_and_get_auth( api::AuthFlow, )> where - T: SessionStateInfo, + T: SessionStateInfo + Sync + Send, ApiKeyAuth: AuthenticateAndFetch, PublishableKeyAuth: AuthenticateAndFetch, { @@ -1116,7 +1287,10 @@ where .map_err(|_| errors::ApiErrorResponse::MissingRequiredField { field_name: "client_secret", })?; - return Ok((Box::new(PublishableKeyAuth), api::AuthFlow::Client)); + return Ok(( + Box::new(HeaderAuth(PublishableKeyAuth)), + api::AuthFlow::Client, + )); } if payload.get_client_secret().is_some() { @@ -1125,7 +1299,7 @@ where } .into()); } - Ok((Box::new(ApiKeyAuth), api::AuthFlow::Merchant)) + Ok((Box::new(HeaderAuth(ApiKeyAuth)), api::AuthFlow::Merchant)) } pub async fn get_ephemeral_or_other_auth( @@ -1138,7 +1312,7 @@ pub async fn get_ephemeral_or_other_auth( bool, )> where - T: SessionStateInfo, + T: SessionStateInfo + Sync + Send, ApiKeyAuth: AuthenticateAndFetch, PublishableKeyAuth: AuthenticateAndFetch, EphemeralKeyAuth: AuthenticateAndFetch, @@ -1148,7 +1322,11 @@ where if api_key.starts_with("epk") { Ok((Box::new(EphemeralKeyAuth), api::AuthFlow::Client, true)) } else if is_merchant_flow { - Ok((Box::new(ApiKeyAuth), api::AuthFlow::Merchant, false)) + Ok(( + Box::new(HeaderAuth(ApiKeyAuth)), + api::AuthFlow::Merchant, + false, + )) } else { let payload = payload.get_required_value("ClientSecretFetch")?; let (auth, auth_flow) = check_client_secret_and_get_auth(headers, payload)?; @@ -1156,13 +1334,13 @@ where } } -pub fn is_ephemeral_auth( +pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { let api_key = get_api_key(headers)?; if !api_key.starts_with("epk") { - Ok(Box::new(ApiKeyAuth)) + Ok(Box::new(HeaderAuth(ApiKeyAuth))) } else { Ok(Box::new(EphemeralKeyAuth)) } diff --git a/crates/router/src/services/authentication/detached.rs b/crates/router/src/services/authentication/detached.rs new file mode 100644 index 0000000000..af373d3e55 --- /dev/null +++ b/crates/router/src/services/authentication/detached.rs @@ -0,0 +1,109 @@ +use std::{borrow::Cow, string::ToString}; + +use actix_web::http::header::HeaderMap; +use common_utils::{crypto::VerifySignature, id_type::MerchantId}; +use error_stack::ResultExt; +use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse; + +use crate::core::errors::RouterResult; + +const HEADER_AUTH_TYPE: &str = "x-auth-type"; +const HEADER_MERCHANT_ID: &str = "x-merchant-id"; +const HEADER_KEY_ID: &str = "x-key-id"; +const HEADER_CHECKSUM: &str = "x-checksum"; + +#[derive(Debug)] +pub struct ExtractedPayload { + pub payload_type: PayloadType, + pub merchant_id: Option, + pub key_id: Option, +} + +#[derive(strum::EnumString, strum::Display, PartialEq, Debug)] +#[strum(serialize_all = "snake_case")] +pub enum PayloadType { + ApiKey, + PublishableKey, +} + +pub trait GetAuthType { + fn get_auth_type(&self) -> PayloadType; +} + +impl ExtractedPayload { + pub fn from_headers(headers: &HeaderMap) -> RouterResult { + let merchant_id = headers + .get(HEADER_MERCHANT_ID) + .and_then(|value| value.to_str().ok()) + .ok_or_else(|| ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid or not present", HEADER_MERCHANT_ID), + }) + .map_err(error_stack::Report::from) + .and_then(|merchant_id| { + MerchantId::try_from(Cow::from(merchant_id.to_string())).change_context( + ApiErrorResponse::InvalidRequestData { + message: format!( + "`{}` header is invalid or not present", + HEADER_MERCHANT_ID + ), + }, + ) + })?; + + let auth_type: PayloadType = headers + .get(HEADER_AUTH_TYPE) + .and_then(|inner| inner.to_str().ok()) + .ok_or_else(|| ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header not present", HEADER_AUTH_TYPE), + })? + .parse::() + .change_context(ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header not present", HEADER_AUTH_TYPE), + })?; + + Ok(Self { + payload_type: auth_type, + merchant_id: Some(merchant_id), + key_id: headers + .get(HEADER_KEY_ID) + .and_then(|v| v.to_str().ok()) + .map(|v| v.to_string()), + }) + } + + pub fn verify_checksum( + &self, + headers: &HeaderMap, + algo: impl VerifySignature, + secret: &[u8], + ) -> bool { + let output = || { + let checksum = headers.get(HEADER_CHECKSUM)?.to_str().ok()?; + let payload = self.generate_payload(); + + algo.verify_signature(secret, &hex::decode(checksum).ok()?, payload.as_bytes()) + .ok() + }; + + output().unwrap_or(false) + } + + // The payload should be `:` separated strings of all the fields + fn generate_payload(&self) -> String { + append_option( + &self.payload_type.to_string(), + &self + .merchant_id + .as_ref() + .map(|inner| append_option(inner.get_string_repr(), &self.key_id)), + ) + } +} + +#[inline] +fn append_option(prefix: &str, data: &Option) -> String { + match data { + Some(inner) => format!("{}:{}", prefix, inner), + None => prefix.to_string(), + } +}