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:
Mani Chandra
2024-06-24 17:48:50 +05:30
committed by GitHub
parent 9600461916
commit 8ceaaa9e3d
6 changed files with 69 additions and 17 deletions

View File

@ -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,

View File

@ -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?;

View File

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

View File

@ -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,

View File

@ -1107,6 +1107,7 @@ impl SignInWithMultipleRolesStrategy {
TokenPurpose::AcceptInvite, TokenPurpose::AcceptInvite,
Origin::SignIn, Origin::SignIn,
&state.conf, &state.conf,
vec![],
) )
.await? .await?
.into(), .into(),

View File

@ -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 == &current_flow) .position(|flow| flow == &current_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,