mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +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")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TokenPurpose {
|
||||
AuthSelect,
|
||||
#[serde(rename = "sso")]
|
||||
#[strum(serialize = "sso")]
|
||||
SSO,
|
||||
#[serde(rename = "totp")]
|
||||
#[strum(serialize = "totp")]
|
||||
TOTP,
|
||||
|
||||
@ -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?;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1107,6 +1107,7 @@ impl SignInWithMultipleRolesStrategy {
|
||||
TokenPurpose::AcceptInvite,
|
||||
Origin::SignIn,
|
||||
&state.conf,
|
||||
vec![],
|
||||
)
|
||||
.await?
|
||||
.into(),
|
||||
|
||||
@ -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 == ¤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<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,
|
||||
|
||||
Reference in New Issue
Block a user