Bug: Create authentications to fallback to ApiKeyAuth if AdminApiAuth fails hotfix



Create two new authentication mechanisms that enhance the existing AdminApiAuth and AdminApiAuthWithMerchantIdFromRoute by allowing fallback to regular API key-based authentication when the admin key check fails.
diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs
index c59b8471344..7cd4ce2d4bd 100644
--- a/crates/router/src/configs/secrets_transformers.rs
+++ b/crates/router/src/configs/secrets_transformers.rs
@@ -480,6 +480,7 @@ pub(crate) async fn fetch_raw_secrets(
         #[cfg(feature = "olap")]
         replica_database,
         secrets,
+        fallback_merchant_ids_api_key_auth: conf.fallback_merchant_ids_api_key_auth,
         locker: conf.locker,
         connectors: conf.connectors,
         forex_api,
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index a4c454ec196..83591b15e36 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -66,6 +66,7 @@ pub struct Settings<S: SecretState> {
     pub redis: RedisSettings,
     pub log: Log,
     pub secrets: SecretStateContainer<Secrets, S>,
+    pub fallback_merchant_ids_api_key_auth: Option<FallbackMerchantIds>,
     pub locker: Locker,
     pub key_manager: SecretStateContainer<KeyManagerConfig, S>,
     pub connectors: Connectors,
@@ -676,6 +677,13 @@ pub struct Secrets {
     pub master_enc_key: Secret<String>,
 }
 
+#[derive(Debug, Default, Deserialize, Clone)]
+#[serde(default)]
+pub struct FallbackMerchantIds {
+    #[serde(deserialize_with = "deserialize_merchant_ids")]
+    pub merchant_ids: HashSet<id_type::MerchantId>,
+}
+
 #[derive(Debug, Clone, Default, Deserialize)]
 pub struct UserSettings {
     pub password_validity_in_days: u16,
@@ -1271,6 +1279,53 @@ where
     })?
 }
 
+fn deserialize_merchant_ids_inner(
+    value: impl AsRef<str>,
+) -> Result<HashSet<id_type::MerchantId>, String> {
+    let (values, errors) = value
+        .as_ref()
+        .trim()
+        .split(',')
+        .map(|s| {
+            let trimmed = s.trim();
+            id_type::MerchantId::wrap(trimmed.to_owned()).map_err(|error| {
+                format!(
+                    "Unable to deserialize `{}` as `MerchantId`: {error}",
+                    trimmed
+                )
+            })
+        })
+        .fold(
+            (HashSet::new(), Vec::new()),
+            |(mut values, mut errors), result| match result {
+                Ok(t) => {
+                    values.insert(t);
+                    (values, errors)
+                }
+                Err(error) => {
+                    errors.push(error);
+                    (values, errors)
+                }
+            },
+        );
+
+    if !errors.is_empty() {
+        Err(format!("Some errors occurred:\n{}", errors.join("\n")))
+    } else {
+        Ok(values)
+    }
+}
+
+fn deserialize_merchant_ids<'de, D>(
+    deserializer: D,
+) -> Result<HashSet<id_type::MerchantId>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let s = String::deserialize(deserializer)?;
+    deserialize_merchant_ids_inner(s).map_err(serde::de::Error::custom)
+}
+
 impl<'de> Deserialize<'de> for TenantConfig {
     fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
         #[derive(Deserialize)]
diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs
index 1c494542228..2be9058e906 100644
--- a/crates/router/src/routes/admin.rs
+++ b/crates/router/src/routes/admin.rs
@@ -186,7 +186,7 @@ pub async fn merchant_account_create(
         &req,
         json_payload.into_inner(),
         |state, _, req, _| create_merchant_account(state, req),
-        &auth::AdminApiAuth,
+        &auth::AdminApiAuthWithApiKeyFallback,
         api_locking::LockAction::NotApplicable,
     ))
     .await
@@ -249,7 +249,7 @@ pub async fn retrieve_merchant_account(
         payload,
         |state, _, req, _| get_merchant_account(state, req, None),
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id,
                 // This should ideally be MerchantAccountRead, but since FE is calling this API for
@@ -350,7 +350,7 @@ pub async fn merchant_account_list(
         query_params.into_inner(),
         |state, _, request, _| list_merchant_account(state, request),
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallback,
             &auth::JWTAuthMerchantFromHeader {
                 required_permission: Permission::MerchantAccountRead,
             },
@@ -409,7 +409,7 @@ pub async fn update_merchant_account(
         json_payload.into_inner(),
         |state, _, req, _| merchant_account_update(state, &merchant_id, None, req),
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id: merchant_id.clone(),
                 required_permission: Permission::MerchantAccountWrite,
diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs
index 911617753d4..087e295dc8e 100644
--- a/crates/router/src/routes/api_keys.rs
+++ b/crates/router/src/routes/api_keys.rs
@@ -29,7 +29,7 @@ pub async fn api_key_create(
             api_keys::create_api_key(state, payload, auth_data.key_store).await
         },
         auth::auth_type(
-            &auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id: merchant_id.clone(),
                 required_permission: Permission::MerchantApiKeyWrite,
@@ -130,7 +130,7 @@ pub async fn api_key_retrieve(
         (merchant_id.clone(), key_id.clone()),
         |state, _, (merchant_id, key_id), _| api_keys::retrieve_api_key(state, merchant_id, key_id),
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id: merchant_id.clone(),
                 required_permission: Permission::MerchantApiKeyRead,
@@ -166,7 +166,7 @@ pub async fn api_key_update(
         payload,
         |state, _, payload, _| api_keys::update_api_key(state, payload),
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id,
                 required_permission: Permission::MerchantApiKeyWrite,
@@ -303,7 +303,7 @@ pub async fn api_key_list(
             api_keys::list_api_keys(state, merchant_id, limit, offset).await
         },
         auth::auth_type(
-            &auth::AdminApiAuth,
+            &auth::AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(merchant_id.clone()),
             &auth::JWTAuthMerchantFromRoute {
                 merchant_id,
                 required_permission: Permission::MerchantApiKeyRead,
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 7e3d6802b88..3b9fcfc0d8e 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -654,113 +654,20 @@ where
         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 (auth_data, auth_type) = ApiKeyAuth
+            .authenticate_and_fetch(request_headers, state)
+            .await?;
 
-        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)?;
+        let merchant_id_from_route = self.0.clone();
+        let merchant_id_from_api_key = auth_data.merchant_account.get_id();
 
-        if merchant_id_from_route != stored_api_key.merchant_id {
+        if merchant_id_from_route != *merchant_id_from_api_key {
             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,
-            },
-        ))
+        Ok((auth_data, auth_type))
     }
 }
 
@@ -1398,6 +1305,198 @@ where
     }
 }
 
+#[derive(Debug, Default)]
+pub struct AdminApiAuthWithApiKeyFallback;
+
+#[cfg(feature = "v1")]
+#[async_trait]
+impl<A> AuthenticateAndFetch<(), A> for AdminApiAuthWithApiKeyFallback
+where
+    A: SessionStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &A,
+    ) -> RouterResult<((), AuthenticationType)> {
+        let request_api_key =
+            get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
+
+        let conf = state.conf();
+
+        let admin_api_key = &conf.secrets.get_inner().admin_api_key;
+
+        if request_api_key == admin_api_key.peek() {
+            return Ok(((), AuthenticationType::AdminApiKey));
+        }
+        let Some(fallback_merchant_ids) = conf.fallback_merchant_ids_api_key_auth.as_ref() else {
+            return Err(report!(errors::ApiErrorResponse::Unauthorized)).attach_printable(
+                "Api Key Authentication Failure: fallback merchant set not configured",
+            );
+        };
+
+        let api_key = api_keys::PlaintextApiKey::from(request_api_key);
+        let hash_key = conf.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)
+            .attach_printable("Failed to retrieve API key")?
+            .ok_or(report!(errors::ApiErrorResponse::Unauthorized))
+            .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");
+        }
+
+        if fallback_merchant_ids
+            .merchant_ids
+            .contains(&stored_api_key.merchant_id)
+        {
+            return Ok((
+                (),
+                AuthenticationType::ApiKey {
+                    merchant_id: stored_api_key.merchant_id,
+                    key_id: stored_api_key.key_id,
+                },
+            ));
+        }
+        Err(report!(errors::ApiErrorResponse::Unauthorized)
+            .attach_printable("Admin Authentication Failure"))
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute(pub id_type::MerchantId);
+
+#[cfg(feature = "v1")]
+impl AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute {
+    async fn fetch_merchant_key_store_and_account<A: SessionStateInfo + Sync>(
+        merchant_id: &id_type::MerchantId,
+        state: &A,
+    ) -> RouterResult<(domain::MerchantKeyStore, domain::MerchantAccount)> {
+        let key_manager_state = &(&state.session_state()).into();
+        let key_store = state
+            .store()
+            .get_merchant_key_store_by_merchant_id(
+                key_manager_state,
+                merchant_id,
+                &state.store().get_master_key().to_vec().into(),
+            )
+            .await
+            .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?;
+
+        let merchant = state
+            .store()
+            .find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
+            .await
+            .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?;
+
+        Ok((key_store, merchant))
+    }
+}
+
+#[cfg(feature = "v1")]
+#[async_trait]
+impl<A> AuthenticateAndFetch<AuthenticationData, A>
+    for AdminApiAuthWithApiKeyFallbackAndMerchantIdFromRoute
+where
+    A: SessionStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &A,
+    ) -> RouterResult<(AuthenticationData, AuthenticationType)> {
+        let merchant_id_from_route: id_type::MerchantId = self.0.clone();
+        let request_api_key =
+            get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
+        let conf = state.conf();
+
+        let admin_api_key: &masking::Secret<String> = &conf.secrets.get_inner().admin_api_key;
+
+        if request_api_key == admin_api_key.peek() {
+            let (key_store, merchant) =
+                Self::fetch_merchant_key_store_and_account(&merchant_id_from_route, state).await?;
+            let auth = AuthenticationData {
+                merchant_account: merchant,
+                platform_merchant_account: None,
+                key_store,
+                profile_id: None,
+            };
+            return Ok((
+                auth,
+                AuthenticationType::AdminApiAuthWithMerchantId {
+                    merchant_id: merchant_id_from_route.clone(),
+                },
+            ));
+        }
+        let Some(fallback_merchant_ids) = conf.fallback_merchant_ids_api_key_auth.as_ref() else {
+            return Err(report!(errors::ApiErrorResponse::Unauthorized)).attach_printable(
+                "Api Key Authentication Failure: fallback merchant set not configured",
+            );
+        };
+
+        let api_key = api_keys::PlaintextApiKey::from(request_api_key);
+        let hash_key = conf.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)
+            .attach_printable("Failed to retrieve API key")?
+            .ok_or(report!(errors::ApiErrorResponse::Unauthorized))
+            .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");
+        }
+
+        if fallback_merchant_ids
+            .merchant_ids
+            .contains(&stored_api_key.merchant_id)
+        {
+            let (_, api_key_merchant) =
+                Self::fetch_merchant_key_store_and_account(&stored_api_key.merchant_id, state)
+                    .await?;
+            let (route_key_store, route_merchant) =
+                Self::fetch_merchant_key_store_and_account(&merchant_id_from_route, state).await?;
+            if api_key_merchant.get_org_id() == route_merchant.get_org_id() {
+                let auth = AuthenticationData {
+                    merchant_account: route_merchant,
+                    platform_merchant_account: None,
+                    key_store: route_key_store,
+                    profile_id: None,
+                };
+                return Ok((
+                    auth.clone(),
+                    AuthenticationType::MerchantId {
+                        merchant_id: auth.merchant_account.get_id().clone(),
+                    },
+                ));
+            }
+        }
+
+        Err(report!(errors::ApiErrorResponse::Unauthorized)
+            .attach_printable("Admin Authentication Failure"))
+    }
+}
+
 /// A helper struct to extract headers from the request
 pub(crate) struct HeaderMapStruct<'a> {
     headers: &'a HeaderMap,



## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Hotfix PR
Original PR: [https://github.com/juspay/hyperswitch/pull/7744](https://github.com/juspay/hyperswitch/pull/7744)

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [ ] 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
