mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(users): add support to verify 2FA using recovery code (#4737)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -351,7 +351,8 @@ email_role_arn = "" # The amazon resource name ( arn ) of the role which
|
|||||||
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
|
sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session.
|
||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90 # Number of days after which password should be updated
|
password_validity_in_days = 90 # Number of days after which password should be updated
|
||||||
|
two_factor_auth_expiry_in_secs = 300 # Number of seconds after which 2FA should be done again if doing update/change from inside
|
||||||
|
|
||||||
#tokenization configuration which describe token lifetime and payment method for specific connector
|
#tokenization configuration which describe token lifetime and payment method for specific connector
|
||||||
[tokenization]
|
[tokenization]
|
||||||
|
|||||||
@ -113,6 +113,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@ -120,6 +120,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
@ -120,6 +120,7 @@ slack_invite_url = "https://join.slack.com/t/hyperswitch-io/shared_invite/zt-2aw
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[frm]
|
[frm]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
@ -269,6 +269,7 @@ sts_role_session_name = ""
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[bank_config.eps]
|
[bank_config.eps]
|
||||||
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
|
stripe = { banks = "arzte_und_apotheker_bank,austrian_anadi_bank_ag,bank_austria,bankhaus_carl_spangler,bankhaus_schelhammer_und_schattera_ag,bawag_psk_ag,bks_bank_ag,brull_kallmus_bank_ag,btv_vier_lander_bank,capital_bank_grawe_gruppe_ag,dolomitenbank,easybank_ag,erste_bank_und_sparkassen,hypo_alpeadriabank_international_ag,hypo_noe_lb_fur_niederosterreich_u_wien,hypo_oberosterreich_salzburg_steiermark,hypo_tirol_bank_ag,hypo_vorarlberg_bank_ag,hypo_bank_burgenland_aktiengesellschaft,marchfelder_bank,oberbank_ag,raiffeisen_bankengruppe_osterreich,schoellerbank_ag,sparda_bank_wien,volksbank_gruppe,volkskreditbank_ag,vr_bank_braunau" }
|
||||||
|
|||||||
@ -53,6 +53,7 @@ recon_admin_api_key = "recon_test_admin"
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[locker]
|
[locker]
|
||||||
host = ""
|
host = ""
|
||||||
|
|||||||
@ -17,7 +17,7 @@ use crate::user::{
|
|||||||
RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest,
|
RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest,
|
||||||
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
|
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
|
||||||
TokenOrPayloadResponse, TokenResponse, UpdateUserAccountDetailsRequest, UserFromEmailRequest,
|
TokenOrPayloadResponse, TokenResponse, UpdateUserAccountDetailsRequest, UserFromEmailRequest,
|
||||||
UserMerchantCreate, VerifyEmailRequest, VerifyTotpRequest,
|
UserMerchantCreate, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl ApiEventMetric for DashboardEntryResponse {
|
impl ApiEventMetric for DashboardEntryResponse {
|
||||||
@ -75,6 +75,7 @@ common_utils::impl_misc_api_event_type!(
|
|||||||
TokenResponse,
|
TokenResponse,
|
||||||
UserFromEmailRequest,
|
UserFromEmailRequest,
|
||||||
BeginTotpResponse,
|
BeginTotpResponse,
|
||||||
|
VerifyRecoveryCodeRequest,
|
||||||
VerifyTotpRequest,
|
VerifyTotpRequest,
|
||||||
RecoveryCodes
|
RecoveryCodes
|
||||||
);
|
);
|
||||||
|
|||||||
@ -263,6 +263,11 @@ pub struct VerifyTotpRequest {
|
|||||||
pub totp: Option<Secret<String>>,
|
pub totp: Option<Secret<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct VerifyRecoveryCodeRequest {
|
||||||
|
pub recovery_code: Secret<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct RecoveryCodes {
|
pub struct RecoveryCodes {
|
||||||
pub recovery_codes: Vec<Secret<String>>,
|
pub recovery_codes: Vec<Secret<String>>,
|
||||||
|
|||||||
@ -395,6 +395,7 @@ pub struct Secrets {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
pub struct UserSettings {
|
pub struct UserSettings {
|
||||||
pub password_validity_in_days: u16,
|
pub password_validity_in_days: u16,
|
||||||
|
pub two_factor_auth_expiry_in_secs: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
|||||||
@ -14,4 +14,4 @@ pub const MAX_PASSWORD_LENGTH: usize = 70;
|
|||||||
pub const MIN_PASSWORD_LENGTH: usize = 8;
|
pub const MIN_PASSWORD_LENGTH: usize = 8;
|
||||||
|
|
||||||
pub const TOTP_PREFIX: &str = "TOTP_";
|
pub const TOTP_PREFIX: &str = "TOTP_";
|
||||||
pub const REDIS_RECOVERY_CODES_PREFIX: &str = "RC_";
|
pub const REDIS_RECOVERY_CODE_PREFIX: &str = "RC_";
|
||||||
|
|||||||
@ -72,6 +72,8 @@ pub enum UserErrors {
|
|||||||
InvalidTotp,
|
InvalidTotp,
|
||||||
#[error("TotpRequired")]
|
#[error("TotpRequired")]
|
||||||
TotpRequired,
|
TotpRequired,
|
||||||
|
#[error("InvalidRecoveryCode")]
|
||||||
|
InvalidRecoveryCode,
|
||||||
#[error("TwoFactorAuthRequired")]
|
#[error("TwoFactorAuthRequired")]
|
||||||
TwoFactorAuthRequired,
|
TwoFactorAuthRequired,
|
||||||
#[error("TwoFactorAuthNotSetup")]
|
#[error("TwoFactorAuthNotSetup")]
|
||||||
@ -188,12 +190,15 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
|||||||
Self::TotpRequired => {
|
Self::TotpRequired => {
|
||||||
AER::BadRequest(ApiError::new(sub_code, 38, self.get_error_message(), None))
|
AER::BadRequest(ApiError::new(sub_code, 38, self.get_error_message(), None))
|
||||||
}
|
}
|
||||||
Self::TwoFactorAuthRequired => {
|
Self::InvalidRecoveryCode => {
|
||||||
AER::BadRequest(ApiError::new(sub_code, 39, self.get_error_message(), None))
|
AER::BadRequest(ApiError::new(sub_code, 39, self.get_error_message(), None))
|
||||||
}
|
}
|
||||||
Self::TwoFactorAuthNotSetup => {
|
Self::TwoFactorAuthRequired => {
|
||||||
AER::BadRequest(ApiError::new(sub_code, 40, self.get_error_message(), None))
|
AER::BadRequest(ApiError::new(sub_code, 40, self.get_error_message(), None))
|
||||||
}
|
}
|
||||||
|
Self::TwoFactorAuthNotSetup => {
|
||||||
|
AER::BadRequest(ApiError::new(sub_code, 41, self.get_error_message(), None))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,6 +238,7 @@ impl UserErrors {
|
|||||||
Self::TotpNotSetup => "TOTP not setup",
|
Self::TotpNotSetup => "TOTP not setup",
|
||||||
Self::InvalidTotp => "Invalid TOTP",
|
Self::InvalidTotp => "Invalid TOTP",
|
||||||
Self::TotpRequired => "TOTP required",
|
Self::TotpRequired => "TOTP required",
|
||||||
|
Self::InvalidRecoveryCode => "Invalid Recovery Code",
|
||||||
Self::TwoFactorAuthRequired => "Two factor auth required",
|
Self::TwoFactorAuthRequired => "Two factor auth required",
|
||||||
Self::TwoFactorAuthNotSetup => "Two factor auth not setup",
|
Self::TwoFactorAuthNotSetup => "Two factor auth not setup",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -179,7 +179,7 @@ pub async fn signin(
|
|||||||
})?
|
})?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
user_from_db.compare_password(request.password)?;
|
user_from_db.compare_password(&request.password)?;
|
||||||
|
|
||||||
let signin_strategy =
|
let signin_strategy =
|
||||||
if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() {
|
if let Some(preferred_merchant_id) = user_from_db.get_preferred_merchant_id() {
|
||||||
@ -217,7 +217,7 @@ pub async fn signin_token_only_flow(
|
|||||||
.to_not_found_response(UserErrors::InvalidCredentials)?
|
.to_not_found_response(UserErrors::InvalidCredentials)?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
user_from_db.compare_password(request.password)?;
|
user_from_db.compare_password(&request.password)?;
|
||||||
|
|
||||||
let next_flow =
|
let next_flow =
|
||||||
domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?;
|
domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?;
|
||||||
@ -341,7 +341,7 @@ pub async fn change_password(
|
|||||||
.change_context(UserErrors::InternalServerError)?
|
.change_context(UserErrors::InternalServerError)?
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
user.compare_password(request.old_password.to_owned())
|
user.compare_password(&request.old_password)
|
||||||
.change_context(UserErrors::InvalidOldPassword)?;
|
.change_context(UserErrors::InvalidOldPassword)?;
|
||||||
|
|
||||||
if request.old_password == request.new_password {
|
if request.old_password == request.new_password {
|
||||||
@ -439,7 +439,7 @@ pub async fn rotate_password(
|
|||||||
let password = domain::UserPassword::new(request.password.to_owned())?;
|
let password = domain::UserPassword::new(request.password.to_owned())?;
|
||||||
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
|
||||||
|
|
||||||
if user.compare_password(request.password).is_ok() {
|
if user.compare_password(&request.password).is_ok() {
|
||||||
return Err(UserErrors::ChangePasswordError.into());
|
return Err(UserErrors::ChangePasswordError.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1766,6 +1766,51 @@ pub async fn generate_recovery_codes(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn verify_recovery_code(
|
||||||
|
state: AppState,
|
||||||
|
user_token: auth::UserFromSinglePurposeToken,
|
||||||
|
req: user_api::VerifyRecoveryCodeRequest,
|
||||||
|
) -> 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 user_from_db.get_totp_status() != TotpStatus::Set {
|
||||||
|
return Err(UserErrors::TwoFactorAuthNotSetup.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut recovery_codes = user_from_db
|
||||||
|
.get_recovery_codes()
|
||||||
|
.ok_or(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
let matching_index = utils::user::password::get_index_for_correct_recovery_code(
|
||||||
|
&req.recovery_code,
|
||||||
|
&recovery_codes,
|
||||||
|
)?
|
||||||
|
.ok_or(UserErrors::InvalidRecoveryCode)?;
|
||||||
|
|
||||||
|
tfa_utils::insert_recovery_code_in_redis(&state, user_from_db.get_user_id()).await?;
|
||||||
|
let _ = recovery_codes.remove(matching_index);
|
||||||
|
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.update_user_by_user_id(
|
||||||
|
user_from_db.get_user_id(),
|
||||||
|
storage_user::UserUpdate::TotpUpdate {
|
||||||
|
totp_status: None,
|
||||||
|
totp_secret: None,
|
||||||
|
totp_recovery_codes: Some(recovery_codes),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)?;
|
||||||
|
|
||||||
|
Ok(ApplicationResponse::StatusOk)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn terminate_two_factor_auth(
|
pub async fn terminate_two_factor_auth(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
user_token: auth::UserFromSinglePurposeToken,
|
user_token: auth::UserFromSinglePurposeToken,
|
||||||
|
|||||||
@ -1212,14 +1212,16 @@ impl User {
|
|||||||
)
|
)
|
||||||
.service(web::resource("/totp/begin").route(web::get().to(totp_begin)))
|
.service(web::resource("/totp/begin").route(web::get().to(totp_begin)))
|
||||||
.service(web::resource("/totp/verify").route(web::post().to(totp_verify)))
|
.service(web::resource("/totp/verify").route(web::post().to(totp_verify)))
|
||||||
.service(
|
|
||||||
web::resource("/recovery_codes/generate")
|
|
||||||
.route(web::get().to(generate_recovery_codes)),
|
|
||||||
)
|
|
||||||
.service(
|
.service(
|
||||||
web::resource("/2fa/terminate").route(web::get().to(terminate_two_factor_auth)),
|
web::resource("/2fa/terminate").route(web::get().to(terminate_two_factor_auth)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route = route.service(
|
||||||
|
web::scope("/recovery_code")
|
||||||
|
.service(web::resource("/verify").route(web::post().to(verify_recovery_code)))
|
||||||
|
.service(web::resource("/generate").route(web::post().to(generate_recovery_codes))),
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
{
|
{
|
||||||
route = route
|
route = route
|
||||||
|
|||||||
@ -215,9 +215,9 @@ impl From<Flow> for ApiIdentifier {
|
|||||||
| Flow::UpdateUserAccountDetails
|
| Flow::UpdateUserAccountDetails
|
||||||
| Flow::TotpBegin
|
| Flow::TotpBegin
|
||||||
| Flow::TotpVerify
|
| Flow::TotpVerify
|
||||||
| Flow::TerminateTwoFactorAuth
|
| Flow::RecoveryCodeVerify
|
||||||
| Flow::GenerateRecoveryCodes => Self::User,
|
| Flow::RecoveryCodesGenerate
|
||||||
|
| Flow::TerminateTwoFactorAuth => Self::User,
|
||||||
Flow::ListRoles
|
Flow::ListRoles
|
||||||
| Flow::GetRole
|
| Flow::GetRole
|
||||||
| Flow::GetRoleFromToken
|
| Flow::GetRoleFromToken
|
||||||
|
|||||||
@ -666,8 +666,26 @@ pub async fn totp_verify(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn verify_recovery_code(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
json_payload: web::Json<user_api::VerifyRecoveryCodeRequest>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::RecoveryCodeVerify;
|
||||||
|
Box::pin(api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.clone(),
|
||||||
|
&req,
|
||||||
|
json_payload.into_inner(),
|
||||||
|
|state, user, req_body, _| user_core::verify_recovery_code(state, user, req_body),
|
||||||
|
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
|
||||||
|
api_locking::LockAction::NotApplicable,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||||
let flow = Flow::GenerateRecoveryCodes;
|
let flow = Flow::RecoveryCodesGenerate;
|
||||||
Box::pin(api::server_wrap(
|
Box::pin(api::server_wrap(
|
||||||
flow,
|
flow,
|
||||||
state.clone(),
|
state.clone(),
|
||||||
|
|||||||
@ -774,8 +774,8 @@ impl UserFromStorage {
|
|||||||
self.0.user_id.as_str()
|
self.0.user_id.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compare_password(&self, candidate: Secret<String>) -> UserResult<()> {
|
pub fn compare_password(&self, candidate: &Secret<String>) -> UserResult<()> {
|
||||||
match password::is_correct_password(candidate, self.0.password.clone()) {
|
match password::is_correct_password(candidate, &self.0.password) {
|
||||||
Ok(true) => Ok(()),
|
Ok(true) => Ok(()),
|
||||||
Ok(false) => Err(UserErrors::InvalidCredentials.into()),
|
Ok(false) => Err(UserErrors::InvalidCredentials.into()),
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use argon2::{
|
|||||||
};
|
};
|
||||||
use common_utils::errors::CustomResult;
|
use common_utils::errors::CustomResult;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
use masking::{ExposeInterface, Secret};
|
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||||
use rand::{seq::SliceRandom, Rng};
|
use rand::{seq::SliceRandom, Rng};
|
||||||
|
|
||||||
use crate::core::errors::UserErrors;
|
use crate::core::errors::UserErrors;
|
||||||
@ -25,13 +25,13 @@ pub fn generate_password_hash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_correct_password(
|
pub fn is_correct_password(
|
||||||
candidate: Secret<String>,
|
candidate: &Secret<String>,
|
||||||
password: Secret<String>,
|
password: &Secret<String>,
|
||||||
) -> CustomResult<bool, UserErrors> {
|
) -> CustomResult<bool, UserErrors> {
|
||||||
let password = password.expose();
|
let password = password.peek();
|
||||||
let parsed_hash =
|
let parsed_hash =
|
||||||
PasswordHash::new(&password).change_context(UserErrors::InternalServerError)?;
|
PasswordHash::new(password).change_context(UserErrors::InternalServerError)?;
|
||||||
let result = Argon2::default().verify_password(candidate.expose().as_bytes(), &parsed_hash);
|
let result = Argon2::default().verify_password(candidate.peek().as_bytes(), &parsed_hash);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
Err(argon2Err::Password) => Ok(false),
|
Err(argon2Err::Password) => Ok(false),
|
||||||
@ -40,6 +40,19 @@ pub fn is_correct_password(
|
|||||||
.change_context(UserErrors::InternalServerError)
|
.change_context(UserErrors::InternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_index_for_correct_recovery_code(
|
||||||
|
candidate: &Secret<String>,
|
||||||
|
recovery_codes: &[Secret<String>],
|
||||||
|
) -> CustomResult<Option<usize>, UserErrors> {
|
||||||
|
for (index, recovery_code) in recovery_codes.iter().enumerate() {
|
||||||
|
let is_match = is_correct_password(candidate, recovery_code)?;
|
||||||
|
if is_match {
|
||||||
|
return Ok(Some(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_temp_password() -> Secret<String> {
|
pub fn get_temp_password() -> Secret<String> {
|
||||||
let uuid_pass = uuid::Uuid::new_v4().to_string();
|
let uuid_pass = uuid::Uuid::new_v4().to_string();
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
|
|||||||
@ -45,7 +45,7 @@ pub async fn check_totp_in_redis(state: &AppState, user_id: &str) -> UserResult<
|
|||||||
|
|
||||||
pub async fn check_recovery_code_in_redis(state: &AppState, user_id: &str) -> UserResult<bool> {
|
pub async fn check_recovery_code_in_redis(state: &AppState, user_id: &str) -> UserResult<bool> {
|
||||||
let redis_conn = get_redis_connection(state)?;
|
let redis_conn = get_redis_connection(state)?;
|
||||||
let key = format!("{}{}", consts::user::REDIS_RECOVERY_CODES_PREFIX, user_id);
|
let key = format!("{}{}", consts::user::REDIS_RECOVERY_CODE_PREFIX, user_id);
|
||||||
redis_conn
|
redis_conn
|
||||||
.exists::<()>(&key)
|
.exists::<()>(&key)
|
||||||
.await
|
.await
|
||||||
@ -59,3 +59,16 @@ fn get_redis_connection(state: &AppState) -> UserResult<Arc<RedisConnectionPool>
|
|||||||
.change_context(UserErrors::InternalServerError)
|
.change_context(UserErrors::InternalServerError)
|
||||||
.attach_printable("Failed to get redis connection")
|
.attach_printable("Failed to get redis connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn insert_recovery_code_in_redis(state: &AppState, user_id: &str) -> UserResult<()> {
|
||||||
|
let redis_conn = get_redis_connection(state)?;
|
||||||
|
let key = format!("{}{}", consts::user::REDIS_RECOVERY_CODE_PREFIX, user_id);
|
||||||
|
redis_conn
|
||||||
|
.set_key_with_expiry(
|
||||||
|
key.as_str(),
|
||||||
|
common_utils::date_time::now_unix_timestamp(),
|
||||||
|
state.conf.user.two_factor_auth_expiry_in_secs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(UserErrors::InternalServerError)
|
||||||
|
}
|
||||||
|
|||||||
@ -406,8 +406,10 @@ pub enum Flow {
|
|||||||
TotpBegin,
|
TotpBegin,
|
||||||
/// Verify TOTP
|
/// Verify TOTP
|
||||||
TotpVerify,
|
TotpVerify,
|
||||||
|
/// Verify Access Code
|
||||||
|
RecoveryCodeVerify,
|
||||||
/// Generate or Regenerate recovery codes
|
/// Generate or Regenerate recovery codes
|
||||||
GenerateRecoveryCodes,
|
RecoveryCodesGenerate,
|
||||||
// Terminate two factor authentication
|
// Terminate two factor authentication
|
||||||
TerminateTwoFactorAuth,
|
TerminateTwoFactorAuth,
|
||||||
/// List initial webhook delivery attempts
|
/// List initial webhook delivery attempts
|
||||||
|
|||||||
@ -30,6 +30,7 @@ jwt_secret = "secret"
|
|||||||
|
|
||||||
[user]
|
[user]
|
||||||
password_validity_in_days = 90
|
password_validity_in_days = 90
|
||||||
|
two_factor_auth_expiry_in_secs = 300
|
||||||
|
|
||||||
[locker]
|
[locker]
|
||||||
host = ""
|
host = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user