mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-04 05:59:48 +08:00 
			
		
		
		
	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:
		@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)))
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user