diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 2124ff1aff..9a2a0ca839 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -2,6 +2,7 @@ pub mod apple_pay_certificates_migration; pub mod connector_onboarding; pub mod customer; pub mod dispute; +pub mod external_service_auth; pub mod gsm; mod locker_migration; pub mod payment; diff --git a/crates/api_models/src/events/external_service_auth.rs b/crates/api_models/src/events/external_service_auth.rs new file mode 100644 index 0000000000..31196150b2 --- /dev/null +++ b/crates/api_models/src/events/external_service_auth.rs @@ -0,0 +1,30 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::external_service_auth::{ + ExternalSignoutTokenRequest, ExternalTokenResponse, ExternalVerifyTokenRequest, + ExternalVerifyTokenResponse, +}; + +impl ApiEventMetric for ExternalTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ExternalServiceAuth) + } +} + +impl ApiEventMetric for ExternalVerifyTokenRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ExternalServiceAuth) + } +} + +impl ApiEventMetric for ExternalVerifyTokenResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ExternalServiceAuth) + } +} + +impl ApiEventMetric for ExternalSignoutTokenRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ExternalServiceAuth) + } +} diff --git a/crates/api_models/src/external_service_auth.rs b/crates/api_models/src/external_service_auth.rs new file mode 100644 index 0000000000..775529c435 --- /dev/null +++ b/crates/api_models/src/external_service_auth.rs @@ -0,0 +1,35 @@ +use common_utils::{id_type, pii}; +use masking::Secret; + +#[derive(Debug, serde::Serialize)] +pub struct ExternalTokenResponse { + pub token: Secret, +} +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ExternalVerifyTokenRequest { + pub token: Secret, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ExternalSignoutTokenRequest { + pub token: Secret, +} + +#[derive(serde::Serialize, Debug)] +#[serde(untagged)] +pub enum ExternalVerifyTokenResponse { + Hypersense { + user_id: String, + merchant_id: id_type::MerchantId, + name: Secret, + email: pii::Email, + }, +} + +impl ExternalVerifyTokenResponse { + pub fn get_user_id(&self) -> &str { + match self { + Self::Hypersense { user_id, .. } => user_id, + } + } +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 5ec80d2b53..c3cf1f1d25 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -16,6 +16,7 @@ pub mod ephemeral_key; #[cfg(feature = "errors")] pub mod errors; pub mod events; +pub mod external_service_auth; pub mod feature_matrix; pub mod files; pub mod gsm; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 2e90d646e9..556090858f 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -99,6 +99,7 @@ pub enum ApiEventsType { ApplePayCertificatesMigration, FraudCheck, Recon, + ExternalServiceAuth, Dispute { dispute_id: String, }, diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index c22cecc20f..18b3ad1435 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -18,6 +18,7 @@ pub mod customers; pub mod disputes; pub mod encryption; pub mod errors; +pub mod external_service_auth; pub mod files; #[cfg(feature = "frm")] pub mod fraud_check; diff --git a/crates/router/src/core/external_service_auth.rs b/crates/router/src/core/external_service_auth.rs new file mode 100644 index 0000000000..92c8f5b249 --- /dev/null +++ b/crates/router/src/core/external_service_auth.rs @@ -0,0 +1,94 @@ +use api_models::external_service_auth as external_service_auth_api; +use common_utils::fp_utils; +use error_stack::ResultExt; +use masking::ExposeInterface; + +use crate::{ + core::errors::{self, RouterResponse}, + services::{ + api as service_api, + authentication::{self, ExternalServiceType, ExternalToken}, + }, + SessionState, +}; + +pub async fn generate_external_token( + state: SessionState, + user: authentication::UserFromToken, + external_service_type: ExternalServiceType, +) -> RouterResponse { + let token = ExternalToken::new_token( + user.user_id.clone(), + user.merchant_id.clone(), + &state.conf, + external_service_type.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| { + format!( + "Failed to create external token for params [user_id, mid, external_service_type] [{}, {:?}, {:?}]", + user.user_id, user.merchant_id, external_service_type, + ) + })?; + + Ok(service_api::ApplicationResponse::Json( + external_service_auth_api::ExternalTokenResponse { + token: token.into(), + }, + )) +} + +pub async fn signout_external_token( + state: SessionState, + json_payload: external_service_auth_api::ExternalSignoutTokenRequest, +) -> RouterResponse<()> { + let token = authentication::decode_jwt::(&json_payload.token.expose(), &state) + .await + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + authentication::blacklist::insert_user_in_blacklist(&state, &token.user_id) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken)?; + + Ok(service_api::ApplicationResponse::StatusOk) +} + +pub async fn verify_external_token( + state: SessionState, + json_payload: external_service_auth_api::ExternalVerifyTokenRequest, + external_service_type: ExternalServiceType, +) -> RouterResponse { + let token_from_payload = json_payload.token.expose(); + + let token = authentication::decode_jwt::(&token_from_payload, &state) + .await + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + fp_utils::when( + authentication::blacklist::check_user_in_blacklist(&state, &token.user_id, token.exp) + .await?, + || Err(errors::ApiErrorResponse::InvalidJwtToken), + )?; + + token.check_service_type(&external_service_type)?; + + let user_in_db = state + .global_store + .find_user_by_id(&token.user_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("User not found in database")?; + + let email = user_in_db.email.clone(); + let name = user_in_db.name; + + Ok(service_api::ApplicationResponse::Json( + external_service_auth_api::ExternalVerifyTokenResponse::Hypersense { + user_id: user_in_db.user_id, + merchant_id: token.merchant_id, + name, + email, + }, + )) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 9b43d7f2b1..18e8e7cdcc 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -144,6 +144,7 @@ pub fn mk_app( .service(routes::MerchantConnectorAccount::server(state.clone())) .service(routes::RelayWebhooks::server(state.clone())) .service(routes::Webhooks::server(state.clone())) + .service(routes::Hypersense::server(state.clone())) .service(routes::Relay::server(state.clone())); #[cfg(feature = "oltp")] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 80906570d7..b589d4755f 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -23,6 +23,7 @@ pub mod files; pub mod fraud_check; pub mod gsm; pub mod health; +pub mod hypersense; pub mod lock_utils; #[cfg(feature = "v1")] pub mod locker_migration; @@ -69,9 +70,9 @@ pub use self::app::PaymentMethodsSession; pub use self::app::Recon; pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, - Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates, - MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, - Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks, + Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense, + Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, + Poll, Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 3da65e968b..14372cbfbb 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -88,6 +88,7 @@ pub use crate::{ use crate::{ configs::{secrets_transformers, Settings}, db::kafka_store::{KafkaStore, TenantID}, + routes::hypersense as hypersense_routes, }; #[derive(Clone)] @@ -1330,6 +1331,27 @@ impl Recon { } } +pub struct Hypersense; + +impl Hypersense { + pub fn server(state: AppState) -> Scope { + web::scope("/hypersense") + .app_data(web::Data::new(state)) + .service( + web::resource("/token") + .route(web::get().to(hypersense_routes::get_hypersense_token)), + ) + .service( + web::resource("/verify_token") + .route(web::post().to(hypersense_routes::verify_hypersense_token)), + ) + .service( + web::resource("/signout") + .route(web::post().to(hypersense_routes::signout_hypersense_token)), + ) + } +} + #[cfg(feature = "olap")] pub struct Blocklist; diff --git a/crates/router/src/routes/hypersense.rs b/crates/router/src/routes/hypersense.rs new file mode 100644 index 0000000000..a018dfa660 --- /dev/null +++ b/crates/router/src/routes/hypersense.rs @@ -0,0 +1,76 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::external_service_auth as external_service_auth_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, external_service_auth}, + services::{ + api, + authentication::{self, ExternalServiceType}, + }, +}; + +pub async fn get_hypersense_token(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::HypersenseTokenRequest; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user, _, _| { + external_service_auth::generate_external_token( + state, + user, + ExternalServiceType::Hypersense, + ) + }, + &authentication::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn signout_hypersense_token( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::HypersenseSignoutToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _: (), json_payload, _| { + external_service_auth::signout_external_token(state, json_payload) + }, + &authentication::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn verify_hypersense_token( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::HypersenseVerifyToken; + Box::pin(api::server_wrap( + flow, + state.clone(), + &http_req, + json_payload.into_inner(), + |state, _: (), json_payload, _| { + external_service_auth::verify_external_token( + state, + json_payload, + ExternalServiceType::Hypersense, + ) + }, + &authentication::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index ce9dda97c9..a50a27b9ec 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -39,6 +39,7 @@ pub enum ApiIdentifier { ApplePayCertificatesMigration, Relay, Documentation, + Hypersense, PaymentMethodsSession, } @@ -311,6 +312,10 @@ impl From for ApiIdentifier { Flow::FeatureMatrix => Self::Documentation, + Flow::HypersenseTokenRequest + | Flow::HypersenseVerifyToken + | Flow::HypersenseSignoutToken => Self::Hypersense, + Flow::PaymentMethodSessionCreate | Flow::PaymentMethodSessionRetrieve | Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index edf127ac30..502ebb5099 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -13,9 +13,7 @@ use api_models::payouts; use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_enums::TokenPurpose; -#[cfg(feature = "v2")] -use common_utils::fp_utils; -use common_utils::{date_time, id_type}; +use common_utils::{date_time, fp_utils, id_type}; #[cfg(feature = "v2")] use diesel_models::ephemeral_key; use error_stack::{report, ResultExt}; @@ -195,6 +193,13 @@ impl AuthenticationType { } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, serde::Deserialize, strum::Display)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ExternalServiceType { + Hypersense, +} + #[cfg(feature = "olap")] #[derive(Clone, Debug)] pub struct UserFromSinglePurposeToken { @@ -3857,3 +3862,44 @@ impl ReconToken { jwt::generate_jwt(&token_payload, settings).await } } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ExternalToken { + pub user_id: String, + pub merchant_id: id_type::MerchantId, + pub exp: u64, + pub external_service_type: ExternalServiceType, +} + +impl ExternalToken { + pub async fn new_token( + user_id: String, + merchant_id: id_type::MerchantId, + settings: &Settings, + external_service_type: ExternalServiceType, + ) -> UserResult { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration)?.as_secs(); + + let token_payload = Self { + user_id, + merchant_id, + exp, + external_service_type, + }; + jwt::generate_jwt(&token_payload, settings).await + } + + pub fn check_service_type( + &self, + required_service_type: &ExternalServiceType, + ) -> RouterResult<()> { + Ok(fp_utils::when( + &self.external_service_type != required_service_type, + || { + Err(errors::ApiErrorResponse::AccessForbidden { + resource: required_service_type.to_string(), + }) + }, + )?) + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b88effea34..3eda63bdba 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -543,6 +543,12 @@ pub enum Flow { RelayRetrieve, /// Incoming Relay Webhook Receive IncomingRelayWebhookReceive, + /// Generate Hypersense Token + HypersenseTokenRequest, + /// Verify Hypersense Token + HypersenseVerifyToken, + /// Signout Hypersense Token + HypersenseSignoutToken, /// Payment Method Session Create PaymentMethodSessionCreate, /// Payment Method Session Retrieve