From 42e5ef155128f4df717e8fb101da6e6929659a0a Mon Sep 17 00:00:00 2001 From: Riddhiagrawal001 <50551695+Riddhiagrawal001@users.noreply.github.com> Date: Thu, 23 May 2024 17:31:35 +0530 Subject: [PATCH] feat(users): Create terminate 2fa API (#4731) --- crates/api_models/src/user.rs | 5 ++ crates/router/src/consts/user.rs | 1 + crates/router/src/core/errors/user.rs | 12 ++++ crates/router/src/core/user.rs | 65 +++++++++++++++++-- crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user.rs | 20 ++++++ crates/router/src/types/domain/user.rs | 4 ++ .../router/src/utils/user/two_factor_auth.rs | 9 +++ crates/router_env/src/logger/types.rs | 2 + 10 files changed, 115 insertions(+), 7 deletions(-) diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 7864117856e6..6b2748ca3442 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -224,6 +224,11 @@ pub struct TokenOnlyQueryParam { pub token_only: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SkipTwoFactorAuthQueryParam { + pub skip_two_factor_auth: Option, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct TokenResponse { pub token: Secret, diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs index 33642205d57f..7b61a834f603 100644 --- a/crates/router/src/consts/user.rs +++ b/crates/router/src/consts/user.rs @@ -14,3 +14,4 @@ pub const MAX_PASSWORD_LENGTH: usize = 70; pub const MIN_PASSWORD_LENGTH: usize = 8; pub const TOTP_PREFIX: &str = "TOTP_"; +pub const REDIS_RECOVERY_CODES_PREFIX: &str = "RC_"; diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 2d1c196f5df7..cda850787184 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -72,6 +72,10 @@ pub enum UserErrors { InvalidTotp, #[error("TotpRequired")] TotpRequired, + #[error("TwoFactorAuthRequired")] + TwoFactorAuthRequired, + #[error("TwoFactorAuthNotSetup")] + TwoFactorAuthNotSetup, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -184,6 +188,12 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 38, self.get_error_message(), None)) } + Self::TwoFactorAuthRequired => { + AER::BadRequest(ApiError::new(sub_code, 39, self.get_error_message(), None)) + } + Self::TwoFactorAuthNotSetup => { + AER::BadRequest(ApiError::new(sub_code, 40, self.get_error_message(), None)) + } } } } @@ -223,6 +233,8 @@ impl UserErrors { Self::TotpNotSetup => "TOTP not setup", Self::InvalidTotp => "Invalid TOTP", Self::TotpRequired => "TOTP required", + Self::TwoFactorAuthRequired => "Two factor auth required", + Self::TwoFactorAuthNotSetup => "Two factor auth not setup", } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7a0ef683e7a6..705c12907ff5 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -24,8 +24,9 @@ use crate::{ routes::{app::ReqState, AppState}, services::{authentication as auth, authorization::roles, ApplicationResponse}, types::{domain, transformers::ForeignInto}, - utils, + utils::{self, user::two_factor_auth as tfa_utils}, }; + pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; @@ -1631,7 +1632,7 @@ pub async fn begin_totp( })); } - let totp = utils::user::two_factor_auth::generate_default_totp(user_from_db.get_email(), None)?; + let totp = tfa_utils::generate_default_totp(user_from_db.get_email(), None)?; let recovery_codes = domain::RecoveryCodes::generate_new(); let key_store = user_from_db.get_or_create_key_store(&state).await?; @@ -1693,10 +1694,8 @@ pub async fn verify_totp( .await? .ok_or(UserErrors::InternalServerError)?; - let totp = utils::user::two_factor_auth::generate_default_totp( - user_from_db.get_email(), - Some(user_totp_secret), - )?; + let totp = + tfa_utils::generate_default_totp(user_from_db.get_email(), Some(user_totp_secret))?; if totp .generate_current() @@ -1739,7 +1738,7 @@ pub async fn generate_recovery_codes( state: AppState, user_token: auth::UserFromSinglePurposeToken, ) -> UserResponse { - if !utils::user::two_factor_auth::check_totp_in_redis(&state, &user_token.user_id).await? { + if !tfa_utils::check_totp_in_redis(&state, &user_token.user_id).await? { return Err(UserErrors::TotpRequired.into()); } @@ -1766,3 +1765,55 @@ pub async fn generate_recovery_codes( recovery_codes: recovery_codes.into_inner(), })) } + +pub async fn terminate_two_factor_auth( + state: AppState, + user_token: auth::UserFromSinglePurposeToken, + skip_two_factor_auth: bool, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_id(&user_token.user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into(); + + if !skip_two_factor_auth { + if !tfa_utils::check_totp_in_redis(&state, &user_token.user_id).await? + && !tfa_utils::check_recovery_code_in_redis(&state, &user_token.user_id).await? + { + return Err(UserErrors::TwoFactorAuthRequired.into()); + } + + if user_from_db.get_recovery_codes().is_none() { + return Err(UserErrors::TwoFactorAuthNotSetup.into()); + } + + if user_from_db.get_totp_status() != TotpStatus::Set { + state + .store + .update_user_by_user_id( + user_from_db.get_user_id(), + storage_user::UserUpdate::TotpUpdate { + totp_status: Some(TotpStatus::Set), + totp_secret: None, + totp_recovery_codes: None, + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + } + } + + let current_flow = domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::TOTP.into())?; + let next_flow = current_flow.next(user_from_db, &state).await?; + let token = next_flow.get_token(&state).await?; + + auth::cookies::set_cookie_response( + user_api::TokenResponse { + token: token.clone(), + token_type: next_flow.get_flow().into(), + }, + token, + ) +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 0e152ec32c1d..cf2e986c325f 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1215,6 +1215,9 @@ impl User { .service( web::resource("/recovery_codes/generate") .route(web::get().to(generate_recovery_codes)), + ) + .service( + web::resource("/2fa/terminate").route(web::get().to(terminate_two_factor_auth)), ); #[cfg(feature = "email")] diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 706726979dc1..97d92d49911d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -215,6 +215,7 @@ impl From for ApiIdentifier { | Flow::UpdateUserAccountDetails | Flow::TotpBegin | Flow::TotpVerify + | Flow::TerminateTwoFactorAuth | Flow::GenerateRecoveryCodes => Self::User, Flow::ListRoles diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index f542c446e498..2019855114a3 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -679,3 +679,23 @@ pub async fn generate_recovery_codes(state: web::Data, req: HttpReques )) .await } + +pub async fn terminate_two_factor_auth( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::TerminateTwoFactorAuth; + let skip_two_factor_auth = query.into_inner().skip_two_factor_auth.unwrap_or(false); + + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user, _, _| user_core::terminate_two_factor_auth(state, user, skip_two_factor_auth), + &auth::SinglePurposeJWTAuth(TokenPurpose::TOTP), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 051e6ccf38ed..0704e51e7813 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -930,6 +930,10 @@ impl UserFromStorage { self.0.totp_status } + pub fn get_recovery_codes(&self) -> Option>> { + self.0.totp_recovery_codes.clone() + } + pub async fn decrypt_and_get_totp_secret( &self, state: &AppState, diff --git a/crates/router/src/utils/user/two_factor_auth.rs b/crates/router/src/utils/user/two_factor_auth.rs index 62bcf2f7eb15..479c346b6e53 100644 --- a/crates/router/src/utils/user/two_factor_auth.rs +++ b/crates/router/src/utils/user/two_factor_auth.rs @@ -43,6 +43,15 @@ pub async fn check_totp_in_redis(state: &AppState, user_id: &str) -> UserResult< .change_context(UserErrors::InternalServerError) } +pub async fn check_recovery_code_in_redis(state: &AppState, user_id: &str) -> UserResult { + let redis_conn = get_redis_connection(state)?; + let key = format!("{}{}", consts::user::REDIS_RECOVERY_CODES_PREFIX, user_id); + redis_conn + .exists::<()>(&key) + .await + .change_context(UserErrors::InternalServerError) +} + fn get_redis_connection(state: &AppState) -> UserResult> { state .store diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0e35aba31741..be071ffefc37 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -408,6 +408,8 @@ pub enum Flow { TotpVerify, /// Generate or Regenerate recovery codes GenerateRecoveryCodes, + // Terminate two factor authentication + TerminateTwoFactorAuth, /// List initial webhook delivery attempts WebhookEventInitialDeliveryAttemptList, /// List delivery attempts for a webhook event