mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 01:57:45 +08:00 
			
		
		
		
	feat(users): Decision manager flow changes for SSO (#4995)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -2758,6 +2758,10 @@ pub enum BankHolderType { | |||||||
| #[strum(serialize_all = "snake_case")] | #[strum(serialize_all = "snake_case")] | ||||||
| #[serde(rename_all = "snake_case")] | #[serde(rename_all = "snake_case")] | ||||||
| pub enum TokenPurpose { | pub enum TokenPurpose { | ||||||
|  |     AuthSelect, | ||||||
|  |     #[serde(rename = "sso")] | ||||||
|  |     #[strum(serialize = "sso")] | ||||||
|  |     SSO, | ||||||
|     #[serde(rename = "totp")] |     #[serde(rename = "totp")] | ||||||
|     #[strum(serialize = "totp")] |     #[strum(serialize = "totp")] | ||||||
|     TOTP, |     TOTP, | ||||||
|  | |||||||
| @ -1048,7 +1048,7 @@ pub async fn accept_invite_from_email_token_only_flow( | |||||||
|         .map_err(|e| logger::error!(?e)); |         .map_err(|e| logger::error!(?e)); | ||||||
|  |  | ||||||
|     let current_flow = domain::CurrentFlow::new( |     let current_flow = domain::CurrentFlow::new( | ||||||
|         user_token.origin, |         user_token, | ||||||
|         domain::SPTFlow::AcceptInvitationFromEmail.into(), |         domain::SPTFlow::AcceptInvitationFromEmail.into(), | ||||||
|     )?; |     )?; | ||||||
|     let next_flow = current_flow.next(user_from_db.clone(), &state).await?; |     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 |         .await | ||||||
|         .map_err(|e| logger::error!(?e)); |         .map_err(|e| logger::error!(?e)); | ||||||
|  |  | ||||||
|     let current_flow = |     let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::VerifyEmail.into())?; | ||||||
|         domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::VerifyEmail.into())?; |  | ||||||
|     let next_flow = current_flow.next(user_from_db, &state).await?; |     let next_flow = current_flow.next(user_from_db, &state).await?; | ||||||
|     let token = next_flow.get_token(&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 next_flow = current_flow.next(user_from_db, &state).await?; | ||||||
|     let token = next_flow.get_token(&state).await?; |     let token = next_flow.get_token(&state).await?; | ||||||
|  |  | ||||||
|  | |||||||
| @ -288,7 +288,7 @@ pub async fn merchant_select_token_only_flow( | |||||||
|         .into(); |         .into(); | ||||||
|  |  | ||||||
|     let current_flow = |     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 next_flow = current_flow.next(user_from_db.clone(), &state).await?; | ||||||
|  |  | ||||||
|     let token = next_flow |     let token = next_flow | ||||||
|  | |||||||
| @ -124,6 +124,7 @@ impl AuthenticationType { | |||||||
| pub struct UserFromSinglePurposeToken { | pub struct UserFromSinglePurposeToken { | ||||||
|     pub user_id: String, |     pub user_id: String, | ||||||
|     pub origin: domain::Origin, |     pub origin: domain::Origin, | ||||||
|  |     pub path: Vec<TokenPurpose>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[cfg(feature = "olap")] | #[cfg(feature = "olap")] | ||||||
| @ -132,6 +133,7 @@ pub struct SinglePurposeToken { | |||||||
|     pub user_id: String, |     pub user_id: String, | ||||||
|     pub purpose: TokenPurpose, |     pub purpose: TokenPurpose, | ||||||
|     pub origin: domain::Origin, |     pub origin: domain::Origin, | ||||||
|  |     pub path: Vec<TokenPurpose>, | ||||||
|     pub exp: u64, |     pub exp: u64, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -142,6 +144,7 @@ impl SinglePurposeToken { | |||||||
|         purpose: TokenPurpose, |         purpose: TokenPurpose, | ||||||
|         origin: domain::Origin, |         origin: domain::Origin, | ||||||
|         settings: &Settings, |         settings: &Settings, | ||||||
|  |         path: Vec<TokenPurpose>, | ||||||
|     ) -> UserResult<String> { |     ) -> UserResult<String> { | ||||||
|         let exp_duration = |         let exp_duration = | ||||||
|             std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS); |             std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS); | ||||||
| @ -151,6 +154,7 @@ impl SinglePurposeToken { | |||||||
|             purpose, |             purpose, | ||||||
|             origin, |             origin, | ||||||
|             exp, |             exp, | ||||||
|  |             path, | ||||||
|         }; |         }; | ||||||
|         jwt::generate_jwt(&token_payload, settings).await |         jwt::generate_jwt(&token_payload, settings).await | ||||||
|     } |     } | ||||||
| @ -356,6 +360,7 @@ where | |||||||
|             UserFromSinglePurposeToken { |             UserFromSinglePurposeToken { | ||||||
|                 user_id: payload.user_id.clone(), |                 user_id: payload.user_id.clone(), | ||||||
|                 origin: payload.origin.clone(), |                 origin: payload.origin.clone(), | ||||||
|  |                 path: payload.path, | ||||||
|             }, |             }, | ||||||
|             AuthenticationType::SinglePurposeJwt { |             AuthenticationType::SinglePurposeJwt { | ||||||
|                 user_id: payload.user_id, |                 user_id: payload.user_id, | ||||||
|  | |||||||
| @ -1107,6 +1107,7 @@ impl SignInWithMultipleRolesStrategy { | |||||||
|                     TokenPurpose::AcceptInvite, |                     TokenPurpose::AcceptInvite, | ||||||
|                     Origin::SignIn, |                     Origin::SignIn, | ||||||
|                     &state.conf, |                     &state.conf, | ||||||
|  |                     vec![], | ||||||
|                 ) |                 ) | ||||||
|                 .await? |                 .await? | ||||||
|                 .into(), |                 .into(), | ||||||
|  | |||||||
| @ -17,9 +17,14 @@ pub enum UserFlow { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl 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 { |         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, |             Self::JWTFlow(flow) => flow.is_required(user, state).await, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -27,6 +32,8 @@ impl UserFlow { | |||||||
|  |  | ||||||
| #[derive(Eq, PartialEq, Clone, Copy)] | #[derive(Eq, PartialEq, Clone, Copy)] | ||||||
| pub enum SPTFlow { | pub enum SPTFlow { | ||||||
|  |     AuthSelect, | ||||||
|  |     SSO, | ||||||
|     TOTP, |     TOTP, | ||||||
|     VerifyEmail, |     VerifyEmail, | ||||||
|     AcceptInvitationFromEmail, |     AcceptInvitationFromEmail, | ||||||
| @ -36,15 +43,26 @@ pub enum SPTFlow { | |||||||
| } | } | ||||||
|  |  | ||||||
| impl 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 { |         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 |             // TOTP | ||||||
|             Self::TOTP => Ok(true), |             Self::TOTP => Ok(!path.contains(&TokenPurpose::SSO)), | ||||||
|             // Main email APIs |             // Main email APIs | ||||||
|             Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true), |             Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true), | ||||||
|             Self::VerifyEmail => Ok(true), |             Self::VerifyEmail => Ok(true), | ||||||
|             // Final Checks |             // 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 |             Self::MerchantSelect => user | ||||||
|                 .get_roles_from_db(state) |                 .get_roles_from_db(state) | ||||||
|                 .await |                 .await | ||||||
| @ -62,6 +80,7 @@ impl SPTFlow { | |||||||
|             self.into(), |             self.into(), | ||||||
|             next_flow.origin.clone(), |             next_flow.origin.clone(), | ||||||
|             &state.conf, |             &state.conf, | ||||||
|  |             next_flow.path.to_vec(), | ||||||
|         ) |         ) | ||||||
|         .await |         .await | ||||||
|         .map(|token| token.into()) |         .map(|token| token.into()) | ||||||
| @ -103,6 +122,8 @@ impl JWTFlow { | |||||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||||
| #[serde(rename_all = "snake_case")] | #[serde(rename_all = "snake_case")] | ||||||
| pub enum Origin { | pub enum Origin { | ||||||
|  |     #[serde(rename = "sign_in_with_sso")] | ||||||
|  |     SignInWithSSO, | ||||||
|     SignIn, |     SignIn, | ||||||
|     SignUp, |     SignUp, | ||||||
|     MagicLink, |     MagicLink, | ||||||
| @ -114,6 +135,7 @@ pub enum Origin { | |||||||
| impl Origin { | impl Origin { | ||||||
|     fn get_flows(&self) -> &'static [UserFlow] { |     fn get_flows(&self) -> &'static [UserFlow] { | ||||||
|         match self { |         match self { | ||||||
|  |             Self::SignInWithSSO => &SIGNIN_WITH_SSO_FLOW, | ||||||
|             Self::SignIn => &SIGNIN_FLOW, |             Self::SignIn => &SIGNIN_FLOW, | ||||||
|             Self::SignUp => &SIGNUP_FLOW, |             Self::SignUp => &SIGNUP_FLOW, | ||||||
|             Self::VerifyEmail => &VERIFY_EMAIL_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] = [ | const SIGNIN_FLOW: [UserFlow; 4] = [ | ||||||
|     UserFlow::SPTFlow(SPTFlow::TOTP), |     UserFlow::SPTFlow(SPTFlow::TOTP), | ||||||
|     UserFlow::SPTFlow(SPTFlow::ForceSetPassword), |     UserFlow::SPTFlow(SPTFlow::ForceSetPassword), | ||||||
| @ -154,7 +181,9 @@ const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [ | |||||||
|     UserFlow::JWTFlow(JWTFlow::UserInfo), |     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::TOTP), | ||||||
|     UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail), |     UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail), | ||||||
|     UserFlow::SPTFlow(SPTFlow::ForceSetPassword), |     UserFlow::SPTFlow(SPTFlow::ForceSetPassword), | ||||||
| @ -169,31 +198,40 @@ const RESET_PASSWORD_FLOW: [UserFlow; 2] = [ | |||||||
| pub struct CurrentFlow { | pub struct CurrentFlow { | ||||||
|     origin: Origin, |     origin: Origin, | ||||||
|     current_flow_index: usize, |     current_flow_index: usize, | ||||||
|  |     path: Vec<TokenPurpose>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl CurrentFlow { | impl CurrentFlow { | ||||||
|     pub fn new(origin: Origin, current_flow: UserFlow) -> UserResult<Self> { |     pub fn new( | ||||||
|         let flows = origin.get_flows(); |         token: auth::UserFromSinglePurposeToken, | ||||||
|  |         current_flow: UserFlow, | ||||||
|  |     ) -> UserResult<Self> { | ||||||
|  |         let flows = token.origin.get_flows(); | ||||||
|         let index = flows |         let index = flows | ||||||
|             .iter() |             .iter() | ||||||
|             .position(|flow| flow == ¤t_flow) |             .position(|flow| flow == ¤t_flow) | ||||||
|             .ok_or(UserErrors::InternalServerError)?; |             .ok_or(UserErrors::InternalServerError)?; | ||||||
|  |         let mut path = token.path; | ||||||
|  |         path.push(current_flow.into()); | ||||||
|  |  | ||||||
|         Ok(Self { |         Ok(Self { | ||||||
|             origin, |             origin: token.origin, | ||||||
|             current_flow_index: index, |             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 flows = self.origin.get_flows(); | ||||||
|         let remaining_flows = flows.iter().skip(self.current_flow_index + 1); |         let remaining_flows = flows.iter().skip(self.current_flow_index + 1); | ||||||
|  |  | ||||||
|         for flow in remaining_flows { |         for flow in remaining_flows { | ||||||
|             if flow.is_required(&user, state).await? { |             if flow.is_required(&user, &self.path, state).await? { | ||||||
|                 return Ok(NextFlow { |                 return Ok(NextFlow { | ||||||
|                     origin: self.origin.clone(), |                     origin: self.origin.clone(), | ||||||
|                     next_flow: *flow, |                     next_flow: *flow, | ||||||
|                     user, |                     user, | ||||||
|  |                     path: self.path, | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -205,6 +243,7 @@ pub struct NextFlow { | |||||||
|     origin: Origin, |     origin: Origin, | ||||||
|     next_flow: UserFlow, |     next_flow: UserFlow, | ||||||
|     user: UserFromStorage, |     user: UserFromStorage, | ||||||
|  |     path: Vec<TokenPurpose>, | ||||||
| } | } | ||||||
|  |  | ||||||
| impl NextFlow { | impl NextFlow { | ||||||
| @ -214,12 +253,14 @@ impl NextFlow { | |||||||
|         state: &SessionState, |         state: &SessionState, | ||||||
|     ) -> UserResult<Self> { |     ) -> UserResult<Self> { | ||||||
|         let flows = origin.get_flows(); |         let flows = origin.get_flows(); | ||||||
|  |         let path = vec![]; | ||||||
|         for flow in flows { |         for flow in flows { | ||||||
|             if flow.is_required(&user, state).await? { |             if flow.is_required(&user, &path, state).await? { | ||||||
|                 return Ok(Self { |                 return Ok(Self { | ||||||
|                     origin, |                     origin, | ||||||
|                     next_flow: *flow, |                     next_flow: *flow, | ||||||
|                     user, |                     user, | ||||||
|  |                     path, | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -284,6 +325,8 @@ impl From<UserFlow> for TokenPurpose { | |||||||
| impl From<SPTFlow> for TokenPurpose { | impl From<SPTFlow> for TokenPurpose { | ||||||
|     fn from(value: SPTFlow) -> Self { |     fn from(value: SPTFlow) -> Self { | ||||||
|         match value { |         match value { | ||||||
|  |             SPTFlow::AuthSelect => Self::AuthSelect, | ||||||
|  |             SPTFlow::SSO => Self::SSO, | ||||||
|             SPTFlow::TOTP => Self::TOTP, |             SPTFlow::TOTP => Self::TOTP, | ||||||
|             SPTFlow::VerifyEmail => Self::VerifyEmail, |             SPTFlow::VerifyEmail => Self::VerifyEmail, | ||||||
|             SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail, |             SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail, | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Mani Chandra
					Mani Chandra