mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(router): Added JWT authentication (#346)
This commit is contained in:
47
Cargo.lock
generated
47
Cargo.lock
generated
@ -2103,6 +2103,20 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "8.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09f4f04699947111ec1733e71778d763555737579e44b85844cae8e1940a1828"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "language-tags"
|
name = "language-tags"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -2368,6 +2382,17 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.45"
|
version = "0.1.45"
|
||||||
@ -2625,6 +2650,15 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@ -3105,6 +3139,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"http",
|
"http",
|
||||||
"josekit",
|
"josekit",
|
||||||
|
"jsonwebtoken",
|
||||||
"literally",
|
"literally",
|
||||||
"masking",
|
"masking",
|
||||||
"maud",
|
"maud",
|
||||||
@ -3518,6 +3553,18 @@ dependencies = [
|
|||||||
"outref",
|
"outref",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
|
|||||||
@ -44,6 +44,7 @@ futures = "0.3.25"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
http = "0.2.8"
|
http = "0.2.8"
|
||||||
josekit = "0.8.1"
|
josekit = "0.8.1"
|
||||||
|
jsonwebtoken = "8.2.0"
|
||||||
literally = "0.1.3"
|
literally = "0.1.3"
|
||||||
maud = { version = "0.24", features = ["actix-web"] }
|
maud = { version = "0.24", features = ["actix-web"] }
|
||||||
mimalloc = { version = "0.1", optional = true }
|
mimalloc = { version = "0.1", optional = true }
|
||||||
|
|||||||
@ -320,6 +320,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
|
|||||||
fn from(value: errors::ApiErrorResponse) -> Self {
|
fn from(value: errors::ApiErrorResponse) -> Self {
|
||||||
match value {
|
match value {
|
||||||
errors::ApiErrorResponse::Unauthorized
|
errors::ApiErrorResponse::Unauthorized
|
||||||
|
| errors::ApiErrorResponse::InvalidJwtToken
|
||||||
| errors::ApiErrorResponse::InvalidEphermeralKey => Self::Unauthorized,
|
| errors::ApiErrorResponse::InvalidEphermeralKey => Self::Unauthorized,
|
||||||
errors::ApiErrorResponse::InvalidRequestUrl
|
errors::ApiErrorResponse::InvalidRequestUrl
|
||||||
| errors::ApiErrorResponse::InvalidHttpMethod => Self::InvalidRequestUrl,
|
| errors::ApiErrorResponse::InvalidHttpMethod => Self::InvalidRequestUrl,
|
||||||
|
|||||||
@ -67,6 +67,11 @@ pub enum ApiErrorResponse {
|
|||||||
/// information doesn't satisfy a condition.
|
/// information doesn't satisfy a condition.
|
||||||
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_10", message = "{message}")]
|
#[error(error_type = ErrorType::InvalidRequestError, code = "IR_10", message = "{message}")]
|
||||||
PreconditionFailed { message: String },
|
PreconditionFailed { message: String },
|
||||||
|
#[error(
|
||||||
|
error_type = ErrorType::InvalidRequestError, code = "IR_11",
|
||||||
|
message = "Access forbidden, invalid JWT token was used."
|
||||||
|
)]
|
||||||
|
InvalidJwtToken,
|
||||||
|
|
||||||
#[error(error_type = ErrorType::ProcessingError, code = "CE_01", message = "Payment failed while processing with connector. Retry payment.")]
|
#[error(error_type = ErrorType::ProcessingError, code = "CE_01", message = "Payment failed while processing with connector. Retry payment.")]
|
||||||
PaymentAuthorizationFailed { data: Option<serde_json::Value> },
|
PaymentAuthorizationFailed { data: Option<serde_json::Value> },
|
||||||
@ -146,7 +151,9 @@ impl actix_web::ResponseError for ApiErrorResponse {
|
|||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Unauthorized | Self::InvalidEphermeralKey => StatusCode::UNAUTHORIZED, // 401
|
Self::Unauthorized | Self::InvalidEphermeralKey | Self::InvalidJwtToken => {
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
} // 401
|
||||||
Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
|
Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
|
||||||
Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
|
Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
|
||||||
Self::MissingRequiredField { .. } | Self::InvalidDataValue { .. } => {
|
Self::MissingRequiredField { .. } | Self::InvalidDataValue { .. } => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ use actix_web::http::header::HeaderMap;
|
|||||||
use api_models::{payment_methods::ListPaymentMethodRequest, payments::PaymentsRequest};
|
use api_models::{payment_methods::ListPaymentMethodRequest, payments::PaymentsRequest};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors::{self, RouterResult, StorageErrorExt},
|
core::errors::{self, RouterResult, StorageErrorExt},
|
||||||
@ -101,6 +102,32 @@ impl AuthenticateAndFetch<storage::MerchantAccount> for PublishableKeyAuth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct JWTAuth;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct JwtAuthPayloadFetchMerchantAccount {
|
||||||
|
merchant_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthenticateAndFetch<storage::MerchantAccount> for JWTAuth {
|
||||||
|
async fn authenticate_and_fetch(
|
||||||
|
&self,
|
||||||
|
request_headers: &HeaderMap,
|
||||||
|
state: &AppState,
|
||||||
|
) -> RouterResult<storage::MerchantAccount> {
|
||||||
|
let mut token = get_jwt(request_headers)?;
|
||||||
|
token = strip_jwt_token(token)?;
|
||||||
|
let payload = decode_jwt::<JwtAuthPayloadFetchMerchantAccount>(token, state)?;
|
||||||
|
state
|
||||||
|
.store
|
||||||
|
.find_merchant_account_by_merchant_id(&payload.merchant_id)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InvalidJwtToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ClientSecretFetch {
|
pub trait ClientSecretFetch {
|
||||||
fn get_client_secret(&self) -> Option<&String>;
|
fn get_client_secret(&self) -> Option<&String>;
|
||||||
}
|
}
|
||||||
@ -117,6 +144,19 @@ impl ClientSecretFetch for ListPaymentMethodRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn jwt_auth_or<T>(
|
||||||
|
headers: &HeaderMap,
|
||||||
|
default_auth: Box<dyn AuthenticateAndFetch<T>>,
|
||||||
|
) -> Box<dyn AuthenticateAndFetch<T>>
|
||||||
|
where
|
||||||
|
JWTAuth: AuthenticateAndFetch<T>,
|
||||||
|
{
|
||||||
|
if is_jwt_auth(headers) {
|
||||||
|
return Box::new(JWTAuth);
|
||||||
|
}
|
||||||
|
default_auth
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_auth_type_and_flow(
|
pub fn get_auth_type_and_flow(
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
) -> RouterResult<(
|
) -> RouterResult<(
|
||||||
@ -183,6 +223,22 @@ pub async fn is_ephemeral_auth(
|
|||||||
Ok(Box::new(MerchantIdAuth(ephemeral_key.merchant_id)))
|
Ok(Box::new(MerchantIdAuth(ephemeral_key.merchant_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_jwt_auth(headers: &HeaderMap) -> bool {
|
||||||
|
headers.get(crate::headers::AUTHORIZATION).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_jwt<T>(token: &str, state: &AppState) -> RouterResult<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let secret = state.conf.keys.jwt_secret.as_bytes();
|
||||||
|
let key = DecodingKey::from_secret(secret);
|
||||||
|
decode::<T>(token, &key, &Validation::new(Algorithm::HS256))
|
||||||
|
.map(|decoded| decoded.claims)
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ApiErrorResponse::InvalidJwtToken)
|
||||||
|
}
|
||||||
|
|
||||||
fn get_api_key(headers: &HeaderMap) -> RouterResult<&str> {
|
fn get_api_key(headers: &HeaderMap) -> RouterResult<&str> {
|
||||||
headers
|
headers
|
||||||
.get("api-key")
|
.get("api-key")
|
||||||
@ -192,3 +248,19 @@ fn get_api_key(headers: &HeaderMap) -> RouterResult<&str> {
|
|||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("Failed to convert API key to string")
|
.attach_printable("Failed to convert API key to string")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_jwt(headers: &HeaderMap) -> RouterResult<&str> {
|
||||||
|
headers
|
||||||
|
.get(crate::headers::AUTHORIZATION)
|
||||||
|
.get_required_value(crate::headers::AUTHORIZATION)?
|
||||||
|
.to_str()
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Failed to convert JWT token to string")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_jwt_token(token: &str) -> RouterResult<&str> {
|
||||||
|
token
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.ok_or_else(|| errors::ApiErrorResponse::InvalidJwtToken.into())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user