mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(users): Create generate recovery codes API (#4708)
This commit is contained in:
@ -14,10 +14,10 @@ use crate::user::{
|
||||
ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse,
|
||||
ForgotPasswordRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest,
|
||||
GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse, ReInviteUserRequest,
|
||||
ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest, SignInResponse,
|
||||
SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenOrPayloadResponse,
|
||||
TokenResponse, UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate,
|
||||
VerifyEmailRequest, VerifyTotpRequest,
|
||||
RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest,
|
||||
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
|
||||
TokenOrPayloadResponse, TokenResponse, UpdateUserAccountDetailsRequest, UserFromEmailRequest,
|
||||
UserMerchantCreate, VerifyEmailRequest, VerifyTotpRequest,
|
||||
};
|
||||
|
||||
impl ApiEventMetric for DashboardEntryResponse {
|
||||
@ -75,7 +75,8 @@ common_utils::impl_misc_api_event_type!(
|
||||
TokenResponse,
|
||||
UserFromEmailRequest,
|
||||
BeginTotpResponse,
|
||||
VerifyTotpRequest
|
||||
VerifyTotpRequest,
|
||||
RecoveryCodes
|
||||
);
|
||||
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
|
||||
@ -257,3 +257,8 @@ pub struct TotpSecret {
|
||||
pub struct VerifyTotpRequest {
|
||||
pub totp: Option<Secret<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct RecoveryCodes {
|
||||
pub recovery_codes: Vec<Secret<String>>,
|
||||
}
|
||||
|
||||
@ -12,3 +12,5 @@ pub const TOTP_VALIDITY_DURATION_IN_SECONDS: u64 = 30;
|
||||
pub const TOTP_TOLERANCE: u8 = 1;
|
||||
pub const MAX_PASSWORD_LENGTH: usize = 70;
|
||||
pub const MIN_PASSWORD_LENGTH: usize = 8;
|
||||
|
||||
pub const TOTP_PREFIX: &str = "TOTP_";
|
||||
|
||||
@ -66,10 +66,12 @@ pub enum UserErrors {
|
||||
RoleNameParsingError,
|
||||
#[error("RoleNameAlreadyExists")]
|
||||
RoleNameAlreadyExists,
|
||||
#[error("TOTPNotSetup")]
|
||||
#[error("TotpNotSetup")]
|
||||
TotpNotSetup,
|
||||
#[error("InvalidTOTP")]
|
||||
#[error("InvalidTotp")]
|
||||
InvalidTotp,
|
||||
#[error("TotpRequired")]
|
||||
TotpRequired,
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -179,6 +181,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
Self::InvalidTotp => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 37, self.get_error_message(), None))
|
||||
}
|
||||
Self::TotpRequired => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 38, self.get_error_message(), None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -217,6 +222,7 @@ impl UserErrors {
|
||||
Self::RoleNameAlreadyExists => "Role name already exists",
|
||||
Self::TotpNotSetup => "TOTP not setup",
|
||||
Self::InvalidTotp => "Invalid TOTP",
|
||||
Self::TotpRequired => "TOTP required",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1631,7 +1631,7 @@ pub async fn begin_totp(
|
||||
}));
|
||||
}
|
||||
|
||||
let totp = utils::user::generate_default_totp(user_from_db.get_email(), None)?;
|
||||
let totp = utils::user::two_factor_auth::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,8 +1693,10 @@ pub async fn verify_totp(
|
||||
.await?
|
||||
.ok_or(UserErrors::InternalServerError)?;
|
||||
|
||||
let totp =
|
||||
utils::user::generate_default_totp(user_from_db.get_email(), Some(user_totp_secret))?;
|
||||
let totp = utils::user::two_factor_auth::generate_default_totp(
|
||||
user_from_db.get_email(),
|
||||
Some(user_totp_secret),
|
||||
)?;
|
||||
|
||||
if totp
|
||||
.generate_current()
|
||||
@ -1732,3 +1734,35 @@ pub async fn verify_totp(
|
||||
token,
|
||||
)
|
||||
}
|
||||
|
||||
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? {
|
||||
return Err(UserErrors::TotpRequired.into());
|
||||
}
|
||||
|
||||
let recovery_codes = domain::RecoveryCodes::generate_new();
|
||||
|
||||
state
|
||||
.store
|
||||
.update_user_by_user_id(
|
||||
&user_token.user_id,
|
||||
storage_user::UserUpdate::TotpUpdate {
|
||||
totp_status: None,
|
||||
totp_secret: None,
|
||||
totp_recovery_codes: Some(
|
||||
recovery_codes
|
||||
.get_hashed()
|
||||
.change_context(UserErrors::InternalServerError)?,
|
||||
),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
Ok(ApplicationResponse::Json(user_api::RecoveryCodes {
|
||||
recovery_codes: recovery_codes.into_inner(),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -1211,7 +1211,11 @@ impl User {
|
||||
.route(web::post().to(set_dashboard_metadata)),
|
||||
)
|
||||
.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)),
|
||||
);
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
|
||||
@ -214,7 +214,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::VerifyEmailRequest
|
||||
| Flow::UpdateUserAccountDetails
|
||||
| Flow::TotpBegin
|
||||
| Flow::TotpVerify => Self::User,
|
||||
| Flow::TotpVerify
|
||||
| Flow::GenerateRecoveryCodes => Self::User,
|
||||
|
||||
Flow::ListRoles
|
||||
| Flow::GetRole
|
||||
|
||||
@ -665,3 +665,17 @@ pub async fn totp_verify(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let flow = Flow::GenerateRecoveryCodes;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
(),
|
||||
|state, user, _, _| user_core::generate_recovery_codes(state, user),
|
||||
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use api_models::user as user_api;
|
||||
use common_utils::{errors::CustomResult, pii};
|
||||
use common_utils::errors::CustomResult;
|
||||
use diesel_models::{enums::UserStatus, user_role::UserRole};
|
||||
use error_stack::ResultExt;
|
||||
use masking::ExposeInterface;
|
||||
use totp_rs::{Algorithm, TOTP};
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
core::errors::{StorageError, UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
services::{
|
||||
@ -22,6 +19,7 @@ pub mod dashboard_metadata;
|
||||
pub mod password;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
pub mod sample_data;
|
||||
pub mod two_factor_auth;
|
||||
|
||||
impl UserFromToken {
|
||||
pub async fn get_merchant_account_from_db(
|
||||
@ -193,25 +191,3 @@ pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> maskin
|
||||
user_api::SignInResponse::MerchantSelect(data) => data.token.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_default_totp(
|
||||
email: pii::Email,
|
||||
secret: Option<masking::Secret<String>>,
|
||||
) -> UserResult<TOTP> {
|
||||
let secret = secret
|
||||
.map(|sec| totp_rs::Secret::Encoded(sec.expose()))
|
||||
.unwrap_or_else(totp_rs::Secret::generate_secret)
|
||||
.to_bytes()
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
consts::user::TOTP_DIGITS,
|
||||
consts::user::TOTP_TOLERANCE,
|
||||
consts::user::TOTP_VALIDITY_DURATION_IN_SECONDS,
|
||||
secret,
|
||||
Some(consts::user::TOTP_ISSUER_NAME.to_string()),
|
||||
email.expose().expose(),
|
||||
)
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
|
||||
52
crates/router/src/utils/user/two_factor_auth.rs
Normal file
52
crates/router/src/utils/user/two_factor_auth.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use common_utils::pii;
|
||||
use error_stack::ResultExt;
|
||||
use masking::ExposeInterface;
|
||||
use redis_interface::RedisConnectionPool;
|
||||
use totp_rs::{Algorithm, TOTP};
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
core::errors::{UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
};
|
||||
|
||||
pub fn generate_default_totp(
|
||||
email: pii::Email,
|
||||
secret: Option<masking::Secret<String>>,
|
||||
) -> UserResult<TOTP> {
|
||||
let secret = secret
|
||||
.map(|sec| totp_rs::Secret::Encoded(sec.expose()))
|
||||
.unwrap_or_else(totp_rs::Secret::generate_secret)
|
||||
.to_bytes()
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
TOTP::new(
|
||||
Algorithm::SHA1,
|
||||
consts::user::TOTP_DIGITS,
|
||||
consts::user::TOTP_TOLERANCE,
|
||||
consts::user::TOTP_VALIDITY_DURATION_IN_SECONDS,
|
||||
secret,
|
||||
Some(consts::user::TOTP_ISSUER_NAME.to_string()),
|
||||
email.expose().expose(),
|
||||
)
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
|
||||
pub async fn check_totp_in_redis(state: &AppState, user_id: &str) -> UserResult<bool> {
|
||||
let redis_conn = get_redis_connection(state)?;
|
||||
let key = format!("{}{}", consts::user::TOTP_PREFIX, user_id);
|
||||
redis_conn
|
||||
.exists::<()>(&key)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
|
||||
fn get_redis_connection(state: &AppState) -> UserResult<Arc<RedisConnectionPool>> {
|
||||
state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Failed to get redis connection")
|
||||
}
|
||||
@ -406,6 +406,8 @@ pub enum Flow {
|
||||
TotpBegin,
|
||||
/// Verify TOTP
|
||||
TotpVerify,
|
||||
/// Generate or Regenerate recovery codes
|
||||
GenerateRecoveryCodes,
|
||||
/// List initial webhook delivery attempts
|
||||
WebhookEventInitialDeliveryAttemptList,
|
||||
/// List delivery attempts for a webhook event
|
||||
|
||||
Reference in New Issue
Block a user