Skip to content

Commit 7fee571

Browse files
authored
fix(authentication): add Organization context validation in Merchant Create and Merchant List APIs (#8103)
1 parent b8b68af commit 7fee571

File tree

5 files changed

+154
-24
lines changed

5 files changed

+154
-24
lines changed

crates/router/src/core/admin.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ pub async fn get_organization(
190190
pub async fn create_merchant_account(
191191
state: SessionState,
192192
req: api::MerchantAccountCreate,
193+
org_data_from_auth: Option<authentication::AuthenticationDataWithOrg>,
193194
) -> RouterResponse<api::MerchantAccountResponse> {
194195
#[cfg(feature = "keymanager_create")]
195196
use common_utils::{keymanager, types::keymanager::EncryptionTransferRequest};
@@ -242,7 +243,12 @@ pub async fn create_merchant_account(
242243
};
243244

244245
let domain_merchant_account = req
245-
.create_domain_model_from_request(&state, key_store.clone(), &merchant_id)
246+
.create_domain_model_from_request(
247+
&state,
248+
key_store.clone(),
249+
&merchant_id,
250+
org_data_from_auth,
251+
)
246252
.await?;
247253
let key_manager_state = &(&state).into();
248254
db.insert_merchant_key_store(
@@ -301,6 +307,7 @@ trait MerchantAccountCreateBridge {
301307
state: &SessionState,
302308
key: domain::MerchantKeyStore,
303309
identifier: &id_type::MerchantId,
310+
org_data_from_auth: Option<authentication::AuthenticationDataWithOrg>,
304311
) -> RouterResult<domain::MerchantAccount>;
305312
}
306313

@@ -312,6 +319,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
312319
state: &SessionState,
313320
key_store: domain::MerchantKeyStore,
314321
identifier: &id_type::MerchantId,
322+
org_data_from_auth: Option<authentication::AuthenticationDataWithOrg>,
315323
) -> RouterResult<domain::MerchantAccount> {
316324
let db = &*state.accounts_store;
317325
let publishable_key = create_merchant_publishable_key();
@@ -361,7 +369,22 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
361369
)
362370
.await?;
363371

364-
let organization = CreateOrValidateOrganization::new(self.organization_id)
372+
let org_id = match (&self.organization_id, &org_data_from_auth) {
373+
(Some(req_org_id), Some(auth)) => {
374+
if req_org_id != &auth.organization_id {
375+
return Err(errors::ApiErrorResponse::InvalidRequestData {
376+
message: "Mismatched organization_id in request and authenticated context"
377+
.to_string(),
378+
}
379+
.into());
380+
}
381+
Some(req_org_id.clone())
382+
}
383+
(None, Some(auth)) => Some(auth.organization_id.clone()),
384+
(req_org_id, _) => req_org_id.clone(),
385+
};
386+
387+
let organization = CreateOrValidateOrganization::new(org_id)
365388
.create_or_validate(db)
366389
.await?;
367390

@@ -667,6 +690,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
667690
state: &SessionState,
668691
key_store: domain::MerchantKeyStore,
669692
identifier: &id_type::MerchantId,
693+
_org_data: Option<authentication::AuthenticationDataWithOrg>,
670694
) -> RouterResult<domain::MerchantAccount> {
671695
let publishable_key = create_merchant_publishable_key();
672696
let db = &*state.accounts_store;
@@ -787,7 +811,16 @@ pub async fn list_merchant_account(
787811
pub async fn list_merchant_account(
788812
state: SessionState,
789813
req: api_models::admin::MerchantAccountListRequest,
814+
org_data_from_auth: Option<authentication::AuthenticationDataWithOrg>,
790815
) -> RouterResponse<Vec<api::MerchantAccountResponse>> {
816+
if let Some(auth) = org_data_from_auth {
817+
if auth.organization_id != req.organization_id {
818+
return Err(errors::ApiErrorResponse::InvalidRequestData {
819+
message: "Organization ID in request and authentication do not match".to_string(),
820+
}
821+
.into());
822+
}
823+
}
791824
let merchant_accounts = state
792825
.store
793826
.list_merchant_accounts_by_organization_id(&(&state).into(), &req.organization_id)

crates/router/src/core/user.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,13 +1584,22 @@ pub async fn create_org_merchant_for_user(
15841584
.await
15851585
.change_context(UserErrors::InternalServerError)?;
15861586
let default_product_type = consts::user::DEFAULT_PRODUCT_TYPE;
1587-
let merchant_account_create_request =
1588-
utils::user::create_merchant_account_request_for_org(req, org, default_product_type)?;
1587+
let merchant_account_create_request = utils::user::create_merchant_account_request_for_org(
1588+
req,
1589+
org.clone(),
1590+
default_product_type,
1591+
)?;
15891592

1590-
admin::create_merchant_account(state.clone(), merchant_account_create_request)
1591-
.await
1592-
.change_context(UserErrors::InternalServerError)
1593-
.attach_printable("Error while creating a merchant")?;
1593+
admin::create_merchant_account(
1594+
state.clone(),
1595+
merchant_account_create_request,
1596+
Some(auth::AuthenticationDataWithOrg {
1597+
organization_id: org.get_organization_id(),
1598+
}),
1599+
)
1600+
.await
1601+
.change_context(UserErrors::InternalServerError)
1602+
.attach_printable("Error while creating a merchant")?;
15941603

15951604
Ok(ApplicationResponse::StatusOk)
15961605
}

crates/router/src/routes/admin.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ pub async fn merchant_account_create(
185185
state,
186186
&req,
187187
json_payload.into_inner(),
188-
|state, _, req, _| create_merchant_account(state, req),
188+
|state, auth, req, _| create_merchant_account(state, req, auth),
189189
&auth::AdminApiAuthWithApiKeyFallback,
190190
api_locking::LockAction::NotApplicable,
191191
))
@@ -223,7 +223,7 @@ pub async fn merchant_account_create(
223223
state,
224224
&req,
225225
new_request_payload_with_org_id,
226-
|state, _, req, _| create_merchant_account(state, req),
226+
|state, _, req, _| create_merchant_account(state, req, None),
227227
&auth::V2AdminApiAuth,
228228
api_locking::LockAction::NotApplicable,
229229
))
@@ -348,7 +348,9 @@ pub async fn merchant_account_list(
348348
state,
349349
&req,
350350
query_params.into_inner(),
351-
|state, _, request, _| list_merchant_account(state, request),
351+
|state, auth: Option<auth::AuthenticationDataWithOrg>, request, _| {
352+
list_merchant_account(state, request, auth)
353+
},
352354
auth::auth_type(
353355
&auth::AdminApiAuthWithApiKeyFallback,
354356
&auth::JWTAuthMerchantFromHeader {

crates/router/src/services/authentication.rs

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ pub struct AuthenticationDataWithUser {
9898
pub profile_id: id_type::ProfileId,
9999
}
100100

101+
#[derive(Clone, Debug)]
102+
pub struct AuthenticationDataWithOrg {
103+
pub organization_id: id_type::OrganizationId,
104+
}
105+
101106
#[derive(Clone)]
102107
pub struct UserFromTokenWithRoleInfo {
103108
pub user: UserFromToken,
@@ -1330,15 +1335,16 @@ pub struct AdminApiAuthWithApiKeyFallback;
13301335

13311336
#[cfg(feature = "v1")]
13321337
#[async_trait]
1333-
impl<A> AuthenticateAndFetch<(), A> for AdminApiAuthWithApiKeyFallback
1338+
impl<A> AuthenticateAndFetch<Option<AuthenticationDataWithOrg>, A>
1339+
for AdminApiAuthWithApiKeyFallback
13341340
where
13351341
A: SessionStateInfo + Sync,
13361342
{
13371343
async fn authenticate_and_fetch(
13381344
&self,
13391345
request_headers: &HeaderMap,
13401346
state: &A,
1341-
) -> RouterResult<((), AuthenticationType)> {
1347+
) -> RouterResult<(Option<AuthenticationDataWithOrg>, AuthenticationType)> {
13421348
let request_api_key =
13431349
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
13441350

@@ -1347,7 +1353,7 @@ where
13471353
let admin_api_key = &conf.secrets.get_inner().admin_api_key;
13481354

13491355
if request_api_key == admin_api_key.peek() {
1350-
return Ok(((), AuthenticationType::AdminApiKey));
1356+
return Ok((None, AuthenticationType::AdminApiKey));
13511357
}
13521358
let Some(fallback_merchant_ids) = conf.fallback_merchant_ids_api_key_auth.as_ref() else {
13531359
return Err(report!(errors::ApiErrorResponse::Unauthorized)).attach_printable(
@@ -1377,12 +1383,37 @@ where
13771383
.attach_printable("API key has expired");
13781384
}
13791385

1386+
let key_manager_state = &(&state.session_state()).into();
1387+
1388+
let key_store = state
1389+
.store()
1390+
.get_merchant_key_store_by_merchant_id(
1391+
key_manager_state,
1392+
&stored_api_key.merchant_id,
1393+
&state.store().get_master_key().to_vec().into(),
1394+
)
1395+
.await
1396+
.change_context(errors::ApiErrorResponse::Unauthorized)
1397+
.attach_printable("Failed to fetch merchant key store for the merchant id")?;
1398+
1399+
let merchant = state
1400+
.store()
1401+
.find_merchant_account_by_merchant_id(
1402+
key_manager_state,
1403+
&stored_api_key.merchant_id,
1404+
&key_store,
1405+
)
1406+
.await
1407+
.to_not_found_response(errors::ApiErrorResponse::Unauthorized)?;
1408+
13801409
if fallback_merchant_ids
13811410
.merchant_ids
13821411
.contains(&stored_api_key.merchant_id)
13831412
{
13841413
return Ok((
1385-
(),
1414+
Some(AuthenticationDataWithOrg {
1415+
organization_id: merchant.organization_id,
1416+
}),
13861417
AuthenticationType::ApiKey {
13871418
merchant_id: stored_api_key.merchant_id,
13881419
key_id: stored_api_key.key_id,
@@ -2705,6 +2736,50 @@ where
27052736
}
27062737
}
27072738

2739+
#[cfg(feature = "v1")]
2740+
#[async_trait]
2741+
impl<A> AuthenticateAndFetch<Option<AuthenticationDataWithOrg>, A> for JWTAuthMerchantFromHeader
2742+
where
2743+
A: SessionStateInfo + Sync,
2744+
{
2745+
async fn authenticate_and_fetch(
2746+
&self,
2747+
request_headers: &HeaderMap,
2748+
state: &A,
2749+
) -> RouterResult<(Option<AuthenticationDataWithOrg>, AuthenticationType)> {
2750+
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
2751+
if payload.check_in_blacklist(state).await? {
2752+
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
2753+
}
2754+
authorization::check_tenant(
2755+
payload.tenant_id.clone(),
2756+
&state.session_state().tenant.tenant_id,
2757+
)?;
2758+
let role_info = authorization::get_role_info(state, &payload).await?;
2759+
authorization::check_permission(self.required_permission, &role_info)?;
2760+
2761+
let merchant_id_from_header = HeaderMapStruct::new(request_headers)
2762+
.get_id_type_from_header::<id_type::MerchantId>(headers::X_MERCHANT_ID)?;
2763+
2764+
// Check if token has access to MerchantId that has been requested through headers
2765+
if payload.merchant_id != merchant_id_from_header {
2766+
return Err(report!(errors::ApiErrorResponse::InvalidJwtToken));
2767+
}
2768+
2769+
let auth = Some(AuthenticationDataWithOrg {
2770+
organization_id: payload.org_id,
2771+
});
2772+
2773+
Ok((
2774+
auth,
2775+
AuthenticationType::MerchantJwt {
2776+
merchant_id: payload.merchant_id,
2777+
user_id: Some(payload.user_id),
2778+
},
2779+
))
2780+
}
2781+
}
2782+
27082783
#[cfg(feature = "v2")]
27092784
#[async_trait]
27102785
impl<A> AuthenticateAndFetch<AuthenticationData, A> for JWTAuthMerchantFromHeader

crates/router/src/types/domain/user.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ use crate::{
3636
},
3737
db::GlobalStorageInterface,
3838
routes::SessionState,
39-
services::{self, authentication::UserFromToken},
39+
services::{
40+
self,
41+
authentication::{AuthenticationDataWithOrg, UserFromToken},
42+
},
4043
types::{domain, transformers::ForeignFrom},
4144
utils::{self, user::password},
4245
};
@@ -518,13 +521,21 @@ impl NewUserMerchant {
518521
let merchant_account_create_request = self
519522
.create_merchant_account_request()
520523
.attach_printable("unable to construct merchant account create request")?;
521-
522-
let ApplicationResponse::Json(merchant_account_response) = Box::pin(
523-
admin::create_merchant_account(state.clone(), merchant_account_create_request),
524-
)
525-
.await
526-
.change_context(UserErrors::InternalServerError)
527-
.attach_printable("Error while creating a merchant")?
524+
let org_id = merchant_account_create_request
525+
.clone()
526+
.organization_id
527+
.ok_or(UserErrors::InternalServerError)?;
528+
let ApplicationResponse::Json(merchant_account_response) =
529+
Box::pin(admin::create_merchant_account(
530+
state.clone(),
531+
merchant_account_create_request,
532+
Some(AuthenticationDataWithOrg {
533+
organization_id: org_id,
534+
}),
535+
))
536+
.await
537+
.change_context(UserErrors::InternalServerError)
538+
.attach_printable("Error while creating a merchant")?
528539
else {
529540
return Err(UserErrors::InternalServerError.into());
530541
};
@@ -566,7 +577,7 @@ impl NewUserMerchant {
566577
.attach_printable("unable to construct merchant account create request")?;
567578

568579
let ApplicationResponse::Json(merchant_account_response) = Box::pin(
569-
admin::create_merchant_account(state.clone(), merchant_account_create_request),
580+
admin::create_merchant_account(state.clone(), merchant_account_create_request, None),
570581
)
571582
.await
572583
.change_context(UserErrors::InternalServerError)

0 commit comments

Comments
 (0)