Skip to content

Conversation

tsdk02
Copy link
Contributor

@tsdk02 tsdk02 commented May 21, 2025

Type of Change

  • 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_id in the query params as long as the token was valid.
  • Fix: Added a validation to ensure that the organization_id provided 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_id is not provided, a new organization is created, potentially allowing unauthorized org creation.
      • If organization_id is manually provided, a user could create a merchant under any organization they should not have access to.
  • 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_id is 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) and organization_id with those in the token payload/auth context.


Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@tsdk02 tsdk02 self-assigned this May 21, 2025
@tsdk02 tsdk02 requested review from a team as code owners May 21, 2025 14:14
@tsdk02 tsdk02 added the C-bug Category: Bug label May 21, 2025
Copy link

semanticdiff-com bot commented May 21, 2025

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/core/user.rs  84% smaller
  crates/router/src/routes/admin.rs  75% smaller
  crates/router/src/types/domain/user.rs  72% smaller
  crates/router/src/core/admin.rs  22% smaller
  crates/router/src/services/authentication.rs  7% smaller

@tsdk02 tsdk02 changed the title fix(authentication): update Merchant Create and Merchant List APIs with Organization context validation fix(authentication): add Organization context validation in Merchant Create and Merchant List APIs May 21, 2025
@@ -646,6 +670,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
state: &SessionState,
key_store: domain::MerchantKeyStore,
identifier: &id_type::MerchantId,
_org_data: Option<authentication::AuthenticationDataWithOrg>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we not using in v2? is it not required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trait MerchantAccountCreateBridge is common to both v1 and v2, and we would not receive org_id from authentication in v2. The request body - MerchantAccountCreate will mandatorily have the organization_id, which can be used to retrieve the organization in the v2 implementation.

@@ -223,7 +223,7 @@ pub async fn merchant_account_create(
state,
&req,
new_request_payload_with_org_id,
|state, _, req, _| create_merchant_account(state, req),
|state, _, req, _| create_merchant_account(state, req, None),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why None in v2 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For v2 create_merchant, V2AdminApiAuth is the only authentication allowed, so we would not receive any org_id from auth, hence passing None.

Comment on lines +1399 to +1407
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)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also be unexpected in a way: if we are able to find API key in our database, but unable to find the merchant account associated with the API key, then our database is possibly in an inconsistent state.

Not entirely sure if we should return 401 or another status code here, let's at least add a log line maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it is being handled in all types of API Key authentications currently. We are throwing 401 when the API Key is found, but merchant account is not found.
Should this be changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can take this up in a separate PR, for handling this in all such instances.

@tsdk02 tsdk02 requested a review from hrithikesh026 May 23, 2025 10:06
Copy link
Contributor

@hrithikesh026 hrithikesh026 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@likhinbopanna likhinbopanna added this pull request to the merge queue May 26, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 26, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue May 26, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 26, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue May 27, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 27, 2025
@likhinbopanna likhinbopanna added this pull request to the merge queue May 27, 2025
Merged via the queue into main with commit 7fee571 May 27, 2025
13 of 20 checks passed
@likhinbopanna likhinbopanna deleted the fix-merchant-create-and-list branch May 27, 2025 10:24
pixincreate added a commit that referenced this pull request May 30, 2025
…ordea-sepa

* 'main' of github.com:juspay/hyperswitch: (30 commits)
  chore(version): 2025.05.30.0
  chore(ci): update postman ci credentials (#8172)
  chore(docs): remove old add_connector.md file (#8143)
  refactor: Payment Attempt as mandatory field in PaymentStatusData (#8126)
  fix(payment_link): sanitize embedded payment link data (#7736)
  chore(version): 2025.05.29.0
  feat(analytics): Add ckh columns for 3ds intelligence analytics (#8136)
  refactor(debit_routing): Handle missing merchant_business_country by defaulting to US (#8141)
  chore(version): 2025.05.28.0
  refactor(success_based): add support for exploration (#8158)
  feat(dynamic_routing): add get api for dynamic routing volume split (#8114)
  fix: incorrect payout_method_id in payouts table (#8107)
  feat(router): Enable client_secret auth for payments_get_intent [v2] (#8119)
  chore: address Rust 1.87.0 clippy lints (#8130)
  feat: list for dynamic routing (#8111)
  ci(cypress): fix mandates unsupported connectors (#8086)
  feat(connector): [Barclaycard] Implement Cards - Non 3DS flow (#8068)
  fix(authentication): add Organization context validation in `Merchant Create` and `Merchant List` APIs (#8103)
  feat(connector): [Worldpayxml] add card payment (#8076)
  feat(connector): Stripe revolut pay wallet integration (#8066)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: Bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants