Bug: feat(authn): Use cookies for authentication



Currently we use auth headers and local storage in FE for transfer JWT from FE to BE.

Cookies is a better way to do this, as it is handled by browser and JavaScript doesn't have access to.

First we will test this in Integ and slowly move it to sandbox and production.
diff --git a/config/config.example.toml b/config/config.example.toml
index 191f2ba7f8b..52b704e1adc 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -403,6 +403,7 @@ two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should
 totp_issuer_name = "Hyperswitch"     # Name of the issuer for TOTP
 base_url = ""                        # Base url used for user specific redirects and emails
 force_two_factor_auth = false        # Whether to force two factor authentication for all users
+force_cookies = true                 # Whether to use only cookies for JWT extraction and authentication
 
 #tokenization configuration which describe token lifetime and payment method for specific connector
 [tokenization]
diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml
index 00a544dc565..0765591c63b 100644
--- a/config/deployments/integration_test.toml
+++ b/config/deployments/integration_test.toml
@@ -145,6 +145,7 @@ two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Integ"
 base_url = "https://integ.hyperswitch.io"
 force_two_factor_auth = false
+force_cookies = true
 
 [frm]
 enabled = true
diff --git a/config/deployments/production.toml b/config/deployments/production.toml
index 0fe9095d280..d9d3e852d1b 100644
--- a/config/deployments/production.toml
+++ b/config/deployments/production.toml
@@ -152,6 +152,7 @@ two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Production"
 base_url = "https://live.hyperswitch.io"
 force_two_factor_auth = true
+force_cookies = false
 
 [frm]
 enabled = false
diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml
index 82c347ae389..b3b57e9d202 100644
--- a/config/deployments/sandbox.toml
+++ b/config/deployments/sandbox.toml
@@ -152,6 +152,7 @@ two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Sandbox"
 base_url = "https://app.hyperswitch.io"
 force_two_factor_auth = false
+force_cookies = false
 
 [frm]
 enabled = true
diff --git a/config/development.toml b/config/development.toml
index ee6ea5dab0b..0c4d92fc519 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -329,6 +329,7 @@ two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Dev"
 base_url = "http://localhost:8080"
 force_two_factor_auth = false
+force_cookies = true
 
 [bank_config.eps]
 stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
diff --git a/config/docker_compose.toml b/config/docker_compose.toml
index ed0ede98d94..f1d02d30345 100644
--- a/config/docker_compose.toml
+++ b/config/docker_compose.toml
@@ -57,6 +57,7 @@ two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch"
 base_url = "http://localhost:8080"
 force_two_factor_auth = false
+force_cookies = true
 
 [locker]
 host = ""
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 7b212ec6d1d..4e559a261b9 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -557,6 +557,7 @@ pub struct UserSettings {
     pub totp_issuer_name: String,
     pub base_url: String,
     pub force_two_factor_auth: bool,
+    pub force_cookies: bool,
 }
 
 #[derive(Debug, Deserialize, Clone)]
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index f01f6c5d749..c6501dac3bd 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -294,7 +294,7 @@ pub async fn connect_account(
 
 pub async fn signout(
     state: SessionState,
-    user_from_token: auth::UserFromToken,
+    user_from_token: auth::UserIdFromAuth,
 ) -> UserResponse<()> {
     tfa_utils::delete_totp_from_redis(&state, &user_from_token.user_id).await?;
     tfa_utils::delete_recovery_code_from_redis(&state, &user_from_token.user_id).await?;
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 068c2f30c79..8fc0dad452a 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -130,7 +130,7 @@ pub async fn signout(state: web::Data<AppState>, http_req: HttpRequest) -> HttpR
         &http_req,
         (),
         |state, user, _, _| user_core::signout(state, user),
-        &auth::DashboardNoPermissionAuth,
+        &auth::AnyPurposeOrLoginTokenAuth,
         api_locking::LockAction::NotApplicable,
     ))
     .await
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 2f5f55b8434..c05e4514aaa 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -871,6 +871,47 @@ where
     }
 }
 
+#[cfg(feature = "olap")]
+#[derive(Debug)]
+pub struct AnyPurposeOrLoginTokenAuth;
+
+#[cfg(feature = "olap")]
+#[async_trait]
+impl<A> AuthenticateAndFetch<UserIdFromAuth, A> for AnyPurposeOrLoginTokenAuth
+where
+    A: SessionStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &A,
+    ) -> RouterResult<(UserIdFromAuth, AuthenticationType)> {
+        let payload =
+            parse_jwt_payload::<A, SinglePurposeOrLoginToken>(request_headers, state).await?;
+        if payload.check_in_blacklist(state).await? {
+            return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
+        }
+
+        let purpose_exists = payload.purpose.is_some();
+        let role_id_exists = payload.role_id.is_some();
+
+        if purpose_exists ^ role_id_exists {
+            Ok((
+                UserIdFromAuth {
+                    user_id: payload.user_id.clone(),
+                },
+                AuthenticationType::SinglePurposeOrLoginJwt {
+                    user_id: payload.user_id,
+                    purpose: payload.purpose,
+                    role_id: payload.role_id,
+                },
+            ))
+        } else {
+            Err(errors::ApiErrorResponse::InvalidJwtToken.into())
+        }
+    }
+}
+
 #[derive(Debug, Default)]
 pub struct AdminApiAuth;
 
@@ -2504,17 +2545,27 @@ where
     T: serde::de::DeserializeOwned,
     A: SessionStateInfo + Sync,
 {
-    let token = match get_cookie_from_header(headers).and_then(cookies::parse_cookie) {
-        Ok(cookies) => cookies,
-        Err(error) => {
-            let token = get_jwt_from_authorization_header(headers);
-            if token.is_err() {
-                logger::error!(?error);
-            }
-            token?.to_owned()
-        }
+    let cookie_token_result = get_cookie_from_header(headers).and_then(cookies::parse_cookie);
+    let auth_header_token_result = get_jwt_from_authorization_header(headers);
+    let force_cookie = state.conf().user.force_cookies;
+
+    logger::info!(
+        user_agent = ?headers.get(headers::USER_AGENT),
+        header_names = ?headers.keys().collect::<Vec<_>>(),
+        is_token_equal =
+            auth_header_token_result.as_deref().ok() == cookie_token_result.as_deref().ok(),
+        cookie_error = ?cookie_token_result.as_ref().err(),
+        token_error = ?auth_header_token_result.as_ref().err(),
+        force_cookie,
+    );
+
+    let final_token = if force_cookie {
+        cookie_token_result?
+    } else {
+        auth_header_token_result?.to_owned()
     };
-    decode_jwt(&token, state).await
+
+    decode_jwt(&final_token, state).await
 }
 
 #[cfg(feature = "v1")]
diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml
index dab85eb3cdd..a3ac1159ddb 100644
--- a/loadtest/config/development.toml
+++ b/loadtest/config/development.toml
@@ -36,6 +36,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch"
 force_two_factor_auth = false
+force_cookies = true
 
 [locker]
 host = ""

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

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

## Description
<!-- Describe your changes in detail -->
This PR will add logging for cookies and also make signout API accessible by any SPT.

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [x] 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`
-->
`config/config.example.toml`
`config/deployments/integration_test.toml`
`config/deployments/production.toml`
`config/deployments/sandbox.toml`
`config/development.toml`
`config/docker_compose.toml`
`loadtest/config/development.toml`



## 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).
-->
Closes #6598.

## 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)?
-->
- This following behaviour applies only to integ and not for sandbox and prod.
```
curl --location 'http://localhost:8080/user' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNzJkNDU0YTAtM2I3YS00MzZiLTllNjMtMmU5ZDQ5YzI3NmZjIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzMxOTI4MDg1Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTczMjEwMDg5MSwib3JnX2lkIjoib3JnX05qeWR3eXhpRW5OQjk2QldobjlwIiwicHJvZmlsZV9pZCI6InByb183bGVMTnpFakVtRWVha1ZKTlJSbSJ9.g7InEVOcYqKSh2zNPBrq20l6O5MppE3-wKqj2YYBkBM'
```
If cookie is not present even if the auth header is present, BE will throw the following error
```
{
    "error": {
        "type": "invalid_request",
        "message": "Invalid Cookie",
        "code": "IR_26"
    }
}
```

- If cookie will be given priority if both are present.

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

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