Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(users): Create terminate 2fa API #4731

Merged
merged 8 commits into from
May 23, 2024
5 changes: 5 additions & 0 deletions crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ pub struct TokenOnlyQueryParam {
pub token_only: Option<bool>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Skip2faQueryParam {
pub skip_2fa: Option<bool>,
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct TokenResponse {
pub token: Secret<String>,
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/consts/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_";
6 changes: 6 additions & 0 deletions crates/router/src/core/errors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub enum UserErrors {
InvalidTotp,
#[error("TotpRequired")]
TotpRequired,
#[error("TwoFARequired")]
TwoFARequired,
}

impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
Expand Down Expand Up @@ -184,6 +186,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::TotpRequired => {
AER::BadRequest(ApiError::new(sub_code, 38, self.get_error_message(), None))
}
Self::TwoFARequired => {
AER::BadRequest(ApiError::new(sub_code, 39, self.get_error_message(), None))
}
}
}
}
Expand Down Expand Up @@ -223,6 +228,7 @@ impl UserErrors {
Self::TotpNotSetup => "TOTP not setup",
Self::InvalidTotp => "Invalid TOTP",
Self::TotpRequired => "TOTP required",
Self::TwoFARequired => "2fa required",
}
}
}
61 changes: 54 additions & 7 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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?;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -1739,7 +1738,7 @@ pub async fn generate_recovery_codes(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
) -> UserResponse<user_api::RecoveryCodes> {
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());
}

Expand All @@ -1766,3 +1765,51 @@ pub async fn generate_recovery_codes(
recovery_codes: recovery_codes.into_inner(),
}))
}

pub async fn terminate_2fa(
state: AppState,
user_token: auth::UserFromSinglePurposeToken,
skip_2fa: Option<bool>,
) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state
.store
.find_user_by_id(&user_token.user_id)
.await
.change_context(UserErrors::InternalServerError)?
.into();

if !skip_2fa.unwrap_or(false) {
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::TwoFARequired.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,
)
}
3 changes: 2 additions & 1 deletion crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1215,7 +1215,8 @@ 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_2fa)));

#[cfg(feature = "email")]
{
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::UpdateUserAccountDetails
| Flow::TotpBegin
| Flow::TotpVerify
| Flow::Terminate2fa
| Flow::GenerateRecoveryCodes => Self::User,

Flow::ListRoles
Expand Down
20 changes: 20 additions & 0 deletions crates/router/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,3 +679,23 @@ pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpReques
))
.await
}

pub async fn terminate_2fa(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<user_api::Skip2faQueryParam>,
) -> HttpResponse {
let flow = Flow::Terminate2fa;
let skip_2fa = query.into_inner().skip_2fa;

Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
(),
|state, user, _, _| user_core::terminate_2fa(state, user, skip_2fa),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable,
))
.await
}
9 changes: 9 additions & 0 deletions crates/router/src/utils/user/two_factor_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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<Arc<RedisConnectionPool>> {
state
.store
Expand Down
2 changes: 2 additions & 0 deletions crates/router_env/src/logger/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ pub enum Flow {
TotpVerify,
/// Generate or Regenerate recovery codes
GenerateRecoveryCodes,
// Terminate 2factor authentication
Terminate2fa,
/// List initial webhook delivery attempts
WebhookEventInitialDeliveryAttemptList,
/// List delivery attempts for a webhook event
Expand Down
Loading