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_TOTP_PREFIX: &str = "TOTP_";
pub const REDIS_RECOVERY_CODE_PREFIX: &str = "RC_"; pub const REDIS_RECOVERY_CODE_PREFIX: &str = "RC_";
pub const REDIS_TOTP_SECRET_PREFIX: &str = "TOTP_SEC_"; 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( pub async fn list_merchants_for_user(
state: SessionState, state: SessionState,
user_from_token: Box<dyn auth::GetUserIdFromAuth>, user_from_token: auth::UserIdFromAuth,
) -> UserResponse<Vec<user_api::UserMerchantAccount>> { ) -> UserResponse<Vec<user_api::UserMerchantAccount>> {
let user_roles = state let user_roles = state
.store .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 .await
.change_context(UserErrors::InternalServerError)?; .change_context(UserErrors::InternalServerError)?;
@ -1697,7 +1697,7 @@ pub async fn reset_totp(
pub async fn verify_totp( pub async fn verify_totp(
state: SessionState, state: SessionState,
user_token: auth::UserFromSinglePurposeToken, user_token: auth::UserIdFromAuth,
req: user_api::VerifyTotpRequest, req: user_api::VerifyTotpRequest,
) -> UserResponse<user_api::TokenResponse> { ) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state let user_from_db: domain::UserFromStorage = state
@ -1737,7 +1737,7 @@ pub async fn verify_totp(
pub async fn update_totp( pub async fn update_totp(
state: SessionState, state: SessionState,
user_token: auth::UserFromSinglePurposeToken, user_token: auth::UserIdFromAuth,
req: user_api::VerifyTotpRequest, req: user_api::VerifyTotpRequest,
) -> UserResponse<()> { ) -> UserResponse<()> {
let user_from_db: domain::UserFromStorage = state let user_from_db: domain::UserFromStorage = state
@ -1806,7 +1806,7 @@ pub async fn update_totp(
pub async fn generate_recovery_codes( pub async fn generate_recovery_codes(
state: SessionState, state: SessionState,
user_token: auth::UserFromSinglePurposeToken, user_token: auth::UserIdFromAuth,
) -> UserResponse<user_api::RecoveryCodes> { ) -> UserResponse<user_api::RecoveryCodes> {
if !tfa_utils::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()); return Err(UserErrors::TotpRequired.into());
@ -1838,7 +1838,7 @@ pub async fn generate_recovery_codes(
pub async fn verify_recovery_code( pub async fn verify_recovery_code(
state: SessionState, state: SessionState,
user_token: auth::UserFromSinglePurposeToken, user_token: auth::UserIdFromAuth,
req: user_api::VerifyRecoveryCodeRequest, req: user_api::VerifyRecoveryCodeRequest,
) -> UserResponse<user_api::TokenResponse> { ) -> UserResponse<user_api::TokenResponse> {
let user_from_db: domain::UserFromStorage = state 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 // The route is utilized to select an invitation from a list of merchants in an intermediate state
.service( .service(
web::resource("/merchants_select/list") 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("/permission_info").route(web::get().to(get_authorization_info)))
.service(web::resource("/update").route(web::post().to(update_user_account_details))) .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, &req,
(), (),
|state, user, _, _| user_core::list_merchants_for_user(state, user), |state, user, _, _| user_core::list_merchants_for_user(state, user),
&auth::DashboardNoPermissionAuth, &auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::AcceptInvite),
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),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
)) ))
.await .await
@ -674,7 +657,7 @@ pub async fn totp_verify(
&req, &req,
json_payload.into_inner(), json_payload.into_inner(),
|state, user, req_body, _| user_core::verify_totp(state, user, req_body), |state, user, req_body, _| user_core::verify_totp(state, user, req_body),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP), &auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
)) ))
.await .await
@ -692,7 +675,7 @@ pub async fn verify_recovery_code(
&req, &req,
json_payload.into_inner(), json_payload.into_inner(),
|state, user, req_body, _| user_core::verify_recovery_code(state, user, req_body), |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, api_locking::LockAction::NotApplicable,
)) ))
.await .await
@ -710,7 +693,7 @@ pub async fn totp_update(
&req, &req,
json_payload.into_inner(), json_payload.into_inner(),
|state, user, req_body, _| user_core::update_totp(state, user, req_body), |state, user, req_body, _| user_core::update_totp(state, user, req_body),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP), &auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
)) ))
.await .await
@ -724,7 +707,7 @@ pub async fn generate_recovery_codes(state: web::Data<AppState>, req: HttpReques
&req, &req,
(), (),
|state, user, _, _| user_core::generate_recovery_codes(state, user), |state, user, _, _| user_core::generate_recovery_codes(state, user),
&auth::SinglePurposeJWTAuth(TokenPurpose::TOTP), &auth::SinglePurposeOrLoginTokenAuth(TokenPurpose::TOTP),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
)) ))
.await .await

View File

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

View File

@ -7,7 +7,7 @@ use redis_interface::RedisConnectionPool;
use super::AuthToken; use super::AuthToken;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
use super::SinglePurposeToken; use super::{SinglePurposeOrLoginToken, SinglePurposeToken};
#[cfg(feature = "email")] #[cfg(feature = "email")]
use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS}; use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
use crate::{ use crate::{
@ -166,3 +166,14 @@ impl BlackList for SinglePurposeToken {
check_user_in_blacklist(state, &self.user_id, self.exp).await 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
}
}