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")]
|
#[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