Bug: feat: Add config to force 2FA for users



Currently 2FA is skippable in all the environments.

We need to force 2FA in prod later at some point, so there should be config to which forces 2FA is enabled.
diff --git a/config/config.example.toml b/config/config.example.toml
index b0d3c743673..5aa45af85ac 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -395,6 +395,7 @@ password_validity_in_days = 90       # Number of days after which password shoul
 two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should be done again if doing update/change from inside
 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
 
 #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 780af87383f..b99577c7f06 100644
--- a/config/deployments/integration_test.toml
+++ b/config/deployments/integration_test.toml
@@ -138,6 +138,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Integ"
 base_url = "https://integ.hyperswitch.io"
+force_two_factor_auth = false
 
 [frm]
 enabled = true
@@ -394,4 +395,4 @@ connector_list = ""
 card_networks = "Visa, AmericanExpress, Mastercard"
 
 [network_tokenization_supported_connectors]
-connector_list = "cybersource"
\ No newline at end of file
+connector_list = "cybersource"
diff --git a/config/deployments/production.toml b/config/deployments/production.toml
index 430475b9e5c..5aaae800e32 100644
--- a/config/deployments/production.toml
+++ b/config/deployments/production.toml
@@ -145,6 +145,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Production"
 base_url = "https://live.hyperswitch.io"
+force_two_factor_auth = false
 
 [frm]
 enabled = false
@@ -408,4 +409,4 @@ connector_list = ""
 card_networks = "Visa, AmericanExpress, Mastercard"
 
 [network_tokenization_supported_connectors]
-connector_list = "cybersource"
\ No newline at end of file
+connector_list = "cybersource"
diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml
index 99d4253aea3..d6868eb94b1 100644
--- a/config/deployments/sandbox.toml
+++ b/config/deployments/sandbox.toml
@@ -145,6 +145,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Sandbox"
 base_url = "https://app.hyperswitch.io"
+force_two_factor_auth = false
 
 [frm]
 enabled = true
diff --git a/config/development.toml b/config/development.toml
index cbc4573e555..18df92797bc 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -316,6 +316,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch Dev"
 base_url = "http://localhost:8080"
+force_two_factor_auth = false
 
 [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 cf07a1bf9be..760d6e370f7 100644
--- a/config/docker_compose.toml
+++ b/config/docker_compose.toml
@@ -56,6 +56,7 @@ password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch"
 base_url = "http://localhost:8080"
+force_two_factor_auth = false
 
 [locker]
 host = ""
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 089089038b8..089426c68ba 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -211,6 +211,7 @@ pub struct TwoFactorAuthStatusResponseWithAttempts {
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct TwoFactorStatus {
     pub status: Option<TwoFactorAuthStatusResponseWithAttempts>,
+    pub is_skippable: bool,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs
index 61e026ae2c5..f675aad11a7 100644
--- a/crates/router/src/configs/settings.rs
+++ b/crates/router/src/configs/settings.rs
@@ -556,6 +556,7 @@ pub struct UserSettings {
     pub two_factor_auth_expiry_in_secs: i64,
     pub totp_issuer_name: String,
     pub base_url: String,
+    pub force_two_factor_auth: bool,
 }
 
 #[derive(Debug, Deserialize, Clone)]
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 822c29b21d9..35b26926ed2 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -1319,7 +1319,7 @@ pub async fn list_user_roles_details(
     ))
     .await
     .change_context(UserErrors::InternalServerError)
-    .attach_printable("Failed to construct proifle map")?
+    .attach_printable("Failed to construct profile map")?
     .into_iter()
     .map(|profile| (profile.get_id().to_owned(), profile.profile_name))
     .collect::<HashMap<_, _>>();
@@ -1927,7 +1927,7 @@ pub async fn terminate_two_factor_auth(
         .change_context(UserErrors::InternalServerError)?
         .into();
 
-    if !skip_two_factor_auth {
+    if state.conf.user.force_two_factor_auth || !skip_two_factor_auth {
         if !tfa_utils::check_totp_in_redis(&state, &user_token.user_id).await?
             && !tfa_utils::check_recovery_code_in_redis(&state, &user_token.user_id).await?
         {
@@ -1997,9 +1997,12 @@ pub async fn check_two_factor_auth_status_with_attempts(
         .await
         .change_context(UserErrors::InternalServerError)?
         .into();
+
+    let is_skippable = state.conf.user.force_two_factor_auth.not();
     if user_from_db.get_totp_status() == TotpStatus::NotSet {
         return Ok(ApplicationResponse::Json(user_api::TwoFactorStatus {
             status: None,
+            is_skippable,
         }));
     };
 
@@ -2018,6 +2021,7 @@ pub async fn check_two_factor_auth_status_with_attempts(
             totp,
             recovery_code,
         }),
+        is_skippable,
     }))
 }
 
diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml
index 11268e15e54..1e9f355a5e6 100644
--- a/loadtest/config/development.toml
+++ b/loadtest/config/development.toml
@@ -35,6 +35,7 @@ jwt_secret = "secret"
 password_validity_in_days = 90
 two_factor_auth_expiry_in_secs = 300
 totp_issuer_name = "Hyperswitch"
+force_two_factor_auth = false
 
 [locker]
 host = ""

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

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

## Description
<!-- Describe your changes in detail -->
This PR will add force 2fa environment variable. When enabled it will force users to complete 2FA to access the control center.

### Additional Changes

- [x] 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 #4774.

## 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)?
-->
1. 2FA Status API
  - `is_skippable` field is added in the response.
    ```
    curl --location 'http://localhost:8080/user/2fa/v2' \
    --header 'Authorization: JWT' \
    ```
    ```
    {
        "status": null,
        "is_skippable": true
    }
    ```
2. Terminate 2FA API
  - This API will now not allow skip 2FA if the `force_two_factor_auth`
    ```
    curl --location 'http://localhost:8080/user/2fa/terminate?skip_two_factor_auth=true' \
    --header 'Authorization: JWT' \
    ```
    ```
    {
        "error": {
            "type": "invalid_request",
            "message": "Two factor auth required",
            "code": "UR_40"
        }
    }
    ```

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