feat(auth): Create and use SinglePurposeOrLoginTokenAuth (#4830)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Mani Chandra
2024-06-04 16:43:08 +05:30
committed by GitHub
parent 8096d5e577
commit 5414485866
6 changed files with 76 additions and 98 deletions

View File

@ -18,4 +18,4 @@ pub const MIN_PASSWORD_LENGTH: usize = 8;
pub const REDIS_TOTP_PREFIX: &str = "TOTP_";
pub const REDIS_RECOVERY_CODE_PREFIX: &str = "RC_";
pub const REDIS_TOTP_SECRET_PREFIX: &str = "TOTP_SEC_";
pub const REDIS_TOTP_SECRET_TTL_IN_SECS: i64 = 5 * 60; // 5 minutes
pub const REDIS_TOTP_SECRET_TTL_IN_SECS: i64 = 15 * 60; // 15 minutes

View File

@ -1237,11 +1237,11 @@ pub async fn create_merchant_account(
pub async fn list_merchants_for_user(
state: SessionState,
user_from_token: Box<dyn auth::GetUserIdFromAuth>,
user_from_token: auth::UserIdFromAuth,
) -> UserResponse<Vec<user_api::UserMerchantAccount>> {
let user_roles = state
.store
.list_user_roles_by_user_id(user_from_token.get_user_id().as_str())
.list_user_roles_by_user_id(user_from_token.user_id.as_str())
.await
.change_context(UserErrors::InternalServerError)?;
@ -1697,7 +1697,7 @@ pub async fn reset_totp(
pub async fn verify_totp(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
user_token: auth::UserIdFromAuth,
req: user_api::VerifyTotpRequest,
) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state
@ -1737,7 +1737,7 @@ pub async fn verify_totp(
pub async fn update_totp(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
user_token: auth::UserIdFromAuth,
req: user_api::VerifyTotpRequest,
) -> UserResponse<()> {
let user_from_db: domain::UserFromStorage = state
@ -1806,7 +1806,7 @@ pub async fn update_totp(
pub async fn generate_recovery_codes(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
user_token: auth::UserIdFromAuth,
) -> UserResponse<user_api::RecoveryCodes> {
if !tfa_utils::check_totp_in_redis(&state, &user_token.user_id).await? {
return Err(UserErrors::TotpRequired.into());
@ -1838,7 +1838,7 @@ pub async fn generate_recovery_codes(
pub async fn verify_recovery_code(
state: SessionState,
user_token: auth::UserFromSinglePurposeToken,
user_token: auth::UserIdFromAuth,
req: user_api::VerifyRecoveryCodeRequest,
) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state

View File

@ -1316,7 +1316,7 @@ impl User {
// The route is utilized to select an invitation from a list of merchants in an intermediate state
.service(
web::resource("/merchants_select/list")
.route(web::get().to(list_merchants_for_user_with_spt)),
.route(web::get().to(list_merchants_for_user)),
)
.service(web::resource("/permission_info").route(web::get().to(get_authorization_info)))
.service(web::resource("/update").route(web::post().to(update_user_account_details)))

View File

@ -318,24 +318,7 @@ pub async fn list_merchants_for_user(state: web::Data<AppState>, req: HttpReques
&req,
(),
|state, user, _, _| user_core::list_merchants_for_user(state, user),
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn list_merchants_for_user_with_spt(
state: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
let flow = Flow::UserMerchantAccountList;
Box::pin(api::server_wrap(
flow,
state,
&req,
(),
|state, user, _, _| user_core::list_merchants_for_user(state, user),
&auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::AcceptInvite),
api_locking::LockAction::NotApplicable,
))
.await
@ -674,7 +657,7 @@ pub async fn totp_verify(
&req,
json_payload.into_inner(),
|state, user, req_body, _| user_core::verify_totp(state, user, req_body),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable,
))
.await
@ -692,7 +675,7 @@ pub async fn verify_recovery_code(
&req,
json_payload.into_inner(),
|state, user, req_body, _| user_core::verify_recovery_code(state, user, req_body),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable,
))
.await
@ -710,7 +693,7 @@ pub async fn totp_update(
&req,
json_payload.into_inner(),
|state, user, req_body, _| user_core::update_totp(state, user, req_body),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable,
))
.await
@ -724,7 +707,7 @@ pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpReques
&req,
(),
|state, user, _, _| user_core::generate_recovery_codes(state, user),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP),
&auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable,
))
.await

View File

@ -64,10 +64,15 @@ pub enum AuthenticationType {
UserJwt {
user_id: String,
},
SinglePurposeJWT {
SinglePurposeJwt {
user_id: String,
purpose: TokenPurpose,
},
SinglePurposeOrLoginJwt {
user_id: String,
purpose: Option<TokenPurpose>,
role_id: Option<String>,
},
MerchantId {
merchant_id: String,
},
@ -107,7 +112,8 @@ impl AuthenticationType {
| Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()),
Self::AdminApiKey
| Self::UserJwt { .. }
| Self::SinglePurposeJWT { .. }
| Self::SinglePurposeJwt { .. }
| Self::SinglePurposeOrLoginJwt { .. }
| Self::NoAuth => None,
}
}
@ -189,6 +195,19 @@ pub struct UserFromToken {
pub org_id: String,
}
pub struct UserIdFromAuth {
pub user_id: String,
}
#[cfg(feature = "olap")]
#[derive(serde::Serialize, serde::Deserialize)]
pub struct SinglePurposeOrLoginToken {
pub user_id: String,
pub role_id: Option<String>,
pub purpose: Option<TokenPurpose>,
pub exp: u64,
}
pub trait AuthInfo {
fn get_merchant_id(&self) -> Option<&str>;
}
@ -205,23 +224,6 @@ impl AuthInfo for AuthenticationData {
}
}
pub trait GetUserIdFromAuth {
fn get_user_id(&self) -> String;
}
impl GetUserIdFromAuth for UserFromToken {
fn get_user_id(&self) -> String {
self.user_id.clone()
}
}
#[cfg(feature = "olap")]
impl GetUserIdFromAuth for UserFromSinglePurposeToken {
fn get_user_id(&self) -> String {
self.user_id.clone()
}
}
#[async_trait]
pub trait AuthenticateAndFetch<T, A>
where
@ -355,7 +357,7 @@ where
user_id: payload.user_id.clone(),
origin: payload.origin.clone(),
},
AuthenticationType::SinglePurposeJWT {
AuthenticationType::SinglePurposeJwt {
user_id: payload.user_id,
purpose: payload.purpose,
},
@ -363,9 +365,13 @@ where
}
}
#[cfg(feature = "olap")]
#[derive(Debug)]
pub struct SinglePurposeOrLoginTokenAuth(pub TokenPurpose);
#[cfg(feature = "olap")]
#[async_trait]
impl<A> AuthenticateAndFetch<Box<dyn GetUserIdFromAuth>, A> for SinglePurposeJWTAuth
impl<A> AuthenticateAndFetch<UserIdFromAuth, A> for SinglePurposeOrLoginTokenAuth
where
A: SessionStateInfo + Sync,
{
@ -373,26 +379,35 @@ where
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<(Box<dyn GetUserIdFromAuth>, AuthenticationType)> {
let payload = parse_jwt_payload::<A, SinglePurposeToken>(request_headers, state).await?;
) -> RouterResult<(UserIdFromAuth, AuthenticationType)> {
let payload =
parse_jwt_payload::<A, SinglePurposeOrLoginToken>(request_headers, state).await?;
if payload.check_in_blacklist(state).await? {
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
}
if self.0 != payload.purpose {
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
}
let is_purpose_equal = payload
.purpose
.as_ref()
.is_some_and(|purpose| purpose == &self.0);
let purpose_exists = payload.purpose.is_some();
let role_id_exists = payload.role_id.is_some();
if is_purpose_equal && !role_id_exists || role_id_exists && !purpose_exists {
Ok((
Box::new(UserFromSinglePurposeToken {
UserIdFromAuth {
user_id: payload.user_id.clone(),
origin: payload.origin.clone(),
}),
AuthenticationType::SinglePurposeJWT {
},
AuthenticationType::SinglePurposeOrLoginJwt {
user_id: payload.user_id,
purpose: payload.purpose,
role_id: payload.role_id,
},
))
} else {
Err(errors::ApiErrorResponse::InvalidJwtToken.into())
}
}
}
@ -864,37 +879,6 @@ where
}
}
#[cfg(feature = "olap")]
#[async_trait]
impl<A> AuthenticateAndFetch<Box<dyn GetUserIdFromAuth>, A> for DashboardNoPermissionAuth
where
A: SessionStateInfo + Sync,
{
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<(Box<dyn GetUserIdFromAuth>, AuthenticationType)> {
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
if payload.check_in_blacklist(state).await? {
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
}
Ok((
Box::new(UserFromToken {
user_id: payload.user_id.clone(),
merchant_id: payload.merchant_id.clone(),
org_id: payload.org_id,
role_id: payload.role_id,
}),
AuthenticationType::MerchantJwt {
merchant_id: payload.merchant_id,
user_id: Some(payload.user_id),
},
))
}
}
#[cfg(feature = "olap")]
#[async_trait]
impl<A> AuthenticateAndFetch<(), A> for DashboardNoPermissionAuth

View File

@ -7,7 +7,7 @@ use redis_interface::RedisConnectionPool;
use super::AuthToken;
#[cfg(feature = "olap")]
use super::SinglePurposeToken;
use super::{SinglePurposeOrLoginToken, SinglePurposeToken};
#[cfg(feature = "email")]
use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
use crate::{
@ -166,3 +166,14 @@ impl BlackList for SinglePurposeToken {
check_user_in_blacklist(state, &self.user_id, self.exp).await
}
}
#[cfg(feature = "olap")]
#[async_trait::async_trait]
impl BlackList for SinglePurposeOrLoginToken {
async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
where
A: SessionStateInfo + Sync,
{
check_user_in_blacklist(state, &self.user_id, self.exp).await
}
}