diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index decbf9fd12..f6d47f9a63 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 6803b79d6c..505cee0c4a 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 c4beb0fe9a..9e7f676c09 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 e2224da29e..675b395180 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, } #[cfg(feature = "olap")] @@ -132,6 +133,7 @@ pub struct SinglePurposeToken { pub user_id: String, pub purpose: TokenPurpose, pub origin: domain::Origin, + pub path: Vec, pub exp: u64, } @@ -142,6 +144,7 @@ impl SinglePurposeToken { purpose: TokenPurpose, origin: domain::Origin, settings: &Settings, + path: Vec, ) -> UserResult { 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 5ff79b7fee..4ad25f003d 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 2ee9b38978..ef73b88015 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 { + async fn is_required( + &self, + user: &UserFromStorage, + path: &[TokenPurpose], + state: &SessionState, + ) -> UserResult { 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 { + async fn is_required( + &self, + user: &UserFromStorage, + path: &[TokenPurpose], + state: &SessionState, + ) -> UserResult { 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, } impl CurrentFlow { - pub fn new(origin: Origin, current_flow: UserFlow) -> UserResult { - let flows = origin.get_flows(); + pub fn new( + token: auth::UserFromSinglePurposeToken, + current_flow: UserFlow, + ) -> UserResult { + let flows = token.origin.get_flows(); let index = flows .iter() .position(|flow| flow == ¤t_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 { + pub async fn next(self, user: UserFromStorage, state: &SessionState) -> UserResult { 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, } impl NextFlow { @@ -214,12 +253,14 @@ impl NextFlow { state: &SessionState, ) -> UserResult { 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 for TokenPurpose { impl From 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,