From ecacefd2988c6314c7534b3cf29afaf5c995c587 Mon Sep 17 00:00:00 2001 From: Rachit Naithani <81706961+racnan@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:28:40 +0530 Subject: [PATCH] feat(router): Added JWT authentication (#346) --- Cargo.lock | 47 ++++++++++++ crates/router/Cargo.toml | 1 + .../router/src/compatibility/stripe/errors.rs | 1 + .../src/core/errors/api_error_response.rs | 19 +++-- crates/router/src/services/authentication.rs | 72 +++++++++++++++++++ 5 files changed, 134 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c21a6c7e20..6aa461e603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2103,6 +2103,20 @@ dependencies = [ "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]] name = "language-tags" version = "0.3.2" @@ -2368,6 +2382,17 @@ dependencies = [ "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]] name = "num-integer" version = "0.1.45" @@ -2625,6 +2650,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "percent-encoding" version = "2.2.0" @@ -3105,6 +3139,7 @@ dependencies = [ "hex", "http", "josekit", + "jsonwebtoken", "literally", "masking", "maud", @@ -3518,6 +3553,18 @@ dependencies = [ "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]] name = "slab" version = "0.4.7" diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 19fef4356f..d0aa0d0709 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -44,6 +44,7 @@ futures = "0.3.25" hex = "0.4.3" http = "0.2.8" josekit = "0.8.1" +jsonwebtoken = "8.2.0" literally = "0.1.3" maud = { version = "0.24", features = ["actix-web"] } mimalloc = { version = "0.1", optional = true } diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index f16ced793a..30ac8b608f 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -320,6 +320,7 @@ impl From for StripeErrorCode { fn from(value: errors::ApiErrorResponse) -> Self { match value { errors::ApiErrorResponse::Unauthorized + | errors::ApiErrorResponse::InvalidJwtToken | errors::ApiErrorResponse::InvalidEphermeralKey => Self::Unauthorized, errors::ApiErrorResponse::InvalidRequestUrl | errors::ApiErrorResponse::InvalidHttpMethod => Self::InvalidRequestUrl, diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index e02ee36a3e..6ebe7032ac 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -67,6 +67,11 @@ pub enum ApiErrorResponse { /// information doesn't satisfy a condition. #[error(error_type = ErrorType::InvalidRequestError, code = "IR_10", message = "{message}")] 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.")] PaymentAuthorizationFailed { data: Option }, @@ -146,18 +151,20 @@ impl actix_web::ResponseError for ApiErrorResponse { use reqwest::StatusCode; match self { - Self::Unauthorized | Self::InvalidEphermeralKey => StatusCode::UNAUTHORIZED, // 401 - Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404 - Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405 + Self::Unauthorized | Self::InvalidEphermeralKey | Self::InvalidJwtToken => { + StatusCode::UNAUTHORIZED + } // 401 + Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404 + Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405 Self::MissingRequiredField { .. } | Self::InvalidDataValue { .. } => { StatusCode::BAD_REQUEST } // 400 Self::InvalidDataFormat { .. } | Self::InvalidRequestData { .. } => { StatusCode::UNPROCESSABLE_ENTITY } // 422 - Self::RefundAmountExceedsPaymentAmount => StatusCode::BAD_REQUEST, // 400 - Self::MaximumRefundCount => StatusCode::BAD_REQUEST, // 400 - Self::PreconditionFailed { .. } => StatusCode::BAD_REQUEST, // 400 + Self::RefundAmountExceedsPaymentAmount => StatusCode::BAD_REQUEST, // 400 + Self::MaximumRefundCount => StatusCode::BAD_REQUEST, // 400 + Self::PreconditionFailed { .. } => StatusCode::BAD_REQUEST, // 400 Self::PaymentAuthorizationFailed { .. } | Self::PaymentAuthenticationFailed { .. } diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 01218d28cb..a821bf1dd2 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -2,6 +2,7 @@ use actix_web::http::header::HeaderMap; use api_models::{payment_methods::ListPaymentMethodRequest, payments::PaymentsRequest}; use async_trait::async_trait; use error_stack::{report, IntoReport, ResultExt}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use crate::{ core::errors::{self, RouterResult, StorageErrorExt}, @@ -101,6 +102,32 @@ impl AuthenticateAndFetch for PublishableKeyAuth { } } +#[derive(Debug)] +pub struct JWTAuth; + +#[derive(serde::Deserialize)] +struct JwtAuthPayloadFetchMerchantAccount { + merchant_id: String, +} + +#[async_trait] +impl AuthenticateAndFetch for JWTAuth { + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &AppState, + ) -> RouterResult { + let mut token = get_jwt(request_headers)?; + token = strip_jwt_token(token)?; + let payload = decode_jwt::(token, state)?; + state + .store + .find_merchant_account_by_merchant_id(&payload.merchant_id) + .await + .change_context(errors::ApiErrorResponse::InvalidJwtToken) + } +} + pub trait ClientSecretFetch { fn get_client_secret(&self) -> Option<&String>; } @@ -117,6 +144,19 @@ impl ClientSecretFetch for ListPaymentMethodRequest { } } +pub fn jwt_auth_or( + headers: &HeaderMap, + default_auth: Box>, +) -> Box> +where + JWTAuth: AuthenticateAndFetch, +{ + if is_jwt_auth(headers) { + return Box::new(JWTAuth); + } + default_auth +} + pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( @@ -183,6 +223,22 @@ pub async fn is_ephemeral_auth( 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(token: &str, state: &AppState) -> RouterResult +where + T: serde::de::DeserializeOwned, +{ + let secret = state.conf.keys.jwt_secret.as_bytes(); + let key = DecodingKey::from_secret(secret); + decode::(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> { headers .get("api-key") @@ -192,3 +248,19 @@ fn get_api_key(headers: &HeaderMap) -> RouterResult<&str> { .change_context(errors::ApiErrorResponse::InternalServerError) .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()) +}