fix(authentication): add Organization context validation in `Merchant Create` and `Merchant List` APIs
Type of Change
- [x] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD
Description
This PR introduces authentication validation checks in the following APIs to prevent unauthorized access across organizations:
List Merchant API
- Problem: When using JWT-based auth, the API allowed any
organization_idin the query params as long as the token was valid. - Fix: Added a validation to ensure that the
organization_idprovided in the query must match the one derived from the authentication context. - This prevents users from listing merchants in other organizations using a valid token.
Create Merchant API
- Problem:
Only the Admin API Key and configured fallback API Keys (set via env) can be used to create merchants.
- If the Admin API Key fails, the fallback API Key is used as a backup.
- When using this fallback API Key:
- If
organization_idis not provided, a new organization is created, potentially allowing unauthorized org creation. - If
organization_idis manually provided, a user could create a merchant under any organization they should not have access to.
- If
- Fix:
- When the fallback API Key is used, added a check to ensure that the merchant is created only within the organization associated with the authenticated merchant.
- This prevents unauthorized creation of merchants in unintended organizations.
Changes to Auth Flow
We have updated the authentication mechanism to now return:
Option<AuthenticationDataWithOrg>
#[derive(Clone, Debug)]
pub struct AuthenticationDataWithOrg {
pub organization_id: id_type::OrganizationId,
}
Some(AuthenticationDataWithOrg { organization_id }) is returned when:
- A Configured Fallback API Key is used and the organization is resolved from the associated merchant.
- A valid JWT token is used and the organization is derived from the merchant’s context.
None is returned when:
- Admin API Key is used
Additional Changes
- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables
Motivation and Context
The current implementations of the List and Create Merchant APIs do not enforce strict validation between the authenticated user's organization context and the organization_id passed in requests. This creates potential security risks where:
- Users with valid JWTs can view merchants from other organizations.
- API key users can create merchants under unrelated or unauthorized organizations.
This PR addresses those gaps by ensuring that all organization_id references are explicitly validated against the authenticated context.
How did you test it?
Create Merchant API:
We can test the behavior of the Create Merchant API under two authentication modes:
- Admin API Key (Primary)
- Configured API Key (Fallback via env)
Request:
curl --location 'http://localhost:8080/accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data-raw '{
"merchant_id": "merchant_1747853425",
"locker_id": "m0010",
"merchant_name": "M1 Account",
"merchant_details": {
"primary_contact_person": "John Test",
"primary_email": "[email protected]",
"primary_phone": "sunt laborum",
"secondary_contact_person": "John Test2",
"secondary_email": "[email protected]",
"secondary_phone": "cillum do dolor id",
"website": "https://www.example.com",
"about_business": "Online Retail with a wide selection of organic products for North America",
"address": {
"line1": "1467",
"line2": "Harrison Street",
"line3": "Harrison Street",
"city": "San Fransico",
"state": "California",
"zip": "94122",
"country": "US",
"first_name":"john",
"last_name":"Doe"
}
},
"organization_id": "org_AUw4KIR0COrD90wgqZLq", // (optional)
"return_url": "https://google.com/success",
"webhook_details": {
"webhook_version": "1.0.1",
"webhook_username": "ekart_retail",
"webhook_password": "password_ekart@123",
"webhook_url":"https://webhook.site",
"payment_created_enabled": true,
"payment_succeeded_enabled": true,
"payment_failed_enabled": true
},
"sub_merchants_enabled": false,
"parent_merchant_id":"merchant_123",
"metadata": {
"city": "NY",
"unit": "245"
},
"primary_business_details": [
{
"country": "US",
"business": "default"
}
]
}'
Expected Response:
{
"merchant_id": "merchant_1747853253",
"merchant_name": "M1 Account",
"return_url": "https://google.com/success",
"enable_payment_response_hash": true,
"payment_response_hash_key": "zXBHjOf9FmDuG4hi8tX409O6yVeY9H4Ldt5ofqvxFvv6RROH3Edj3p6WDU94maPG",
"redirect_to_merchant_with_http_post": false,
"merchant_details": {
"primary_contact_person": "John Test",
"primary_phone": "sunt laborum",
"primary_email": "[email protected]",
"secondary_contact_person": "John Test2",
"secondary_phone": "cillum do dolor id",
"secondary_email": "[email protected]",
"website": "https://www.example.com",
"about_business": "Online Retail with a wide selection of organic products for North America",
"address": {
"city": "San Fransico",
"country": "US",
"line1": "1467",
"line2": "Harrison Street",
"line3": "Harrison Street",
"zip": "94122",
"state": "California",
"first_name": "john",
"last_name": "Doe"
}
},
"webhook_details": {
"webhook_version": "1.0.1",
"webhook_username": "ekart_retail",
"webhook_password": "password_ekart@123",
"webhook_url": "https://webhook.site",
"payment_created_enabled": true,
"payment_succeeded_enabled": true,
"payment_failed_enabled": true
},
"payout_routing_algorithm": null,
"sub_merchants_enabled": false,
"parent_merchant_id": null,
"publishable_key": "pk_dev_14a42e013f8042888e6aef2b00fb6d35",
"metadata": {
"city": "NY",
"unit": "245",
"compatible_connector": null
},
"locker_id": "m0010",
"primary_business_details": [
{
"country": "US",
"business": "default"
}
],
"frm_routing_algorithm": null,
"organization_id": "org_AUw4KIR0COrD90wgqZLq",
"is_recon_enabled": false,
"default_profile": "pro_tIoLTR9Zz3QO66EtmhE4",
"recon_status": "not_requested",
"pm_collect_link_config": null,
"product_type": "orchestration"
}
1. Admin API Key
| Scenario | organization_id in Request | Expected Behavior |
|---|---|---|
| 1.1 | Present | Use the provided organization_id to create the merchant. |
| 1.2 | Absent | Create a new organization and associate the merchant with it. |
The Admin API key is allowed to create new organizations if
organization_idis missing.
2. Configured Fallback API Key (via env)
| Scenario | organization_id in Request | Expected Behavior |
|---|---|---|
| 2.1 | Present & matches auth org | Proceed with merchant creation. |
| 2.2 | Present but mismatched org | Reject with InvalidRequestData. |
| 2.3 | Absent | Automatically use the organization from the authentication context. |
Fallback API Key must always operate within the authenticated org. Cross-org creation should be blocked.
List Merchant API:
We can test the behavior of the List Merchant API under three authentication modes:
- Admin API Key
- Configured Fallback API Key
- JWT Token
Request:
curl --location 'http://localhost:8080/accounts/list?organization_id=org_AUw4KIR0COrD90wgqZLQ' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_UfHPiN8Ua6cSsV6QF0LZmH0U7JlPY1CMrjaKNWy2zyA6i5VR0NoKdW3lKqvb6vea' \
--data ''
Expected Response:
[
{
"merchant_id": "merchant_1747665050",
"merchant_name": "Hyperswitch",
"return_url": null,
"enable_payment_response_hash": true,
"payment_response_hash_key": "rki2yoKsrd7xZWc4EiGsHWO1W0XaGKNwmfyHJ1E4IeuEJWKlzsGavQDBd89e6PxL",
"redirect_to_merchant_with_http_post": false,
"merchant_details": null,
"webhook_details": null,
"payout_routing_algorithm": null,
"sub_merchants_enabled": false,
"parent_merchant_id": null,
"publishable_key": "pk_dev_dfb4faba44374de8851bc7f36b1a25b9",
"metadata": null,
"locker_id": null,
"primary_business_details": [],
"frm_routing_algorithm": null,
"organization_id": "org_AUw4KIR0COrD90wgqZLQ",
"is_recon_enabled": false,
"default_profile": "pro_poTKIbFuUTQlH4Lukesv",
"recon_status": "not_requested",
"pm_collect_link_config": null,
"product_type": "orchestration"
},
{
"merchant_id": "merchant_1747665195",
"merchant_name": "Standard",
"return_url": null,
"enable_payment_response_hash": true,
"payment_response_hash_key": "XuyZ2ltZeYtTvVSaYjgQ4U5TL8JLTKcoIkzab89PkfUAWueP3OeYbNmypErVefyh",
"redirect_to_merchant_with_http_post": false,
"merchant_details": null,
"webhook_details": null,
"payout_routing_algorithm": null,
"sub_merchants_enabled": false,
"parent_merchant_id": null,
"publishable_key": "pk_dev_44ab7414c3634bde825492fa49b9a49a",
"metadata": null,
"locker_id": null,
"primary_business_details": [],
"frm_routing_algorithm": null,
"organization_id": "org_AUw4KIR0COrD90wgqZLQ",
"is_recon_enabled": false,
"default_profile": "pro_QxoXgT37uHxKdVZZxpQS",
"recon_status": "not_requested",
"pm_collect_link_config": null,
"product_type": "orchestration"
}
]
1. Admin API Key
| Scenario | organization_id in Query | Expected Behavior |
|---|---|---|
| 1.1 | Present | List merchants belonging to the specified organization. |
| 1.2 | Absent | InvalidRequestData (organization_id is required). |
Admin API key is allowed to list merchants from any organization explicitly passed via query.
2. Configured Fallback API Key (via env)
| Scenario | organization_id in Query | Expected Behavior |
|---|---|---|
| 2.1 | Present & matches org from auth | List merchants for that org. |
| 2.2 | Present but mismatched with auth org | Reject with InvalidRequestData. |
| 2.3 | Absent | organization_id is mandatory – reject the request. |
Fallback API Key must always operate within its own org context.
3. JWT Authentication
| Scenario | X-Merchant-Id Header | organization_id in Query | Expected Behavior |
|---|---|---|---|
| 3.1 | Present & matches JWT | Present & matches org auth context | Return merchant list |
| 3.2 | Present & matches JWT | Present but mismatches org auth context | Error - InvalidRequestData |
| 3.2 | Present but mismatches JWT | Present | Error – mismatched merchant context |
| 3.3 | Present | org_id not provided | organization_id required |
| 3.4 | Header missing | org_id matches JWT context | merchant_id required via header |
JWT-based requests must strictly match both
merchant_id(via header) andorganization_idwith those in the token payload/auth context.
Checklist
- [x] I formatted the code
cargo +nightly fmt --all - [x] I addressed lints thrown by
cargo clippy - [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible