Bug: feat(users): Decision manager changes for SSO Flows



Because SSO needs new flows and will affect other login flows and email flows like accept invite, we have to change some flows in Decision Manager for SSO flows.
diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs
index decbf9fd12a..f6d47f9a638 100644
--- a/crates/common_enums/src/enums.rs
+++ b/crates/common_enums/src/enums.rs
@@ -2758,6 +2758,10 @@ pub enum BankHolderType {
 #[strum(serialize_all = "snake_case")]
 #[serde(rename_all = "snake_case")]
 pub enum TokenPurpose {
+    AuthSelect,
+    #[serde(rename = "sso")]
+    #[strum(serialize = "sso")]
+    SSO,
     #[serde(rename = "totp")]
     #[strum(serialize = "totp")]
     TOTP,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 6803b79d6ce..505cee0c4ac 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -1048,7 +1048,7 @@ pub async fn accept_invite_from_email_token_only_flow(
         .map_err(|e| logger::error!(?e));
 
     let current_flow = domain::CurrentFlow::new(
-        user_token.origin,
+        user_token,
         domain::SPTFlow::AcceptInvitationFromEmail.into(),
     )?;
     let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
@@ -1502,8 +1502,7 @@ pub async fn verify_email_token_only_flow(
         .await
         .map_err(|e| logger::error!(?e));
 
-    let current_flow =
-        domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::VerifyEmail.into())?;
+    let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::VerifyEmail.into())?;
     let next_flow = current_flow.next(user_from_db, &state).await?;
     let token = next_flow.get_token(&state).await?;
 
@@ -1959,7 +1958,7 @@ pub async fn terminate_two_factor_auth(
         }
     }
 
-    let current_flow = domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::TOTP.into())?;
+    let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::TOTP.into())?;
     let next_flow = current_flow.next(user_from_db, &state).await?;
     let token = next_flow.get_token(&state).await?;
 
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index c4beb0fe9a4..9e7f676c095 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -288,7 +288,7 @@ pub async fn merchant_select_token_only_flow(
         .into();
 
     let current_flow =
-        domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::MerchantSelect.into())?;
+        domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?;
     let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
 
     let token = next_flow
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index e2224da29ea..675b3951806 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -124,6 +124,7 @@ impl AuthenticationType {
 pub struct UserFromSinglePurposeToken {
     pub user_id: String,
     pub origin: domain::Origin,
+    pub path: Vec<TokenPurpose>,
 }
 
 #[cfg(feature = "olap")]
@@ -132,6 +133,7 @@ pub struct SinglePurposeToken {
     pub user_id: String,
     pub purpose: TokenPurpose,
     pub origin: domain::Origin,
+    pub path: Vec<TokenPurpose>,
     pub exp: u64,
 }
 
@@ -142,6 +144,7 @@ impl SinglePurposeToken {
         purpose: TokenPurpose,
         origin: domain::Origin,
         settings: &Settings,
+        path: Vec<TokenPurpose>,
     ) -> UserResult<String> {
         let exp_duration =
             std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS);
@@ -151,6 +154,7 @@ impl SinglePurposeToken {
             purpose,
             origin,
             exp,
+            path,
         };
         jwt::generate_jwt(&token_payload, settings).await
     }
@@ -356,6 +360,7 @@ where
             UserFromSinglePurposeToken {
                 user_id: payload.user_id.clone(),
                 origin: payload.origin.clone(),
+                path: payload.path,
             },
             AuthenticationType::SinglePurposeJwt {
                 user_id: payload.user_id,
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 5ff79b7feee..4ad25f003d1 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -1107,6 +1107,7 @@ impl SignInWithMultipleRolesStrategy {
                     TokenPurpose::AcceptInvite,
                     Origin::SignIn,
                     &state.conf,
+                    vec![],
                 )
                 .await?
                 .into(),
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
index 2ee9b389788..ef73b88015f 100644
--- a/crates/router/src/types/domain/user/decision_manager.rs
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -17,9 +17,14 @@ pub enum UserFlow {
 }
 
 impl UserFlow {
-    async fn is_required(&self, user: &UserFromStorage, state: &SessionState) -> UserResult<bool> {
+    async fn is_required(
+        &self,
+        user: &UserFromStorage,
+        path: &[TokenPurpose],
+        state: &SessionState,
+    ) -> UserResult<bool> {
         match self {
-            Self::SPTFlow(flow) => flow.is_required(user, state).await,
+            Self::SPTFlow(flow) => flow.is_required(user, path, state).await,
             Self::JWTFlow(flow) => flow.is_required(user, state).await,
         }
     }
@@ -27,6 +32,8 @@ impl UserFlow {
 
 #[derive(Eq, PartialEq, Clone, Copy)]
 pub enum SPTFlow {
+    AuthSelect,
+    SSO,
     TOTP,
     VerifyEmail,
     AcceptInvitationFromEmail,
@@ -36,15 +43,26 @@ pub enum SPTFlow {
 }
 
 impl SPTFlow {
-    async fn is_required(&self, user: &UserFromStorage, state: &SessionState) -> UserResult<bool> {
+    async fn is_required(
+        &self,
+        user: &UserFromStorage,
+        path: &[TokenPurpose],
+        state: &SessionState,
+    ) -> UserResult<bool> {
         match self {
+            // Auth
+            // AuthSelect and SSO flow are not enabled, once the terminate SSO API is ready, we can enable these flows
+            Self::AuthSelect => Ok(false),
+            Self::SSO => Ok(false),
             // TOTP
-            Self::TOTP => Ok(true),
+            Self::TOTP => Ok(!path.contains(&TokenPurpose::SSO)),
             // Main email APIs
             Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true),
             Self::VerifyEmail => Ok(true),
             // Final Checks
-            Self::ForceSetPassword => user.is_password_rotate_required(state),
+            Self::ForceSetPassword => user
+                .is_password_rotate_required(state)
+                .map(|rotate_required| rotate_required && !path.contains(&TokenPurpose::SSO)),
             Self::MerchantSelect => user
                 .get_roles_from_db(state)
                 .await
@@ -62,6 +80,7 @@ impl SPTFlow {
             self.into(),
             next_flow.origin.clone(),
             &state.conf,
+            next_flow.path.to_vec(),
         )
         .await
         .map(|token| token.into())
@@ -103,6 +122,8 @@ impl JWTFlow {
 #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum Origin {
+    #[serde(rename = "sign_in_with_sso")]
+    SignInWithSSO,
     SignIn,
     SignUp,
     MagicLink,
@@ -114,6 +135,7 @@ pub enum Origin {
 impl Origin {
     fn get_flows(&self) -> &'static [UserFlow] {
         match self {
+            Self::SignInWithSSO => &SIGNIN_WITH_SSO_FLOW,
             Self::SignIn => &SIGNIN_FLOW,
             Self::SignUp => &SIGNUP_FLOW,
             Self::VerifyEmail => &VERIFY_EMAIL_FLOW,
@@ -124,6 +146,11 @@ impl Origin {
     }
 }
 
+const SIGNIN_WITH_SSO_FLOW: [UserFlow; 2] = [
+    UserFlow::SPTFlow(SPTFlow::MerchantSelect),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
 const SIGNIN_FLOW: [UserFlow; 4] = [
     UserFlow::SPTFlow(SPTFlow::TOTP),
     UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
@@ -154,7 +181,9 @@ const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [
     UserFlow::JWTFlow(JWTFlow::UserInfo),
 ];
 
-const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 4] = [
+const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 6] = [
+    UserFlow::SPTFlow(SPTFlow::AuthSelect),
+    UserFlow::SPTFlow(SPTFlow::SSO),
     UserFlow::SPTFlow(SPTFlow::TOTP),
     UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail),
     UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
@@ -169,31 +198,40 @@ const RESET_PASSWORD_FLOW: [UserFlow; 2] = [
 pub struct CurrentFlow {
     origin: Origin,
     current_flow_index: usize,
+    path: Vec<TokenPurpose>,
 }
 
 impl CurrentFlow {
-    pub fn new(origin: Origin, current_flow: UserFlow) -> UserResult<Self> {
-        let flows = origin.get_flows();
+    pub fn new(
+        token: auth::UserFromSinglePurposeToken,
+        current_flow: UserFlow,
+    ) -> UserResult<Self> {
+        let flows = token.origin.get_flows();
         let index = flows
             .iter()
             .position(|flow| flow == &current_flow)
             .ok_or(UserErrors::InternalServerError)?;
+        let mut path = token.path;
+        path.push(current_flow.into());
 
         Ok(Self {
-            origin,
+            origin: token.origin,
             current_flow_index: index,
+            path,
         })
     }
 
-    pub async fn next(&self, user: UserFromStorage, state: &SessionState) -> UserResult<NextFlow> {
+    pub async fn next(self, user: UserFromStorage, state: &SessionState) -> UserResult<NextFlow> {
         let flows = self.origin.get_flows();
         let remaining_flows = flows.iter().skip(self.current_flow_index + 1);
+
         for flow in remaining_flows {
-            if flow.is_required(&user, state).await? {
+            if flow.is_required(&user, &self.path, state).await? {
                 return Ok(NextFlow {
                     origin: self.origin.clone(),
                     next_flow: *flow,
                     user,
+                    path: self.path,
                 });
             }
         }
@@ -205,6 +243,7 @@ pub struct NextFlow {
     origin: Origin,
     next_flow: UserFlow,
     user: UserFromStorage,
+    path: Vec<TokenPurpose>,
 }
 
 impl NextFlow {
@@ -214,12 +253,14 @@ impl NextFlow {
         state: &SessionState,
     ) -> UserResult<Self> {
         let flows = origin.get_flows();
+        let path = vec![];
         for flow in flows {
-            if flow.is_required(&user, state).await? {
+            if flow.is_required(&user, &path, state).await? {
                 return Ok(Self {
                     origin,
                     next_flow: *flow,
                     user,
+                    path,
                 });
             }
         }
@@ -284,6 +325,8 @@ impl From<UserFlow> for TokenPurpose {
 impl From<SPTFlow> for TokenPurpose {
     fn from(value: SPTFlow) -> Self {
         match value {
+            SPTFlow::AuthSelect => Self::AuthSelect,
+            SPTFlow::SSO => Self::SSO,
             SPTFlow::TOTP => Self::TOTP,
             SPTFlow::VerifyEmail => Self::VerifyEmail,
             SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail,

## 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 -->
This PR adds SSO flows in login decision flows. Currently they are disabled as we have no APIs for SSO and will be enabled in upcoming PRs which adds SSO APIs.

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


## 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)?
-->
- There should be no changes in any of the flows.
- SPT changes can be tested as follows:
	1. Generate SPT using any of the token only APIs (I am using Sign in)
		```curl
		curl --location 'http://localhost:8080/user/v2/signin?token_only=true' \
		--header 'Content-Type: application/json' \
		--data-raw '{
		    "email": "email",
		    "password": "password"
		}'
		```
		```
		{
		    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZTY4YzFmZTQtODRiMC00Y2NjLWI4NGYtN2VjYmVkNjg5Y2Q5IiwicHVycG9zZSI6InRvdHAiLCJvcmlnaW4iOiJzaWduX2luIiwicGF0aCI6W10sImV4cCI6MTcxODg3MzYyN30.HK_0mcCyvbFwh-nma48Pbeh2kdkO7s3FsVOtIc6UF9w",
		    "token_type": "totp"
		}
		```
	2. Complete the next step
		```
		curl --location 'http://localhost:8080/user/2fa/terminate?skip_two_factor_auth=true' \
		--header 'Authorization: SPT with Purpose as `totp`' \
		```
		```
		{
		    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMzBkOGVkYTEtOWM2ZC00MjgwLTg3MDgtN2MwMjBjY2U1ZmNkIiwicHVycG9zZSI6ImFjY2VwdF9pbnZpdGF0aW9uX2Zyb21fZW1haWwiLCJvcmlnaW4iOiJhY2NlcHRfaW52aXRhdGlvbl9mcm9tX2VtYWlsIiwicGF0aCI6WyJ0b3RwIl0sImV4cCI6MTcxODg3OTQ3OH0.tLsVZ-kYbaxaXaq3Jn4-kJ0ghswonTCjbclUrWZLeoc",
		    "token_type": "accept_invitation_from_email"
		}
		```

		The above token when decoded (using [this](https://jwt.io/)), it should have the `path` field with the step that has been completed. And the SPT body will look something like this:
		```
		{
		  "user_id": "30d8eda1-9c6d-4280-8708-7c020cce5fcd",
		  "purpose": "accept_invitation_from_email",
		  "origin": "accept_invitation_from_email",
		  "path": [
		    "totp"
		  ],
		  "exp": 1718879478
		}
		```

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