diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 3f0febcc59..325ca98024 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -21,7 +21,7 @@ pub mod routes { routes::AppState, services::{ api, - authentication::{self as auth, AuthToken, AuthenticationData}, + authentication::{self as auth, AuthenticationData}, authorization::permissions::Permission, ApplicationResponse, }, @@ -378,23 +378,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GenerateRefundReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; @@ -430,23 +420,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GenerateDisputeReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; @@ -482,23 +462,13 @@ pub mod routes { req: actix_web::HttpRequest, json_payload: web::Json, ) -> impl Responder { - let state_ref = &state; - let req_headers = &req.headers(); - let flow = AnalyticsFlow::GeneratePaymentReport; Box::pin(api::server_wrap( flow, state.clone(), &req, json_payload.into_inner(), - |state, auth: AuthenticationData, payload| async move { - let jwt_payload = - auth::parse_jwt_payload::(req_headers, state_ref).await; - - let user_id = jwt_payload - .change_context(AnalyticsError::UnknownError)? - .user_id; - + |state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move { let user = UserInterface::find_user_by_id(&*state.store, &user_id) .await .change_context(AnalyticsError::UnknownError)?; diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 387da3c064..12b688e3d3 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -66,12 +66,16 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes -#[cfg(any(feature = "olap", feature = "oltp"))] pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days +pub const USER_BLACKLIST_PREFIX: &str = "BU_"; + #[cfg(feature = "email")] pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day +#[cfg(feature = "email")] +pub const EMAIL_TOKEN_BLACKLIST_PREFIX: &str = "BET_"; + #[cfg(feature = "olap")] pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify"; #[cfg(feature = "olap")] diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index ae66728e14..24b6eb9d12 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -264,6 +264,11 @@ pub async fn connect_account( } } +pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> { + auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?; + Ok(ApplicationResponse::StatusOk) +} + pub async fn change_password( state: AppState, request: user_api::ChangePasswordRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 4a726084c2..a69220231f 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -965,6 +965,7 @@ impl User { web::resource("/signin").route(web::post().to(user_signin_without_invite_checks)), ) .service(web::resource("/v2/signin").route(web::post().to(user_signin))) + .service(web::resource("/signout").route(web::post().to(signout))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 2837c1defa..b726c64f0e 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -164,6 +164,7 @@ impl From for ApiIdentifier { | Flow::UserSignUp | Flow::UserSignInWithoutInviteChecks | Flow::UserSignIn + | Flow::Signout | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index d4bdcaae87..0c2694dc70 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -116,6 +116,20 @@ pub async fn user_connect_account( .await } +pub async fn signout(state: web::Data, http_req: HttpRequest) -> HttpResponse { + let flow = Flow::Signout; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + (), + |state, user, _| user_core::signout(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn change_password( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 7f1e078ad5..221106612f 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -36,6 +36,7 @@ use crate::{ types::domain, utils::OptionExt, }; +pub mod blacklist; #[derive(Clone, Debug)] pub struct AuthenticationData { @@ -333,6 +334,9 @@ where state: &A, ) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(( UserWithoutMerchantFromToken { @@ -495,6 +499,9 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -521,6 +528,9 @@ where state: &A, ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -556,6 +566,9 @@ where state: &A, ) -> RouterResult<((), AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.required_permission, permissions)?; @@ -585,12 +598,6 @@ where Ok(payload) } -#[derive(serde::Deserialize)] -struct JwtAuthPayloadFetchMerchantAccount { - merchant_id: String, - role_id: String, -} - #[async_trait] impl AuthenticateAndFetch for JWTAuth where @@ -601,9 +608,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let payload = - parse_jwt_payload::(request_headers, state) - .await?; + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } let permissions = authorization::get_permissions(&payload.role_id)?; authorization::check_authorization(&self.0, permissions)?; @@ -638,6 +646,56 @@ where } } +pub type AuthenticationDataWithUserId = (AuthenticationData, String); + +#[async_trait] +impl AuthenticateAndFetch for JWTAuth +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationDataWithUserId, AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } + + let permissions = authorization::get_permissions(&payload.role_id)?; + authorization::check_authorization(&self.0, permissions)?; + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + &payload.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id(&payload.merchant_id, &key_store) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken)?; + + let auth = AuthenticationData { + merchant_account: merchant, + key_store, + }; + Ok(( + (auth.clone(), payload.user_id.clone()), + AuthenticationType::MerchantJwt { + merchant_id: auth.merchant_account.merchant_id.clone(), + user_id: None, + }, + )) + } +} + pub struct DashboardNoPermissionAuth; #[cfg(feature = "olap")] @@ -652,6 +710,9 @@ where state: &A, ) -> RouterResult<(UserFromToken, AuthenticationType)> { let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(( UserFromToken { @@ -679,7 +740,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<((), AuthenticationType)> { - parse_jwt_payload::(request_headers, state).await?; + let payload = parse_jwt_payload::(request_headers, state).await?; + if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? { + return Err(errors::ApiErrorResponse::InvalidJwtToken.into()); + } Ok(((), AuthenticationType::NoAuth)) } diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs new file mode 100644 index 0000000000..6fab28433b --- /dev/null +++ b/crates/router/src/services/authentication/blacklist.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +#[cfg(feature = "olap")] +use common_utils::date_time; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::RedisConnectionPool; + +use crate::{ + consts::{JWT_TOKEN_TIME_IN_SECS, USER_BLACKLIST_PREFIX}, + core::errors::{ApiErrorResponse, RouterResult}, + routes::app::AppStateInfo, +}; +#[cfg(feature = "olap")] +use crate::{ + core::errors::{UserErrors, UserResult}, + routes::AppState, +}; + +#[cfg(feature = "olap")] +pub async fn insert_user_in_blacklist(state: &AppState, user_id: &str) -> UserResult<()> { + let user_blacklist_key = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let expiry = + expiry_to_i64(JWT_TOKEN_TIME_IN_SECS).change_context(UserErrors::InternalServerError)?; + let redis_conn = get_redis_connection(state).change_context(UserErrors::InternalServerError)?; + redis_conn + .set_key_with_expiry( + user_blacklist_key.as_str(), + date_time::now_unix_timestamp(), + expiry, + ) + .await + .change_context(UserErrors::InternalServerError) +} + +pub async fn check_user_in_blacklist( + state: &A, + user_id: &str, + token_expiry: u64, +) -> RouterResult { + let token = format!("{}{}", USER_BLACKLIST_PREFIX, user_id); + let token_issued_at = expiry_to_i64(token_expiry - JWT_TOKEN_TIME_IN_SECS)?; + let redis_conn = get_redis_connection(state)?; + redis_conn + .get_key::>(token.as_str()) + .await + .change_context(ApiErrorResponse::InternalServerError) + .map(|timestamp| timestamp.map_or(false, |timestamp| timestamp > token_issued_at)) +} + +fn get_redis_connection(state: &A) -> RouterResult> { + state + .store() + .get_redis_conn() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection") +} + +fn expiry_to_i64(expiry: u64) -> RouterResult { + expiry + .try_into() + .into_report() + .change_context(ApiErrorResponse::InternalServerError) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 8e32cb6333..a395235ca8 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -285,6 +285,8 @@ pub enum Flow { FrmFulfillment, /// Change password flow ChangePassword, + /// Signout flow + Signout, /// Set Dashboard Metadata flow SetDashboardMetadata, /// Get Multiple Dashboard Metadata flow