mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
1163 lines
34 KiB
Rust
1163 lines
34 KiB
Rust
use actix_web::http::header::HeaderMap;
|
|
use api_models::{
|
|
payment_methods::{PaymentMethodCreate, PaymentMethodListRequest},
|
|
payments,
|
|
};
|
|
use async_trait::async_trait;
|
|
use common_utils::date_time;
|
|
use error_stack::{report, ResultExt};
|
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
|
use masking::PeekInterface;
|
|
use router_env::logger;
|
|
use serde::Serialize;
|
|
|
|
use self::blacklist::BlackList;
|
|
use super::authorization::{self, permissions::Permission};
|
|
#[cfg(feature = "olap")]
|
|
use super::jwt;
|
|
#[cfg(feature = "recon")]
|
|
use super::recon::ReconToken;
|
|
#[cfg(feature = "olap")]
|
|
use crate::configs::Settings;
|
|
#[cfg(feature = "olap")]
|
|
use crate::consts;
|
|
#[cfg(feature = "olap")]
|
|
use crate::core::errors::UserResult;
|
|
#[cfg(feature = "recon")]
|
|
use crate::routes::AppState;
|
|
use crate::{
|
|
core::{
|
|
api_keys,
|
|
errors::{self, utils::StorageErrorExt, RouterResult},
|
|
},
|
|
db::StorageInterface,
|
|
routes::app::AppStateInfo,
|
|
services::api,
|
|
types::domain,
|
|
utils::OptionExt,
|
|
};
|
|
pub mod blacklist;
|
|
pub mod cookies;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct AuthenticationData {
|
|
pub merchant_account: domain::MerchantAccount,
|
|
pub key_store: domain::MerchantKeyStore,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
|
#[serde(
|
|
tag = "api_auth_type",
|
|
content = "authentication_data",
|
|
rename_all = "snake_case"
|
|
)]
|
|
pub enum AuthenticationType {
|
|
ApiKey {
|
|
merchant_id: String,
|
|
key_id: String,
|
|
},
|
|
AdminApiKey,
|
|
MerchantJwt {
|
|
merchant_id: String,
|
|
user_id: Option<String>,
|
|
},
|
|
UserJwt {
|
|
user_id: String,
|
|
},
|
|
SinglePurposeJWT {
|
|
user_id: String,
|
|
purpose: Purpose,
|
|
},
|
|
MerchantId {
|
|
merchant_id: String,
|
|
},
|
|
PublishableKey {
|
|
merchant_id: String,
|
|
},
|
|
WebhookAuth {
|
|
merchant_id: String,
|
|
},
|
|
NoAuth,
|
|
}
|
|
|
|
impl events::EventInfo for AuthenticationType {
|
|
type Data = Self;
|
|
fn data(&self) -> error_stack::Result<Self::Data, events::EventsError> {
|
|
Ok(self.clone())
|
|
}
|
|
|
|
fn key(&self) -> String {
|
|
"auth_info".to_string()
|
|
}
|
|
}
|
|
|
|
impl AuthenticationType {
|
|
pub fn get_merchant_id(&self) -> Option<&str> {
|
|
match self {
|
|
Self::ApiKey {
|
|
merchant_id,
|
|
key_id: _,
|
|
}
|
|
| Self::MerchantId { merchant_id }
|
|
| Self::PublishableKey { merchant_id }
|
|
| Self::MerchantJwt {
|
|
merchant_id,
|
|
user_id: _,
|
|
}
|
|
| Self::WebhookAuth { merchant_id } => Some(merchant_id.as_ref()),
|
|
Self::AdminApiKey
|
|
| Self::UserJwt { .. }
|
|
| Self::SinglePurposeJWT { .. }
|
|
| Self::NoAuth => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct UserFromSinglePurposeToken {
|
|
pub user_id: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct SinglePurposeToken {
|
|
pub user_id: String,
|
|
pub purpose: Purpose,
|
|
pub exp: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
|
|
pub enum Purpose {
|
|
AcceptInvite,
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
impl SinglePurposeToken {
|
|
pub async fn new_token(
|
|
user_id: String,
|
|
purpose: Purpose,
|
|
settings: &Settings,
|
|
) -> UserResult<String> {
|
|
let exp_duration =
|
|
std::time::Duration::from_secs(consts::SINGLE_PURPOSE_TOKEN_TIME_IN_SECS);
|
|
let exp = jwt::generate_exp(exp_duration)?.as_secs();
|
|
let token_payload = Self {
|
|
user_id,
|
|
purpose,
|
|
exp,
|
|
};
|
|
jwt::generate_jwt(&token_payload, settings).await
|
|
}
|
|
}
|
|
|
|
// TODO: This has to be removed once single purpose token is used as a intermediate token
|
|
#[derive(Clone, Debug)]
|
|
pub struct UserWithoutMerchantFromToken {
|
|
pub user_id: String,
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct UserAuthToken {
|
|
pub user_id: String,
|
|
pub exp: u64,
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
impl UserAuthToken {
|
|
pub async fn new_token(user_id: String, settings: &Settings) -> UserResult<String> {
|
|
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, exp };
|
|
jwt::generate_jwt(&token_payload, settings).await
|
|
}
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
pub struct AuthToken {
|
|
pub user_id: String,
|
|
pub merchant_id: String,
|
|
pub role_id: String,
|
|
pub exp: u64,
|
|
pub org_id: String,
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
impl AuthToken {
|
|
pub async fn new_token(
|
|
user_id: String,
|
|
merchant_id: String,
|
|
role_id: String,
|
|
settings: &Settings,
|
|
org_id: String,
|
|
) -> UserResult<String> {
|
|
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,
|
|
role_id,
|
|
exp,
|
|
org_id,
|
|
};
|
|
jwt::generate_jwt(&token_payload, settings).await
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct UserFromToken {
|
|
pub user_id: String,
|
|
pub merchant_id: String,
|
|
pub role_id: String,
|
|
pub org_id: String,
|
|
}
|
|
|
|
pub trait AuthInfo {
|
|
fn get_merchant_id(&self) -> Option<&str>;
|
|
}
|
|
|
|
impl AuthInfo for () {
|
|
fn get_merchant_id(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl AuthInfo for AuthenticationData {
|
|
fn get_merchant_id(&self) -> Option<&str> {
|
|
Some(&self.merchant_account.merchant_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait AuthenticateAndFetch<T, A>
|
|
where
|
|
A: AppStateInfo,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(T, AuthenticationType)>;
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ApiKeyAuth;
|
|
|
|
pub struct NoAuth;
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<(), A> for NoAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
_request_headers: &HeaderMap,
|
|
_state: &A,
|
|
) -> RouterResult<((), AuthenticationType)> {
|
|
Ok(((), AuthenticationType::NoAuth))
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationData, A> for ApiKeyAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
|
let api_key = get_api_key(request_headers)
|
|
.change_context(errors::ApiErrorResponse::Unauthorized)?
|
|
.trim();
|
|
if api_key.is_empty() {
|
|
return Err(errors::ApiErrorResponse::Unauthorized)
|
|
.attach_printable("API key is empty");
|
|
}
|
|
|
|
let api_key = api_keys::PlaintextApiKey::from(api_key);
|
|
let hash_key = {
|
|
let config = state.conf();
|
|
config.api_keys.get_inner().get_hash_key()?
|
|
};
|
|
let hashed_api_key = api_key.keyed_hash(hash_key.peek());
|
|
|
|
let stored_api_key = state
|
|
.store()
|
|
.find_api_key_by_hash_optional(hashed_api_key.into())
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
|
|
.attach_printable("Failed to retrieve API key")?
|
|
.ok_or(report!(errors::ApiErrorResponse::Unauthorized)) // If retrieve returned `None`
|
|
.attach_printable("Merchant not authenticated")?;
|
|
|
|
if stored_api_key
|
|
.expires_at
|
|
.map(|expires_at| expires_at < date_time::now())
|
|
.unwrap_or(false)
|
|
{
|
|
return Err(report!(errors::ApiErrorResponse::Unauthorized))
|
|
.attach_printable("API key has expired");
|
|
}
|
|
|
|
let key_store = state
|
|
.store()
|
|
.get_merchant_key_store_by_merchant_id(
|
|
&stored_api_key.merchant_id,
|
|
&state.store().get_master_key().to_vec().into(),
|
|
)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::Unauthorized)
|
|
.attach_printable("Failed to fetch merchant key store for the merchant id")?;
|
|
|
|
let merchant = state
|
|
.store()
|
|
.find_merchant_account_by_merchant_id(&stored_api_key.merchant_id, &key_store)
|
|
.await
|
|
.to_not_found_response(errors::ApiErrorResponse::Unauthorized)?;
|
|
|
|
let auth = AuthenticationData {
|
|
merchant_account: merchant,
|
|
key_store,
|
|
};
|
|
Ok((
|
|
auth.clone(),
|
|
AuthenticationType::ApiKey {
|
|
merchant_id: auth.merchant_account.merchant_id.clone(),
|
|
key_id: stored_api_key.key_id,
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct UserWithoutMerchantJWTAuth;
|
|
|
|
#[cfg(feature = "olap")]
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<UserWithoutMerchantFromToken, A> for UserWithoutMerchantJWTAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> {
|
|
let payload = parse_jwt_payload::<A, UserAuthToken>(request_headers, state).await?;
|
|
if payload.check_in_blacklist(state).await? {
|
|
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
|
}
|
|
|
|
Ok((
|
|
UserWithoutMerchantFromToken {
|
|
user_id: payload.user_id.clone(),
|
|
},
|
|
AuthenticationType::UserJwt {
|
|
user_id: payload.user_id,
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug)]
|
|
pub(crate) struct SinglePurposeJWTAuth(pub Purpose);
|
|
|
|
#[cfg(feature = "olap")]
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<UserFromSinglePurposeToken, A> for SinglePurposeJWTAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(UserFromSinglePurposeToken, AuthenticationType)> {
|
|
let payload = parse_jwt_payload::<A, SinglePurposeToken>(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());
|
|
}
|
|
|
|
Ok((
|
|
UserFromSinglePurposeToken {
|
|
user_id: payload.user_id.clone(),
|
|
},
|
|
AuthenticationType::SinglePurposeJWT {
|
|
user_id: payload.user_id,
|
|
purpose: payload.purpose,
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct AdminApiAuth;
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<(), A> for AdminApiAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), AuthenticationType)> {
|
|
let request_admin_api_key =
|
|
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
|
|
let conf = state.conf();
|
|
|
|
let admin_api_key = &conf.secrets.get_inner().admin_api_key;
|
|
|
|
if request_admin_api_key != admin_api_key.peek() {
|
|
Err(report!(errors::ApiErrorResponse::Unauthorized)
|
|
.attach_printable("Admin Authentication Failure"))?;
|
|
}
|
|
|
|
Ok(((), AuthenticationType::AdminApiKey))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct MerchantIdAuth(pub String);
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationData, A> for MerchantIdAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
_request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
|
let key_store = state
|
|
.store()
|
|
.get_merchant_key_store_by_merchant_id(
|
|
self.0.as_ref(),
|
|
&state.store().get_master_key().to_vec().into(),
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
if e.current_context().is_db_not_found() {
|
|
e.change_context(errors::ApiErrorResponse::Unauthorized)
|
|
} else {
|
|
e.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to fetch merchant key store for the merchant id")
|
|
}
|
|
})?;
|
|
|
|
let merchant = state
|
|
.store()
|
|
.find_merchant_account_by_merchant_id(self.0.as_ref(), &key_store)
|
|
.await
|
|
.map_err(|e| {
|
|
if e.current_context().is_db_not_found() {
|
|
e.change_context(errors::ApiErrorResponse::Unauthorized)
|
|
} else {
|
|
e.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
}
|
|
})?;
|
|
|
|
let auth = AuthenticationData {
|
|
merchant_account: merchant,
|
|
key_store,
|
|
};
|
|
Ok((
|
|
auth.clone(),
|
|
AuthenticationType::MerchantId {
|
|
merchant_id: auth.merchant_account.merchant_id.clone(),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct PublishableKeyAuth;
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationData, A> for PublishableKeyAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
|
let publishable_key =
|
|
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
|
|
|
|
state
|
|
.store()
|
|
.find_merchant_account_by_publishable_key(publishable_key)
|
|
.await
|
|
.map_err(|e| {
|
|
if e.current_context().is_db_not_found() {
|
|
e.change_context(errors::ApiErrorResponse::Unauthorized)
|
|
} else {
|
|
e.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
}
|
|
})
|
|
.map(|auth| {
|
|
(
|
|
auth.clone(),
|
|
AuthenticationType::PublishableKey {
|
|
merchant_id: auth.merchant_account.merchant_id.clone(),
|
|
},
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct JWTAuth(pub Permission);
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct JwtAuthPayloadFetchUnit {
|
|
#[serde(rename(deserialize = "exp"))]
|
|
_exp: u64,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<(), A> for JWTAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), 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());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
authorization::check_authorization(&self.0, &permissions)?;
|
|
|
|
Ok((
|
|
(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: payload.merchant_id,
|
|
user_id: Some(payload.user_id),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "olap")]
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<UserFromToken, A> for JWTAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(UserFromToken, 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());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
authorization::check_authorization(&self.0, &permissions)?;
|
|
|
|
Ok((
|
|
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),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
pub struct JWTAuthMerchantFromRoute {
|
|
pub merchant_id: String,
|
|
pub required_permission: Permission,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), 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());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
authorization::check_authorization(&self.required_permission, &permissions)?;
|
|
|
|
// Check if token has access to MerchantId that has been requested through query param
|
|
if payload.merchant_id != self.merchant_id {
|
|
return Err(report!(errors::ApiErrorResponse::InvalidJwtToken));
|
|
}
|
|
Ok((
|
|
(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: payload.merchant_id,
|
|
user_id: Some(payload.user_id),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
pub struct JWTAuthMerchantOrProfileFromRoute {
|
|
pub merchant_id_or_profile_id: String,
|
|
pub required_permission: Permission,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<(), A> for JWTAuthMerchantOrProfileFromRoute
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), 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());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
authorization::check_authorization(&self.required_permission, &permissions)?;
|
|
|
|
// Check if token has access to MerchantId that has been requested through path or query param
|
|
if payload.merchant_id == self.merchant_id_or_profile_id {
|
|
return Ok((
|
|
(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: payload.merchant_id,
|
|
user_id: Some(payload.user_id),
|
|
},
|
|
));
|
|
}
|
|
|
|
// Route did not contain the merchant ID in present JWT, check if it corresponds to a
|
|
// business profile
|
|
let business_profile = state
|
|
.store()
|
|
.find_business_profile_by_profile_id(&self.merchant_id_or_profile_id)
|
|
.await
|
|
// Return access forbidden if business profile not found
|
|
.to_not_found_response(errors::ApiErrorResponse::AccessForbidden {
|
|
resource: self.merchant_id_or_profile_id.clone(),
|
|
})
|
|
.attach_printable("Could not find business profile specified in route")?;
|
|
|
|
// Check if merchant (from JWT) has access to business profile that has been requested
|
|
// through path or query param
|
|
if payload.merchant_id == business_profile.merchant_id {
|
|
Ok((
|
|
(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: payload.merchant_id,
|
|
user_id: Some(payload.user_id),
|
|
},
|
|
))
|
|
} else {
|
|
Err(report!(errors::ApiErrorResponse::InvalidJwtToken))
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn parse_jwt_payload<A, T>(headers: &HeaderMap, state: &A) -> RouterResult<T>
|
|
where
|
|
T: serde::de::DeserializeOwned,
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
let token = match get_cookie_from_header(headers).and_then(cookies::parse_cookie) {
|
|
Ok(cookies) => cookies,
|
|
Err(e) => {
|
|
let token = get_jwt_from_authorization_header(headers);
|
|
if token.is_err() {
|
|
logger::error!(?e);
|
|
}
|
|
token?.to_owned()
|
|
}
|
|
};
|
|
decode_jwt(&token, state).await
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationData, A> for JWTAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(AuthenticationData, 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());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
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(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: auth.merchant_account.merchant_id.clone(),
|
|
user_id: None,
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
pub type AuthenticationDataWithUserId = (AuthenticationData, String);
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationDataWithUserId, A> 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::<A, AuthToken>(request_headers, state).await?;
|
|
if payload.check_in_blacklist(state).await? {
|
|
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
|
}
|
|
|
|
let permissions = authorization::get_permissions(state, &payload).await?;
|
|
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")]
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<UserFromToken, A> for DashboardNoPermissionAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(UserFromToken, 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((
|
|
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
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), 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(((), AuthenticationType::NoAuth))
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<A> AuthenticateAndFetch<AuthenticationData, A> for DashboardNoPermissionAuth
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
|
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
|
|
|
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::Unauthorized)
|
|
.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
|
|
.to_not_found_response(errors::ApiErrorResponse::Unauthorized)?;
|
|
|
|
let auth = AuthenticationData {
|
|
merchant_account: merchant,
|
|
key_store,
|
|
};
|
|
Ok((
|
|
auth.clone(),
|
|
AuthenticationType::MerchantJwt {
|
|
merchant_id: auth.merchant_account.merchant_id.clone(),
|
|
user_id: Some(payload.user_id),
|
|
},
|
|
))
|
|
}
|
|
}
|
|
|
|
pub trait ClientSecretFetch {
|
|
fn get_client_secret(&self) -> Option<&String>;
|
|
}
|
|
|
|
impl ClientSecretFetch for payments::PaymentsRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for PaymentMethodListRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for PaymentMethodCreate {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::cards_info::CardsInfoRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::payments::PaymentsRetrieveRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::payments::RetrievePaymentLinkRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::pm_auth::LinkTokenCreateRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate {
|
|
fn get_client_secret(&self) -> Option<&String> {
|
|
self.client_secret.as_ref()
|
|
}
|
|
}
|
|
|
|
pub fn get_auth_type_and_flow<A: AppStateInfo + Sync>(
|
|
headers: &HeaderMap,
|
|
) -> RouterResult<(
|
|
Box<dyn AuthenticateAndFetch<AuthenticationData, A>>,
|
|
api::AuthFlow,
|
|
)> {
|
|
let api_key = get_api_key(headers)?;
|
|
|
|
if api_key.starts_with("pk_") {
|
|
return Ok((Box::new(PublishableKeyAuth), api::AuthFlow::Client));
|
|
}
|
|
Ok((Box::new(ApiKeyAuth), api::AuthFlow::Merchant))
|
|
}
|
|
|
|
pub fn check_client_secret_and_get_auth<T>(
|
|
headers: &HeaderMap,
|
|
payload: &impl ClientSecretFetch,
|
|
) -> RouterResult<(
|
|
Box<dyn AuthenticateAndFetch<AuthenticationData, T>>,
|
|
api::AuthFlow,
|
|
)>
|
|
where
|
|
T: AppStateInfo,
|
|
ApiKeyAuth: AuthenticateAndFetch<AuthenticationData, T>,
|
|
PublishableKeyAuth: AuthenticateAndFetch<AuthenticationData, T>,
|
|
{
|
|
let api_key = get_api_key(headers)?;
|
|
|
|
if api_key.starts_with("pk_") {
|
|
payload
|
|
.get_client_secret()
|
|
.check_value_present("client_secret")
|
|
.map_err(|_| errors::ApiErrorResponse::MissingRequiredField {
|
|
field_name: "client_secret",
|
|
})?;
|
|
return Ok((Box::new(PublishableKeyAuth), api::AuthFlow::Client));
|
|
}
|
|
|
|
if payload.get_client_secret().is_some() {
|
|
return Err(errors::ApiErrorResponse::InvalidRequestData {
|
|
message: "client_secret is not a valid parameter".to_owned(),
|
|
}
|
|
.into());
|
|
}
|
|
Ok((Box::new(ApiKeyAuth), api::AuthFlow::Merchant))
|
|
}
|
|
|
|
pub async fn is_ephemeral_auth<A: AppStateInfo + Sync>(
|
|
headers: &HeaderMap,
|
|
db: &dyn StorageInterface,
|
|
customer_id: &str,
|
|
) -> RouterResult<Box<dyn AuthenticateAndFetch<AuthenticationData, A>>> {
|
|
let api_key = get_api_key(headers)?;
|
|
|
|
if !api_key.starts_with("epk") {
|
|
return Ok(Box::new(ApiKeyAuth));
|
|
}
|
|
|
|
let ephemeral_key = db
|
|
.get_ephemeral_key(api_key)
|
|
.await
|
|
.change_context(errors::ApiErrorResponse::Unauthorized)?;
|
|
|
|
if ephemeral_key.customer_id.ne(customer_id) {
|
|
return Err(report!(errors::ApiErrorResponse::InvalidEphemeralKey));
|
|
}
|
|
|
|
Ok(Box::new(MerchantIdAuth(ephemeral_key.merchant_id)))
|
|
}
|
|
|
|
pub fn is_jwt_auth(headers: &HeaderMap) -> bool {
|
|
headers.get(crate::headers::AUTHORIZATION).is_some()
|
|
|| get_cookie_from_header(headers)
|
|
.and_then(cookies::parse_cookie)
|
|
.is_ok()
|
|
}
|
|
|
|
pub async fn decode_jwt<T>(token: &str, state: &impl AppStateInfo) -> RouterResult<T>
|
|
where
|
|
T: serde::de::DeserializeOwned,
|
|
{
|
|
let conf = state.conf();
|
|
let secret = conf.secrets.get_inner().jwt_secret.peek().as_bytes();
|
|
|
|
let key = DecodingKey::from_secret(secret);
|
|
decode::<T>(token, &key, &Validation::new(Algorithm::HS256))
|
|
.map(|decoded| decoded.claims)
|
|
.change_context(errors::ApiErrorResponse::InvalidJwtToken)
|
|
}
|
|
|
|
pub fn get_api_key(headers: &HeaderMap) -> RouterResult<&str> {
|
|
get_header_value_by_key("api-key".into(), headers)?.get_required_value("api_key")
|
|
}
|
|
|
|
pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult<Option<&str>> {
|
|
headers
|
|
.get(&key)
|
|
.map(|source_str| {
|
|
source_str
|
|
.to_str()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable(format!(
|
|
"Failed to convert header value to string for header key: {}",
|
|
key
|
|
))
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
pub fn get_jwt_from_authorization_header(headers: &HeaderMap) -> RouterResult<&str> {
|
|
headers
|
|
.get(crate::headers::AUTHORIZATION)
|
|
.get_required_value(crate::headers::AUTHORIZATION)?
|
|
.to_str()
|
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
.attach_printable("Failed to convert JWT token to string")?
|
|
.strip_prefix("Bearer ")
|
|
.ok_or(errors::ApiErrorResponse::InvalidJwtToken.into())
|
|
}
|
|
|
|
pub fn get_cookie_from_header(headers: &HeaderMap) -> RouterResult<&str> {
|
|
headers
|
|
.get(cookies::get_cookie_header())
|
|
.and_then(|header_value| header_value.to_str().ok())
|
|
.ok_or(errors::ApiErrorResponse::InvalidCookie.into())
|
|
}
|
|
|
|
pub fn strip_jwt_token(token: &str) -> RouterResult<&str> {
|
|
token
|
|
.strip_prefix("Bearer ")
|
|
.ok_or_else(|| errors::ApiErrorResponse::InvalidJwtToken.into())
|
|
}
|
|
|
|
pub fn auth_type<'a, T, A>(
|
|
default_auth: &'a dyn AuthenticateAndFetch<T, A>,
|
|
jwt_auth_type: &'a dyn AuthenticateAndFetch<T, A>,
|
|
headers: &HeaderMap,
|
|
) -> &'a dyn AuthenticateAndFetch<T, A>
|
|
where
|
|
{
|
|
if is_jwt_auth(headers) {
|
|
return jwt_auth_type;
|
|
}
|
|
default_auth
|
|
}
|
|
|
|
#[cfg(feature = "recon")]
|
|
pub struct ReconAdmin;
|
|
|
|
#[async_trait]
|
|
#[cfg(feature = "recon")]
|
|
impl<A> AuthenticateAndFetch<(), A> for ReconAdmin
|
|
where
|
|
A: AppStateInfo + Sync,
|
|
{
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &A,
|
|
) -> RouterResult<((), AuthenticationType)> {
|
|
let request_admin_api_key =
|
|
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
|
|
let conf = state.conf();
|
|
|
|
let admin_api_key = conf.secrets.get_inner().recon_admin_api_key.peek();
|
|
|
|
if request_admin_api_key != admin_api_key {
|
|
Err(report!(errors::ApiErrorResponse::Unauthorized)
|
|
.attach_printable("Recon Admin Authentication Failure"))?;
|
|
}
|
|
|
|
Ok(((), AuthenticationType::NoAuth))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "recon")]
|
|
pub struct ReconJWT;
|
|
#[cfg(feature = "recon")]
|
|
pub struct ReconUser {
|
|
pub user_id: String,
|
|
}
|
|
#[cfg(feature = "recon")]
|
|
impl AuthInfo for ReconUser {
|
|
fn get_merchant_id(&self) -> Option<&str> {
|
|
None
|
|
}
|
|
}
|
|
#[cfg(all(feature = "olap", feature = "recon"))]
|
|
#[async_trait]
|
|
impl AuthenticateAndFetch<ReconUser, AppState> for ReconJWT {
|
|
async fn authenticate_and_fetch(
|
|
&self,
|
|
request_headers: &HeaderMap,
|
|
state: &AppState,
|
|
) -> RouterResult<(ReconUser, AuthenticationType)> {
|
|
let payload = parse_jwt_payload::<AppState, ReconToken>(request_headers, state).await?;
|
|
|
|
Ok((
|
|
ReconUser {
|
|
user_id: payload.user_id,
|
|
},
|
|
AuthenticationType::NoAuth,
|
|
))
|
|
}
|
|
}
|