mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 09:38:33 +08:00
feat(users): Create terminate 2fa API (#4731)
This commit is contained in:
@ -224,6 +224,11 @@ pub struct TokenOnlyQueryParam {
|
||||
pub token_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct SkipTwoFactorAuthQueryParam {
|
||||
pub skip_two_factor_auth: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct TokenResponse {
|
||||
pub token: Secret<String>,
|
||||
|
||||
@ -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_";
|
||||
|
||||
@ -72,6 +72,10 @@ pub enum UserErrors {
|
||||
InvalidTotp,
|
||||
#[error("TotpRequired")]
|
||||
TotpRequired,
|
||||
#[error("TwoFactorAuthRequired")]
|
||||
TwoFactorAuthRequired,
|
||||
#[error("TwoFactorAuthNotSetup")]
|
||||
TwoFactorAuthNotSetup,
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -184,6 +188,12 @@ 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::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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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());
|
||||
}
|
||||
|
||||
@ -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<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_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,
|
||||
)
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -215,6 +215,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::UpdateUserAccountDetails
|
||||
| Flow::TotpBegin
|
||||
| Flow::TotpVerify
|
||||
| Flow::TerminateTwoFactorAuth
|
||||
| Flow::GenerateRecoveryCodes => Self::User,
|
||||
|
||||
Flow::ListRoles
|
||||
|
||||
@ -679,3 +679,23 @@ pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpReques
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn terminate_two_factor_auth(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
query: web::Query<user_api::SkipTwoFactorAuthQueryParam>,
|
||||
) -> 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
|
||||
}
|
||||
|
||||
@ -930,6 +930,10 @@ impl UserFromStorage {
|
||||
self.0.totp_status
|
||||
}
|
||||
|
||||
pub fn get_recovery_codes(&self) -> Option<Vec<Secret<String>>> {
|
||||
self.0.totp_recovery_codes.clone()
|
||||
}
|
||||
|
||||
pub async fn decrypt_and_get_totp_secret(
|
||||
&self,
|
||||
state: &AppState,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user