diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 3cd959838a..1c49454222 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -499,7 +499,7 @@ pub async fn connector_create( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), required_permission: Permission::ProfileConnectorWrite, @@ -598,7 +598,7 @@ pub async fn connector_retrieve( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, // This should ideally be ProfileConnectorRead, but since this API responds with @@ -716,7 +716,7 @@ pub async fn connector_list( merchant_id.to_owned(), |state, _auth, merchant_id, _| list_payment_connectors(state, merchant_id, None), auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: Permission::MerchantConnectorRead, @@ -769,7 +769,7 @@ pub async fn connector_list_profile( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: Permission::ProfileConnectorRead, @@ -830,7 +830,7 @@ pub async fn connector_update( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), required_permission: Permission::ProfileConnectorWrite, diff --git a/crates/router/src/routes/profiles.rs b/crates/router/src/routes/profiles.rs index 1b98160546..cd91275df7 100644 --- a/crates/router/src/routes/profiles.rs +++ b/crates/router/src/routes/profiles.rs @@ -29,7 +29,7 @@ pub async fn profile_create( create_profile(state, req, auth_data.merchant_account, auth_data.key_store) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: permissions::Permission::MerchantAccountWrite, @@ -95,7 +95,7 @@ pub async fn profile_retrieve( profile_id, |state, auth_data, profile_id, _| retrieve_profile(state, profile_id, auth_data.key_store), auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id: merchant_id.clone(), required_permission: permissions::Permission::ProfileAccountRead, @@ -158,7 +158,7 @@ pub async fn profile_update( json_payload.into_inner(), |state, auth_data, req, _| update_profile(state, &profile_id, auth_data.key_store, req), auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantAndProfileFromRoute { merchant_id: merchant_id.clone(), profile_id: profile_id.clone(), @@ -243,7 +243,7 @@ pub async fn profiles_list( merchant_id.clone(), |state, _auth, merchant_id, _| list_profile(state, merchant_id, None), auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()), + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: permissions::Permission::MerchantAccountRead, @@ -309,7 +309,7 @@ pub async fn profiles_list_at_profile_level( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: permissions::Permission::ProfileAccountRead, @@ -402,7 +402,7 @@ pub async fn payment_connector_list_profile( ) }, auth::auth_type( - &auth::AdminApiAuthWithMerchantIdFromHeader, + &auth::HeaderAuth(auth::ApiKeyAuthWithMerchantIdFromRoute(merchant_id.clone())), &auth::JWTAuthMerchantFromRoute { merchant_id, required_permission: permissions::Permission::ProfileConnectorRead, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 4eb9175666..7e3d6802b8 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -633,6 +633,137 @@ where } } +#[derive(Debug)] +pub struct ApiKeyAuthWithMerchantIdFromRoute(pub id_type::MerchantId); + +#[cfg(feature = "partial-auth")] +impl GetAuthType for ApiKeyAuthWithMerchantIdFromRoute { + fn get_auth_type(&self) -> detached::PayloadType { + detached::PayloadType::ApiKey + } +} + +#[cfg(feature = "v1")] +#[async_trait] +impl AuthenticateAndFetch for ApiKeyAuthWithMerchantIdFromRoute +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let merchant_id_from_route = self.0.clone(); + let api_key = get_api_key(request_headers) + .change_context(errors::ApiErrorResponse::Unauthorized)? + .trim(); + if api_key.is_empty() { + return Err(errors::ApiErrorResponse::Unauthorized) + .attach_printable("API key is empty"); + } + + let api_key = api_keys::PlaintextApiKey::from(api_key); + let hash_key = { + let config = state.conf(); + config.api_keys.get_inner().get_hash_key()? + }; + let hashed_api_key = api_key.keyed_hash(hash_key.peek()); + + let stored_api_key = state + .store() + .find_api_key_by_hash_optional(hashed_api_key.into()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed + .attach_printable("Failed to retrieve API key")? + .ok_or(report!(errors::ApiErrorResponse::Unauthorized)) // If retrieve returned `None` + .attach_printable("Merchant not authenticated")?; + + if stored_api_key + .expires_at + .map(|expires_at| expires_at < date_time::now()) + .unwrap_or(false) + { + return Err(report!(errors::ApiErrorResponse::Unauthorized)) + .attach_printable("API key has expired"); + } + + let key_manager_state = &(&state.session_state()).into(); + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &stored_api_key.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 profile_id = + get_header_value_by_key(headers::X_PROFILE_ID.to_string(), request_headers)? + .map(id_type::ProfileId::from_str) + .transpose() + .change_context(errors::ValidationError::IncorrectValueProvided { + field_name: "X-Profile-Id", + }) + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + if merchant_id_from_route != stored_api_key.merchant_id { + return Err(report!(errors::ApiErrorResponse::Unauthorized)).attach_printable( + "Merchant ID from route and Merchant ID from api-key in header do not match", + ); + } + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &stored_api_key.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + // Get connected merchant account if API call is done by Platform merchant account on behalf of connected merchant account + let (merchant, platform_merchant_account) = if state.conf().platform.enabled { + get_platform_merchant_account(state, request_headers, merchant).await? + } else { + (merchant, None) + }; + + let key_store = if platform_merchant_account.is_some() { + state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + merchant.get_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")? + } else { + key_store + }; + + let auth = AuthenticationData { + merchant_account: merchant, + platform_merchant_account, + key_store, + profile_id, + }; + Ok(( + auth.clone(), + AuthenticationType::ApiKey { + merchant_id: auth.merchant_account.get_id().clone(), + key_id: stored_api_key.key_id, + }, + )) + } +} + #[cfg(not(feature = "partial-auth"))] #[async_trait] impl AuthenticateAndFetch for HeaderAuth diff --git a/cypress-tests-v2/cypress/support/commands.js b/cypress-tests-v2/cypress/support/commands.js index b6955c542a..33f8907ce3 100644 --- a/cypress-tests-v2/cypress/support/commands.js +++ b/cypress-tests-v2/cypress/support/commands.js @@ -310,7 +310,7 @@ Cypress.Commands.add( "businessProfileCreateCall", (businessProfileCreateBody, globalState) => { // Define the necessary variables and constants - const api_key = globalState.get("adminApiKey"); + const api_key = globalState.get("apiKey"); const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); const url = `${base_url}/v2/profiles`; @@ -353,7 +353,7 @@ Cypress.Commands.add( ); Cypress.Commands.add("businessProfileRetrieveCall", (globalState) => { // Define the necessary variables and constants - const api_key = globalState.get("adminApiKey"); + const api_key = globalState.get("apiKey"); const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); @@ -395,7 +395,7 @@ Cypress.Commands.add( "businessProfileUpdateCall", (businessProfileUpdateBody, globalState) => { // Define the necessary variables and constants - const api_key = globalState.get("adminApiKey"); + const api_key = globalState.get("apiKey"); const base_url = globalState.get("baseUrl"); const merchant_id = globalState.get("merchantId"); const profile_id = globalState.get("profileId"); diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index ec45a75e9b..9872561c95 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -250,7 +250,7 @@ Cypress.Commands.add( Cypress.Commands.add( "createBusinessProfileTest", (createBusinessProfile, globalState, profilePrefix = "profile") => { - const apiKey = globalState.get("adminApiKey"); + const apiKey = globalState.get("apiKey"); const baseUrl = globalState.get("baseUrl"); const connectorId = globalState.get("connectorId"); const merchantId = globalState.get("merchantId"); @@ -309,7 +309,7 @@ Cypress.Commands.add( updateBusinessProfileBody.always_collect_shipping_details_from_wallet_connector = always_collect_shipping_details_from_wallet_connector; - const apiKey = globalState.get("adminApiKey"); + const apiKey = globalState.get("apiKey"); const merchantId = globalState.get("merchantId"); const profileId = globalState.get(`${profilePrefix}Id`); @@ -562,7 +562,7 @@ Cypress.Commands.add( headers: { "Content-Type": "application/json", Accept: "application/json", - "api-key": globalState.get("adminApiKey"), + "api-key": globalState.get("apiKey"), }, body: createConnectorBody, failOnStatusCode: false, @@ -603,7 +603,7 @@ Cypress.Commands.add( profilePrefix = "profile", mcaPrefix = "merchantConnector" ) => { - const api_key = globalState.get("adminApiKey"); + const api_key = globalState.get("apiKey"); const base_url = globalState.get("baseUrl"); const connector_id = globalState.get("connectorId"); const merchant_id = globalState.get("merchantId"); @@ -724,7 +724,7 @@ Cypress.Commands.add( headers: { Accept: "application/json", "Content-Type": "application/json", - "api-key": globalState.get("adminApiKey"), + "api-key": globalState.get("apiKey"), }, body: createConnectorBody, failOnStatusCode: false, @@ -768,7 +768,7 @@ Cypress.Commands.add("connectorRetrieveCall", (globalState) => { headers: { Accept: "application/json", "Content-Type": "application/json", - "api-key": globalState.get("adminApiKey"), + "api-key": globalState.get("apiKey"), "x-merchant-id": merchant_id, }, failOnStatusCode: false, @@ -813,7 +813,7 @@ Cypress.Commands.add("connectorDeleteCall", (globalState) => { Cypress.Commands.add( "connectorUpdateCall", (connectorType, updateConnectorBody, globalState) => { - const api_key = globalState.get("adminApiKey"); + const api_key = globalState.get("apiKey"); const base_url = globalState.get("baseUrl"); const connector_id = globalState.get("connectorId"); const merchant_id = globalState.get("merchantId"); @@ -858,7 +858,7 @@ Cypress.Commands.add("connectorListByMid", (globalState) => { url: `${globalState.get("baseUrl")}/account/${merchant_id}/connectors`, headers: { "Content-Type": "application/json", - "api-key": globalState.get("adminApiKey"), + "api-key": globalState.get("apiKey"), "X-Merchant-Id": merchant_id, }, failOnStatusCode: false, @@ -3554,7 +3554,7 @@ Cypress.Commands.add("ListMcaByMid", (globalState) => { url: `${globalState.get("baseUrl")}/account/${merchantId}/connectors`, headers: { "Content-Type": "application/json", - "api-key": globalState.get("adminApiKey"), + "api-key": globalState.get("apiKey"), "X-Merchant-Id": merchantId, }, failOnStatusCode: false,