mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
refactor(authentication): authenticate merchant by API keys from API keys table (#712)
This commit is contained in:
@ -1,10 +1,8 @@
|
|||||||
use common_utils::{date_time, errors::CustomResult, fp_utils};
|
use common_utils::date_time;
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use masking::{PeekInterface, Secret, StrongSecret};
|
use masking::{PeekInterface, StrongSecret};
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
#[cfg(feature = "kms")]
|
|
||||||
use crate::services::kms;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
configs::settings,
|
configs::settings,
|
||||||
consts,
|
consts,
|
||||||
@ -14,6 +12,8 @@ use crate::{
|
|||||||
types::{api, storage, transformers::ForeignInto},
|
types::{api, storage, transformers::ForeignInto},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
|
use crate::{routes::metrics, services::kms};
|
||||||
|
|
||||||
pub static HASH_KEY: tokio::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> =
|
pub static HASH_KEY: tokio::sync::OnceCell<StrongSecret<[u8; PlaintextApiKey::HASH_KEY_LEN]>> =
|
||||||
tokio::sync::OnceCell::const_new();
|
tokio::sync::OnceCell::const_new();
|
||||||
@ -28,6 +28,10 @@ pub async fn get_hash_key(
|
|||||||
api_key_config.kms_encrypted_hash_key.clone(),
|
api_key_config.kms_encrypted_hash_key.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
metrics::AWS_KMS_FAILURES.add(&metrics::CONTEXT, 1, &[]);
|
||||||
|
error
|
||||||
|
})
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("Failed to KMS decrypt API key hashing key")?;
|
.attach_printable("Failed to KMS decrypt API key hashing key")?;
|
||||||
|
|
||||||
@ -49,11 +53,13 @@ pub async fn get_hash_key(
|
|||||||
|
|
||||||
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
|
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
|
||||||
// of plaintext API key being stored in the data store.
|
// of plaintext API key being stored in the data store.
|
||||||
pub struct PlaintextApiKey(Secret<String>);
|
pub struct PlaintextApiKey(StrongSecret<String>);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct HashedApiKey(String);
|
pub struct HashedApiKey(String);
|
||||||
|
|
||||||
impl PlaintextApiKey {
|
impl PlaintextApiKey {
|
||||||
pub const HASH_KEY_LEN: usize = 32;
|
const HASH_KEY_LEN: usize = 32;
|
||||||
|
|
||||||
const PREFIX_LEN: usize = 12;
|
const PREFIX_LEN: usize = 12;
|
||||||
|
|
||||||
@ -107,22 +113,6 @@ impl PlaintextApiKey {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_hash(
|
|
||||||
&self,
|
|
||||||
key: &[u8; Self::HASH_KEY_LEN],
|
|
||||||
stored_api_key: &HashedApiKey,
|
|
||||||
) -> CustomResult<(), errors::ApiKeyError> {
|
|
||||||
// Converting both hashes to `blake3::Hash` since it provides constant-time equality checks
|
|
||||||
let provided_api_key_hash = blake3::keyed_hash(key, self.0.peek().as_bytes());
|
|
||||||
let stored_api_key_hash = blake3::Hash::from_hex(&stored_api_key.0)
|
|
||||||
.into_report()
|
|
||||||
.change_context(errors::ApiKeyError::FailedToReadHashFromHex)?;
|
|
||||||
|
|
||||||
fp_utils::when(provided_api_key_hash != stored_api_key_hash, || {
|
|
||||||
Err(errors::ApiKeyError::HashVerificationFailed).into_report()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@ -165,7 +155,7 @@ pub async fn retrieve_api_key(
|
|||||||
key_id: &str,
|
key_id: &str,
|
||||||
) -> RouterResponse<api::RetrieveApiKeyResponse> {
|
) -> RouterResponse<api::RetrieveApiKeyResponse> {
|
||||||
let api_key = store
|
let api_key = store
|
||||||
.find_api_key_optional(key_id)
|
.find_api_key_by_key_id_optional(key_id)
|
||||||
.await
|
.await
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
|
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
|
||||||
.attach_printable("Failed to retrieve new API key")?
|
.attach_printable("Failed to retrieve new API key")?
|
||||||
@ -224,12 +214,30 @@ pub async fn list_api_keys(
|
|||||||
Ok(ApplicationResponse::Json(api_keys))
|
Ok(ApplicationResponse::Json(api_keys))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&str> for PlaintextApiKey {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
Self(s.to_owned().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for PlaintextApiKey {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(s.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<HashedApiKey> for storage::HashedApiKey {
|
impl From<HashedApiKey> for storage::HashedApiKey {
|
||||||
fn from(hashed_api_key: HashedApiKey) -> Self {
|
fn from(hashed_api_key: HashedApiKey) -> Self {
|
||||||
hashed_api_key.0.into()
|
hashed_api_key.0.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<storage::HashedApiKey> for HashedApiKey {
|
||||||
|
fn from(hashed_api_key: storage::HashedApiKey) -> Self {
|
||||||
|
Self(hashed_api_key.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
@ -251,8 +259,7 @@ mod tests {
|
|||||||
hashed_api_key.0.as_bytes()
|
hashed_api_key.0.as_bytes()
|
||||||
);
|
);
|
||||||
|
|
||||||
plaintext_api_key
|
let new_hashed_api_key = plaintext_api_key.keyed_hash(hash_key.peek());
|
||||||
.verify_hash(hash_key.peek(), &hashed_api_key)
|
assert_eq!(hashed_api_key, new_hashed_api_key)
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -414,11 +414,3 @@ pub enum WebhooksFlowError {
|
|||||||
#[error("Resource not found")]
|
#[error("Resource not found")]
|
||||||
ResourceNotFound,
|
ResourceNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ApiKeyError {
|
|
||||||
#[error("Failed to read API key hash from hexadecimal string")]
|
|
||||||
FailedToReadHashFromHex,
|
|
||||||
#[error("Failed to verify provided API key hash against stored API key hash")]
|
|
||||||
HashVerificationFailed,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -22,11 +22,16 @@ pub trait ApiKeyInterface {
|
|||||||
|
|
||||||
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError>;
|
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError>;
|
||||||
|
|
||||||
async fn find_api_key_optional(
|
async fn find_api_key_by_key_id_optional(
|
||||||
&self,
|
&self,
|
||||||
key_id: &str,
|
key_id: &str,
|
||||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn find_api_key_by_hash_optional(
|
||||||
|
&self,
|
||||||
|
hashed_api_key: storage::HashedApiKey,
|
||||||
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
|
||||||
|
|
||||||
async fn list_api_keys_by_merchant_id(
|
async fn list_api_keys_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
@ -69,7 +74,7 @@ impl ApiKeyInterface for Store {
|
|||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_api_key_optional(
|
async fn find_api_key_by_key_id_optional(
|
||||||
&self,
|
&self,
|
||||||
key_id: &str,
|
key_id: &str,
|
||||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||||
@ -80,6 +85,17 @@ impl ApiKeyInterface for Store {
|
|||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_api_key_by_hash_optional(
|
||||||
|
&self,
|
||||||
|
hashed_api_key: storage::HashedApiKey,
|
||||||
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||||
|
let conn = pg_connection(&self.master_pool).await?;
|
||||||
|
storage::ApiKey::find_optional_by_hashed_api_key(&conn, hashed_api_key)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
.into_report()
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_api_keys_by_merchant_id(
|
async fn list_api_keys_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
@ -118,7 +134,7 @@ impl ApiKeyInterface for MockDb {
|
|||||||
Err(errors::StorageError::MockDbError)?
|
Err(errors::StorageError::MockDbError)?
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn find_api_key_optional(
|
async fn find_api_key_by_key_id_optional(
|
||||||
&self,
|
&self,
|
||||||
_key_id: &str,
|
_key_id: &str,
|
||||||
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||||
@ -126,6 +142,14 @@ impl ApiKeyInterface for MockDb {
|
|||||||
Err(errors::StorageError::MockDbError)?
|
Err(errors::StorageError::MockDbError)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_api_key_by_hash_optional(
|
||||||
|
&self,
|
||||||
|
_hashed_api_key: storage::HashedApiKey,
|
||||||
|
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
|
||||||
|
// [#172]: Implement function for `MockDb`
|
||||||
|
Err(errors::StorageError::MockDbError)?
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_api_keys_by_merchant_id(
|
async fn list_api_keys_by_merchant_id(
|
||||||
&self,
|
&self,
|
||||||
_merchant_id: &str,
|
_merchant_id: &str,
|
||||||
|
|||||||
@ -5,11 +5,12 @@ use router_env::opentelemetry::{
|
|||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::create_counter;
|
||||||
|
|
||||||
pub static CONTEXT: Lazy<Context> = Lazy::new(Context::current);
|
pub static CONTEXT: Lazy<Context> = Lazy::new(Context::current);
|
||||||
static GLOBAL_METER: Lazy<Meter> = Lazy::new(|| global::meter("ROUTER_API"));
|
static GLOBAL_METER: Lazy<Meter> = Lazy::new(|| global::meter("ROUTER_API"));
|
||||||
|
|
||||||
pub(crate) static HEALTH_METRIC: Lazy<Counter<u64>> =
|
create_counter!(HEALTH_METRIC, GLOBAL_METER); // No. of health API hits
|
||||||
Lazy::new(|| GLOBAL_METER.u64_counter("HEALTH_API").init());
|
create_counter!(KV_MISS, GLOBAL_METER); // No. of KV misses
|
||||||
|
#[cfg(feature = "kms")]
|
||||||
pub(crate) static KV_MISS: Lazy<Counter<u64>> =
|
create_counter!(AWS_KMS_FAILURES, GLOBAL_METER); // No. of AWS KMS API failures
|
||||||
Lazy::new(|| GLOBAL_METER.u64_counter("KV_MISS").init());
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ static PT_METER: Lazy<Meter> = Lazy::new(|| global::meter("PROCESS_TRACKER"));
|
|||||||
pub(crate) static CONSUMER_STATS: Lazy<Histogram<f64>> =
|
pub(crate) static CONSUMER_STATS: Lazy<Histogram<f64>> =
|
||||||
Lazy::new(|| PT_METER.f64_histogram("CONSUMER_OPS").init());
|
Lazy::new(|| PT_METER.f64_histogram("CONSUMER_OPS").init());
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! create_counter {
|
macro_rules! create_counter {
|
||||||
($name:ident, $meter:ident) => {
|
($name:ident, $meter:ident) => {
|
||||||
pub(crate) static $name: Lazy<Counter<u64>> =
|
pub(crate) static $name: Lazy<Counter<u64>> =
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
use actix_web::http::header::HeaderMap;
|
use actix_web::http::header::HeaderMap;
|
||||||
use api_models::{payment_methods::PaymentMethodListRequest, payments::PaymentsRequest};
|
use api_models::{payment_methods::PaymentMethodListRequest, payments::PaymentsRequest};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use common_utils::date_time;
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
|
use masking::PeekInterface;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors::{self, RouterResult},
|
core::{
|
||||||
|
api_keys,
|
||||||
|
errors::{self, RouterResult},
|
||||||
|
},
|
||||||
db::StorageInterface,
|
db::StorageInterface,
|
||||||
routes::{app::AppStateInfo, AppState},
|
routes::{app::AppStateInfo, AppState},
|
||||||
services::api,
|
services::api,
|
||||||
@ -38,11 +43,45 @@ where
|
|||||||
request_headers: &HeaderMap,
|
request_headers: &HeaderMap,
|
||||||
state: &A,
|
state: &A,
|
||||||
) -> RouterResult<storage::MerchantAccount> {
|
) -> RouterResult<storage::MerchantAccount> {
|
||||||
let api_key =
|
let api_key = get_api_key(request_headers)
|
||||||
get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?;
|
.change_context(errors::ApiErrorResponse::Unauthorized)?
|
||||||
|
.trim();
|
||||||
|
if api_key.is_empty() {
|
||||||
|
return Err(errors::ApiErrorResponse::Unauthorized)
|
||||||
|
.into_report()
|
||||||
|
.attach_printable("API key is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let api_key = api_keys::PlaintextApiKey::from(api_key);
|
||||||
|
let hash_key = {
|
||||||
|
let config = state.conf();
|
||||||
|
api_keys::HASH_KEY
|
||||||
|
.get_or_try_init(|| api_keys::get_hash_key(&config.api_keys))
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
.store()
|
.store()
|
||||||
.find_merchant_account_by_api_key(api_key)
|
.find_merchant_account_by_merchant_id(&stored_api_key.merchant_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
if e.current_context().is_db_not_found() {
|
if e.current_context().is_db_not_found() {
|
||||||
|
|||||||
@ -81,6 +81,12 @@ impl From<ApiKeyUpdate> for ApiKeyUpdateInternal {
|
|||||||
#[diesel(sql_type = diesel::sql_types::Text)]
|
#[diesel(sql_type = diesel::sql_types::Text)]
|
||||||
pub struct HashedApiKey(String);
|
pub struct HashedApiKey(String);
|
||||||
|
|
||||||
|
impl HashedApiKey {
|
||||||
|
pub fn into_inner(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<String> for HashedApiKey {
|
impl From<String> for HashedApiKey {
|
||||||
fn from(hashed_api_key: String) -> Self {
|
fn from(hashed_api_key: String) -> Self {
|
||||||
Self(hashed_api_key)
|
Self(hashed_api_key)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ use router_env::{instrument, tracing};
|
|||||||
|
|
||||||
use super::generics;
|
use super::generics;
|
||||||
use crate::{
|
use crate::{
|
||||||
api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, ApiKeyUpdateInternal},
|
api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, ApiKeyUpdateInternal, HashedApiKey},
|
||||||
errors,
|
errors,
|
||||||
schema::api_keys::dsl,
|
schema::api_keys::dsl,
|
||||||
PgPooledConn, StorageResult,
|
PgPooledConn, StorageResult,
|
||||||
@ -65,6 +65,18 @@ impl ApiKey {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(conn))]
|
||||||
|
pub async fn find_optional_by_hashed_api_key(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
hashed_api_key: HashedApiKey,
|
||||||
|
) -> StorageResult<Option<Self>> {
|
||||||
|
generics::generic_find_one_optional::<<Self as HasTable>::Table, _, _>(
|
||||||
|
conn,
|
||||||
|
dsl::hashed_api_key.eq(hashed_api_key),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(conn))]
|
#[instrument(skip(conn))]
|
||||||
pub async fn find_by_merchant_id(
|
pub async fn find_by_merchant_id(
|
||||||
conn: &PgPooledConn,
|
conn: &PgPooledConn,
|
||||||
|
|||||||
Reference in New Issue
Block a user