From 57fbc6e399900bf06fc2f566d9f39e34ee21380d Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 01/13] feat: multiple auths per AsyncAuthorizationService --- jwt-authorizer/src/error.rs | 7 ++++++ jwt-authorizer/src/layer.rs | 45 +++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/jwt-authorizer/src/error.rs b/jwt-authorizer/src/error.rs index 5ad664c..6cf663f 100644 --- a/jwt-authorizer/src/error.rs +++ b/jwt-authorizer/src/error.rs @@ -55,6 +55,9 @@ pub enum AuthError { #[error("Invalid Claim")] InvalidClaims(), + + #[error("No Authorizer")] + NoAuthorizer(), } fn response_wwwauth(status: StatusCode, bearer: &str) -> Response { @@ -166,6 +169,10 @@ impl IntoResponse for AuthError { debug!("AuthErrors::InvalidClaims"); response_wwwauth(StatusCode::FORBIDDEN, "error=\"insufficient_scope\"") } + AuthError::NoAuthorizer() => { + debug!("AuthErrors::NoAuthorizer"); + response_wwwauth(StatusCode::FORBIDDEN, "error=\"invalid_token\"") + } } } } diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index e37f82c..6583868 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -3,6 +3,7 @@ use futures_core::ready; use futures_util::future::BoxFuture; use headers::authorization::Bearer; use headers::{Authorization, HeaderMapExt}; +use jsonwebtoken::TokenData; use pin_project::pin_project; use serde::de::DeserializeOwned; use std::future::Future; @@ -209,10 +210,9 @@ where type Future = BoxFuture<'static, Result, AuthError>>; fn authorize(&self, mut request: Request) -> Self::Future { - let authorizer = self.auth.clone(); + // TODO: extract token per authorizer (jwt_source shloud be per authorizer) let h = request.headers(); - - let token = match &self.jwt_source { + let token_o = match &self.jwt_source { layer::JwtSource::AuthorizationHeader => { let bearer_o: Option> = h.typed_get(); bearer_o.map(|b| String::from(b.0.token())) @@ -221,17 +221,30 @@ where .typed_get::() .and_then(|c| c.get(name.as_str()).map(String::from)), }; - Box::pin(async move { - if let Some(token) = token { - authorizer.check_auth(token.as_str()).await.map(|token_data| { - // Set `token_data` as a request extension so it can be accessed by other - // services down the stack. - request.extensions_mut().insert(token_data); - request - }) + let authorizers: Vec>> = self.auths.iter().map(|a| a.clone()).collect(); + + Box::pin(async move { + if let Some(token) = token_o { + let mut token_data: Result, AuthError> = Err(AuthError::NoAuthorizer()); + for auth in authorizers { + token_data = auth.check_auth(token.as_str()).await; + if token_data.is_ok() { + break; + } + } + match token_data { + Ok(tdata) => { + // Set `token_data` as a request extension so it can be accessed by other + // services down the stack. + request.extensions_mut().insert(tdata); + + return Ok(request); + } + Err(err) => return Err(err), // TODO: error containing all errors (not just the last one) + } } else { - Err(AuthError::MissingToken()) + return Err(AuthError::MissingToken()); } }) } @@ -291,7 +304,7 @@ where C: Clone + DeserializeOwned + Send + Sync, { pub inner: S, - pub auth: Arc>, + pub auths: Vec>>, pub jwt_source: JwtSource, } @@ -322,7 +335,11 @@ where /// /// The `Authorization` header is required to have the value provided. pub fn new(inner: S, auth: Arc>, jwt_source: JwtSource) -> AsyncAuthorizationService { - Self { inner, auth, jwt_source } + Self { + inner, + auths: vec![auth], + jwt_source, + } } } From d7d945c075f672b1250c35c477ddece49ac6accb Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 02/13] feat: ToAuthorizationLayer --- demo-server/src/main.rs | 6 +- jwt-authorizer/src/authorizer.rs | 30 ++++----- jwt-authorizer/src/layer.rs | 110 ++++++++++++++++++++++++++++--- jwt-authorizer/src/lib.rs | 2 +- jwt-authorizer/tests/tests.rs | 4 +- 5 files changed, 124 insertions(+), 28 deletions(-) diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs index a1e7bfe..91f7879 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -1,5 +1,7 @@ use axum::{routing::get, Router}; -use jwt_authorizer::{error::InitError, AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; +use jwt_authorizer::{ + error::InitError, AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, ToAuthorizationLayer, +}; use serde::Deserialize; use std::net::SocketAddr; use tower_http::trace::TraceLayer; @@ -49,7 +51,7 @@ async fn main() -> Result<(), InitError> { let api = Router::new() .route("/protected", get(protected)) // adding the authorizer layer - .layer(jwt_auth.layer().await?); + .layer(jwt_auth.to_layer().await?); let app = Router::new() // public endpoint diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index f49b6b1..66b5980 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -65,7 +65,7 @@ where C: DeserializeOwned + Clone + Send + Sync, { pub(crate) async fn build( - key_source_type: &KeySourceType, + key_source_type: KeySourceType, claims_checker: Option>, refresh: Option, validation: crate::validation::Validation, @@ -157,7 +157,7 @@ where } KeySourceType::JwksString(jwks_str) => { // TODO: expose it in JwtAuthorizer or remove - let set: JwkSet = serde_json::from_str(jwks_str)?; + let set: JwkSet = serde_json::from_str(jwks_str.as_str())?; // TODO: replace [0] by kid/alg search let k = KeyData::from_jwk(&set.keys[0]).map_err(InitError::KeyDecodingError)?; Authorizer { @@ -167,7 +167,7 @@ where } } KeySourceType::Jwks(url) => { - let jwks_url = Url::parse(url).map_err(|e| InitError::JwksUrlError(e.to_string()))?; + let jwks_url = Url::parse(url.as_str()).map_err(|e| InitError::JwksUrlError(e.to_string()))?; let key_store_manager = KeyStoreManager::new(jwks_url, refresh.unwrap_or_default()); Authorizer { key_source: KeySource::KeyStoreSource(key_store_manager), @@ -176,7 +176,7 @@ where } } KeySourceType::Discovery(issuer_url) => { - let jwks_url = Url::parse(&oidc::discover_jwks(issuer_url).await?) + let jwks_url = Url::parse(&oidc::discover_jwks(issuer_url.as_str()).await?) .map_err(|e| InitError::JwksUrlError(e.to_string()))?; let key_store_manager = KeyStoreManager::new(jwks_url, refresh.unwrap_or_default()); @@ -219,7 +219,7 @@ mod tests { #[tokio::test] async fn build_from_secret() { let h = Header::new(Algorithm::HS256); - let a = Authorizer::::build(&KeySourceType::Secret("xxxxxx".to_owned()), None, None, Validation::new()) + let a = Authorizer::::build(KeySourceType::Secret("xxxxxx".to_owned()), None, None, Validation::new()) .await .unwrap(); let k = a.key_source.get_key(h); @@ -238,7 +238,7 @@ mod tests { "e": "AQAB" }]} "#; - let a = Authorizer::::build(&KeySourceType::JwksString(jwks.to_owned()), None, None, Validation::new()) + let a = Authorizer::::build(KeySourceType::JwksString(jwks.to_owned()), None, None, Validation::new()) .await .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::RS256)); @@ -248,7 +248,7 @@ mod tests { #[tokio::test] async fn build_from_file() { let a = Authorizer::::build( - &KeySourceType::RSA("../config/rsa-public1.pem".to_owned()), + KeySourceType::RSA("../config/rsa-public1.pem".to_owned()), None, None, Validation::new(), @@ -259,7 +259,7 @@ mod tests { assert!(k.await.is_ok()); let a = Authorizer::::build( - &KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()), + KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()), None, None, Validation::new(), @@ -270,7 +270,7 @@ mod tests { assert!(k.await.is_ok()); let a = Authorizer::::build( - &KeySourceType::ED("../config/ed25519-public1.pem".to_owned()), + KeySourceType::ED("../config/ed25519-public1.pem".to_owned()), None, None, Validation::new(), @@ -284,7 +284,7 @@ mod tests { #[tokio::test] async fn build_from_text() { let a = Authorizer::::build( - &KeySourceType::RSAString(include_str!("../../config/rsa-public1.pem").to_owned()), + KeySourceType::RSAString(include_str!("../../config/rsa-public1.pem").to_owned()), None, None, Validation::new(), @@ -295,7 +295,7 @@ mod tests { assert!(k.await.is_ok()); let a = Authorizer::::build( - &KeySourceType::ECString(include_str!("../../config/ecdsa-public1.pem").to_owned()), + KeySourceType::ECString(include_str!("../../config/ecdsa-public1.pem").to_owned()), None, None, Validation::new(), @@ -306,7 +306,7 @@ mod tests { assert!(k.await.is_ok()); let a = Authorizer::::build( - &KeySourceType::EDString(include_str!("../../config/ed25519-public1.pem").to_owned()), + KeySourceType::EDString(include_str!("../../config/ed25519-public1.pem").to_owned()), None, None, Validation::new(), @@ -320,7 +320,7 @@ mod tests { #[tokio::test] async fn build_file_errors() { let a = Authorizer::::build( - &KeySourceType::RSA("./config/does-not-exist.pem".to_owned()), + KeySourceType::RSA("./config/does-not-exist.pem".to_owned()), None, None, Validation::new(), @@ -333,7 +333,7 @@ mod tests { #[tokio::test] async fn build_jwks_url_error() { let a = - Authorizer::::build(&KeySourceType::Jwks("://xxxx".to_owned()), None, None, Validation::default()).await; + Authorizer::::build(KeySourceType::Jwks("://xxxx".to_owned()), None, None, Validation::default()).await; println!("{:?}", a.as_ref().err()); assert!(a.is_err()); } @@ -341,7 +341,7 @@ mod tests { #[tokio::test] async fn build_discovery_url_error() { let a = Authorizer::::build( - &KeySourceType::Discovery("://xxxx".to_owned()), + KeySourceType::Discovery("://xxxx".to_owned()), None, None, Validation::default(), diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index 6583868..bad0b31 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -1,6 +1,8 @@ +use axum::async_trait; use axum::http::Request; use futures_core::ready; use futures_util::future::BoxFuture; +use futures_util::stream::{FuturesUnordered, StreamExt}; use headers::authorization::Bearer; use headers::{Authorization, HeaderMapExt}; use jsonwebtoken::TokenData; @@ -184,12 +186,63 @@ where } /// Build axum layer + #[deprecated(since = "0.10.0", note = "please use `to_layer()` instead")] pub async fn layer(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); - let auth = Arc::new(Authorizer::build(&self.key_source_type, self.claims_checker, self.refresh, val).await?); - Ok(AsyncAuthorizationLayer::new(auth, self.jwt_source)) + let auth = Arc::new(Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val).await?); + Ok(AsyncAuthorizationLayer::new(vec![auth], self.jwt_source)) } } + +#[async_trait] +impl ToAuthorizationLayer for JwtAuthorizer +where + C: Clone + DeserializeOwned + Send + Sync, +{ + async fn to_layer(self) -> Result, InitError> { + let val = self.validation.unwrap_or_default(); + let auth = Arc::new(Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val).await?); + Ok(AsyncAuthorizationLayer::new(vec![auth], self.jwt_source)) + } +} + +#[async_trait] +impl ToAuthorizationLayer for Vec> +where + C: Clone + DeserializeOwned + Send + Sync, +{ + async fn to_layer(self) -> Result, InitError> { + let mut errs = Vec::::new(); + let mut auths = Vec::>>::new(); + let mut auths_futs: FuturesUnordered<_> = self + .into_iter() + .map(|a| { + Authorizer::build( + a.key_source_type, + a.claims_checker, + a.refresh, + a.validation.unwrap_or_default(), + ) + }) + .collect(); + + while let Some(a) = auths_futs.next().await { + match a { + Ok(res) => auths.push(Arc::new(res)), + Err(err) => errs.push(err), + } + } + + if let Some(e) = errs.into_iter().next() { + // TODO: composite build error (containing all errors) + Err(e) + } else { + // TODO: jwt_source per Authorizer + Ok(AsyncAuthorizationLayer::new(auths, JwtSource::AuthorizationHeader)) + } + } +} + /// Trait for authorizing requests. pub trait AsyncAuthorizer { type RequestBody; @@ -257,7 +310,7 @@ pub struct AsyncAuthorizationLayer where C: Clone + DeserializeOwned + Send, { - auth: Arc>, + auths: Vec>>, jwt_source: JwtSource, } @@ -265,8 +318,8 @@ impl AsyncAuthorizationLayer where C: Clone + DeserializeOwned + Send, { - pub fn new(auth: Arc>, jwt_source: JwtSource) -> AsyncAuthorizationLayer { - Self { auth, jwt_source } + pub fn new(auths: Vec>>, jwt_source: JwtSource) -> AsyncAuthorizationLayer { + Self { auths, jwt_source } } } @@ -277,10 +330,18 @@ where type Service = AsyncAuthorizationService; fn layer(&self, inner: S) -> Self::Service { - AsyncAuthorizationService::new(inner, self.auth.clone(), self.jwt_source.clone()) + AsyncAuthorizationService::new(inner, self.auths.clone(), self.jwt_source.clone()) } } +#[async_trait] +pub trait ToAuthorizationLayer +where + C: Clone + DeserializeOwned + Send, +{ + async fn to_layer(self) -> Result, InitError>; +} + // ---------- AsyncAuthorizationService -------- /// Source of the bearer token @@ -334,10 +395,10 @@ where /// Authorize requests using a custom scheme. /// /// The `Authorization` header is required to have the value provided. - pub fn new(inner: S, auth: Arc>, jwt_source: JwtSource) -> AsyncAuthorizationService { + pub fn new(inner: S, auths: Vec>>, jwt_source: JwtSource) -> AsyncAuthorizationService { Self { inner, - auths: vec![auth], + auths: auths, jwt_source, } } @@ -431,3 +492,36 @@ where } } } + +#[cfg(test)] +mod tests { + use crate::{JwtAuthorizer, ToAuthorizationLayer}; + + #[tokio::test] + async fn jwt_auth_to_layer() { + let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); + let layer = auth1.to_layer().await; + assert!(layer.is_ok()); + } + + #[tokio::test] + async fn vec_to_layer() { + let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); + let auth2: JwtAuthorizer = JwtAuthorizer::from_secret("bbb"); + let av = vec![auth1, auth2]; + let layer = av.to_layer().await; + assert!(layer.is_ok()); + } + + #[tokio::test] + async fn vec_to_layer_errors() { + let auth1: JwtAuthorizer = JwtAuthorizer::from_ec_pem("aaa"); + let auth2: JwtAuthorizer = JwtAuthorizer::from_ed_pem("bbb"); + let av = vec![auth1, auth2]; + let layer = av.to_layer().await; + assert!(layer.is_err()); + if let Err(err) = layer { + assert_eq!(err.to_string(), "No such file or directory (os error 2)"); + } + } +} diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index abc1254..e35bbfb 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -8,7 +8,7 @@ use serde::de::DeserializeOwned; pub use self::error::AuthError; pub use claims::{NumericDate, OneOrArray, RegisteredClaims}; pub use jwks::key_store_manager::{Refresh, RefreshStrategy}; -pub use layer::JwtAuthorizer; +pub use layer::{JwtAuthorizer, ToAuthorizationLayer}; pub use validation::Validation; pub mod authorizer; diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index 37641c4..e2cf54a 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -12,7 +12,7 @@ mod tests { BoxError, Router, }; use http::{header, HeaderValue}; - use jwt_authorizer::{layer::JwtSource, validation::Validation, JwtAuthorizer, JwtClaims}; + use jwt_authorizer::{layer::JwtSource, validation::Validation, JwtAuthorizer, JwtClaims, ToAuthorizationLayer}; use serde::Deserialize; use tower::{util::MapErrLayer, ServiceExt}; @@ -32,7 +32,7 @@ mod tests { tower::buffer::BufferLayer::new(1), MapErrLayer::new(|e: BoxError| -> Infallible { panic!("{}", e) }), ), - jwt_auth.layer().await.unwrap(), + jwt_auth.to_layer().await.unwrap(), ), ), ) From 55c4f7cc1650e6ae89295b82eca52fce0bc205d1 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 03/13] chore: clippy --- jwt-authorizer/src/layer.rs | 10 +++++----- jwt-authorizer/tests/integration_tests.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index bad0b31..3a8e36b 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -275,7 +275,7 @@ where .and_then(|c| c.get(name.as_str()).map(String::from)), }; - let authorizers: Vec>> = self.auths.iter().map(|a| a.clone()).collect(); + let authorizers: Vec>> = self.auths.iter().cloned().collect(); Box::pin(async move { if let Some(token) = token_o { @@ -292,12 +292,12 @@ where // services down the stack. request.extensions_mut().insert(tdata); - return Ok(request); + Ok(request) } - Err(err) => return Err(err), // TODO: error containing all errors (not just the last one) + Err(err) => Err(err), // TODO: error containing all errors (not just the last one) } } else { - return Err(AuthError::MissingToken()); + Err(AuthError::MissingToken()) } }) } @@ -398,7 +398,7 @@ where pub fn new(inner: S, auths: Vec>>, jwt_source: JwtSource) -> AsyncAuthorizationService { Self { inner, - auths: auths, + auths, jwt_source, } } diff --git a/jwt-authorizer/tests/integration_tests.rs b/jwt-authorizer/tests/integration_tests.rs index a9ac2ef..36b7704 100644 --- a/jwt-authorizer/tests/integration_tests.rs +++ b/jwt-authorizer/tests/integration_tests.rs @@ -11,7 +11,7 @@ use std::{ use axum::{response::Response, routing::get, Json, Router}; use http::{header::AUTHORIZATION, Request, StatusCode}; use hyper::Body; -use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; +use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, ToAuthorizationLayer}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -104,7 +104,7 @@ async fn app(jwt_auth: JwtAuthorizer) -> Router { let protected_route: Router = Router::new() .route("/protected", get(protected_handler)) .route("/protected-with-user", get(protected_with_user)) - .layer(jwt_auth.layer().await.unwrap()); + .layer(jwt_auth.to_layer().await.unwrap()); Router::new().merge(pub_route).merge(protected_route) } From 603c042ee3a18223faa3740a5c772d9329c454c1 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 04/13] refactor: move jwt_source to Authorizer allows multiple sorces with multiple authorizers --- jwt-authorizer/src/authorizer.rs | 71 +++++++++++++++++++++++---- jwt-authorizer/src/layer.rs | 82 ++++++++++++++------------------ 2 files changed, 98 insertions(+), 55 deletions(-) diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index 66b5980..f5384a3 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -1,5 +1,7 @@ use std::{io::Read, sync::Arc}; +use headers::{authorization::Bearer, Authorization, HeaderMapExt}; +use http::HeaderMap; use jsonwebtoken::{decode, decode_header, jwk::JwkSet, Algorithm, DecodingKey, TokenData}; use reqwest::Url; use serde::de::DeserializeOwned; @@ -7,6 +9,7 @@ use serde::de::DeserializeOwned; use crate::{ error::{AuthError, InitError}, jwks::{key_store_manager::KeyStoreManager, KeyData, KeySource}, + layer::{self, JwtSource}, oidc, Refresh, }; @@ -38,6 +41,7 @@ where pub key_source: KeySource, pub claims_checker: Option>, pub validation: crate::validation::Validation, + pub jwt_source: JwtSource, } fn read_data(path: &str) -> Result, InitError> { @@ -69,6 +73,7 @@ where claims_checker: Option>, refresh: Option, validation: crate::validation::Validation, + jwt_source: JwtSource, ) -> Result, InitError> { Ok(match key_source_type { KeySourceType::RSA(path) => { @@ -81,6 +86,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::RSAString(text) => { @@ -93,6 +99,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::EC(path) => { @@ -105,6 +112,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::ECString(text) => { @@ -117,6 +125,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::ED(path) => { @@ -129,6 +138,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::EDString(text) => { @@ -141,6 +151,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::Secret(secret) => { @@ -153,6 +164,7 @@ where })), claims_checker, validation, + jwt_source, } } KeySourceType::JwksString(jwks_str) => { @@ -164,6 +176,7 @@ where key_source: KeySource::SingleKeySource(Arc::new(k)), claims_checker, validation, + jwt_source, } } KeySourceType::Jwks(url) => { @@ -173,6 +186,7 @@ where key_source: KeySource::KeyStoreSource(key_store_manager), claims_checker, validation, + jwt_source, } } KeySourceType::Discovery(issuer_url) => { @@ -184,6 +198,7 @@ where key_source: KeySource::KeyStoreSource(key_store_manager), claims_checker, validation, + jwt_source, } } }) @@ -204,6 +219,18 @@ where Ok(token_data) } + + pub fn extract_token(&self, h: &HeaderMap) -> Option { + match &self.jwt_source { + layer::JwtSource::AuthorizationHeader => { + let bearer_o: Option> = h.typed_get(); + bearer_o.map(|b| String::from(b.0.token())) + } + layer::JwtSource::Cookie(name) => h + .typed_get::() + .and_then(|c| c.get(name.as_str()).map(String::from)), + } + } } #[cfg(test)] @@ -212,16 +239,22 @@ mod tests { use jsonwebtoken::{Algorithm, Header}; use serde_json::Value; - use crate::validation::Validation; + use crate::{layer::JwtSource, validation::Validation}; use super::{Authorizer, KeySourceType}; #[tokio::test] async fn build_from_secret() { let h = Header::new(Algorithm::HS256); - let a = Authorizer::::build(KeySourceType::Secret("xxxxxx".to_owned()), None, None, Validation::new()) - .await - .unwrap(); + let a = Authorizer::::build( + KeySourceType::Secret("xxxxxx".to_owned()), + None, + None, + Validation::new(), + JwtSource::AuthorizationHeader, + ) + .await + .unwrap(); let k = a.key_source.get_key(h); assert!(k.await.is_ok()); } @@ -238,9 +271,15 @@ mod tests { "e": "AQAB" }]} "#; - let a = Authorizer::::build(KeySourceType::JwksString(jwks.to_owned()), None, None, Validation::new()) - .await - .unwrap(); + let a = Authorizer::::build( + KeySourceType::JwksString(jwks.to_owned()), + None, + None, + Validation::new(), + JwtSource::AuthorizationHeader, + ) + .await + .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::RS256)); assert!(k.await.is_ok()); } @@ -252,6 +291,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -263,6 +303,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -274,6 +315,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -288,6 +330,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -299,6 +342,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -310,6 +354,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await .unwrap(); @@ -324,6 +369,7 @@ mod tests { None, None, Validation::new(), + JwtSource::AuthorizationHeader, ) .await; println!("{:?}", a.as_ref().err()); @@ -332,8 +378,14 @@ mod tests { #[tokio::test] async fn build_jwks_url_error() { - let a = - Authorizer::::build(KeySourceType::Jwks("://xxxx".to_owned()), None, None, Validation::default()).await; + let a = Authorizer::::build( + KeySourceType::Jwks("://xxxx".to_owned()), + None, + None, + Validation::default(), + JwtSource::AuthorizationHeader, + ) + .await; println!("{:?}", a.as_ref().err()); assert!(a.is_err()); } @@ -345,6 +397,7 @@ mod tests { None, None, Validation::default(), + JwtSource::AuthorizationHeader, ) .await; println!("{:?}", a.as_ref().err()); diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index 3a8e36b..e401f73 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -1,10 +1,8 @@ use axum::async_trait; use axum::http::Request; use futures_core::ready; -use futures_util::future::BoxFuture; +use futures_util::future::{self, BoxFuture}; use futures_util::stream::{FuturesUnordered, StreamExt}; -use headers::authorization::Bearer; -use headers::{Authorization, HeaderMapExt}; use jsonwebtoken::TokenData; use pin_project::pin_project; use serde::de::DeserializeOwned; @@ -20,7 +18,7 @@ use crate::claims::RegisteredClaims; use crate::error::InitError; use crate::jwks::key_store_manager::Refresh; use crate::validation::Validation; -use crate::{layer, AuthError, RefreshStrategy}; +use crate::{AuthError, RefreshStrategy}; /// Authorizer Layer builder /// @@ -189,8 +187,10 @@ where #[deprecated(since = "0.10.0", note = "please use `to_layer()` instead")] pub async fn layer(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); - let auth = Arc::new(Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val).await?); - Ok(AsyncAuthorizationLayer::new(vec![auth], self.jwt_source)) + let auth = Arc::new( + Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await?, + ); + Ok(AsyncAuthorizationLayer::new(vec![auth])) } } @@ -201,8 +201,10 @@ where { async fn to_layer(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); - let auth = Arc::new(Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val).await?); - Ok(AsyncAuthorizationLayer::new(vec![auth], self.jwt_source)) + let auth = Arc::new( + Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await?, + ); + Ok(AsyncAuthorizationLayer::new(vec![auth])) } } @@ -222,6 +224,7 @@ where a.claims_checker, a.refresh, a.validation.unwrap_or_default(), + a.jwt_source, ) }) .collect(); @@ -237,8 +240,7 @@ where // TODO: composite build error (containing all errors) Err(e) } else { - // TODO: jwt_source per Authorizer - Ok(AsyncAuthorizationLayer::new(auths, JwtSource::AuthorizationHeader)) + Ok(AsyncAuthorizationLayer::new(auths)) } } } @@ -263,41 +265,35 @@ where type Future = BoxFuture<'static, Result, AuthError>>; fn authorize(&self, mut request: Request) -> Self::Future { - // TODO: extract token per authorizer (jwt_source shloud be per authorizer) - let h = request.headers(); - let token_o = match &self.jwt_source { - layer::JwtSource::AuthorizationHeader => { - let bearer_o: Option> = h.typed_get(); - bearer_o.map(|b| String::from(b.0.token())) - } - layer::JwtSource::Cookie(name) => h - .typed_get::() - .and_then(|c| c.get(name.as_str()).map(String::from)), - }; + let tkns_auths: Vec<(Option, Arc>)> = self + .auths + .iter() + .map(|a| (a.extract_token(request.headers()), a.clone())) + .collect(); - let authorizers: Vec>> = self.auths.iter().cloned().collect(); + if !tkns_auths.iter().any(|(t, _)| t.is_some()) { + return Box::pin(future::ready(Err(AuthError::MissingToken()))); + } Box::pin(async move { - if let Some(token) = token_o { - let mut token_data: Result, AuthError> = Err(AuthError::NoAuthorizer()); - for auth in authorizers { + let mut token_data: Result, AuthError> = Err(AuthError::NoAuthorizer()); + for (tkn, auth) in tkns_auths { + if let Some(token) = tkn { token_data = auth.check_auth(token.as_str()).await; if token_data.is_ok() { break; } } - match token_data { - Ok(tdata) => { - // Set `token_data` as a request extension so it can be accessed by other - // services down the stack. - request.extensions_mut().insert(tdata); + } + match token_data { + Ok(tdata) => { + // Set `token_data` as a request extension so it can be accessed by other + // services down the stack. + request.extensions_mut().insert(tdata); - Ok(request) - } - Err(err) => Err(err), // TODO: error containing all errors (not just the last one) + Ok(request) } - } else { - Err(AuthError::MissingToken()) + Err(err) => Err(err), // TODO: error containing all errors (not just the last one) } }) } @@ -311,15 +307,14 @@ where C: Clone + DeserializeOwned + Send, { auths: Vec>>, - jwt_source: JwtSource, } impl AsyncAuthorizationLayer where C: Clone + DeserializeOwned + Send, { - pub fn new(auths: Vec>>, jwt_source: JwtSource) -> AsyncAuthorizationLayer { - Self { auths, jwt_source } + pub fn new(auths: Vec>>) -> AsyncAuthorizationLayer { + Self { auths } } } @@ -330,7 +325,7 @@ where type Service = AsyncAuthorizationService; fn layer(&self, inner: S) -> Self::Service { - AsyncAuthorizationService::new(inner, self.auths.clone(), self.jwt_source.clone()) + AsyncAuthorizationService::new(inner, self.auths.clone()) } } @@ -366,7 +361,6 @@ where { pub inner: S, pub auths: Vec>>, - pub jwt_source: JwtSource, } impl AsyncAuthorizationService @@ -395,12 +389,8 @@ where /// Authorize requests using a custom scheme. /// /// The `Authorization` header is required to have the value provided. - pub fn new(inner: S, auths: Vec>>, jwt_source: JwtSource) -> AsyncAuthorizationService { - Self { - inner, - auths, - jwt_source, - } + pub fn new(inner: S, auths: Vec>>) -> AsyncAuthorizationService { + Self { inner, auths } } } From 0fbdc0df84fd7df8ab7c096844a756243a5a21ca Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 05/13] test: add multiple authorizer tests --- jwt-authorizer/tests/tests.rs | 38 ++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index e2cf54a..0811022 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -23,7 +23,7 @@ mod tests { sub: String, } - async fn app(jwt_auth: JwtAuthorizer) -> Router { + async fn app(jwt_auth: impl ToAuthorizationLayer) -> Router { Router::new().route("/public", get(|| async { "hello" })).route( "/protected", get(|JwtClaims(user): JwtClaims| async move { format!("hello: {}", user.sub) }).layer( @@ -38,7 +38,11 @@ mod tests { ) } - async fn proteced_request_with_header(jwt_auth: JwtAuthorizer, header_name: &str, header_value: &str) -> Response { + async fn proteced_request_with_header( + jwt_auth: impl ToAuthorizationLayer, + header_name: &str, + header_value: &str, + ) -> Response { app(jwt_auth) .await .oneshot( @@ -52,7 +56,7 @@ mod tests { .unwrap() } - async fn make_proteced_request(jwt_auth: JwtAuthorizer, bearer: &str) -> Response { + async fn make_proteced_request(jwt_auth: impl ToAuthorizationLayer, bearer: &str) -> Response { proteced_request_with_header(jwt_auth, "Authorization", &format!("Bearer {bearer}")).await } @@ -332,4 +336,32 @@ mod tests { &"Bearer error=\"invalid_token\"" ); } + + // -------------------------- + // Multiple Authorizers + // -------------------------- + #[tokio::test] + async fn multiple_authorizers() { + let auths: Vec> = vec![ + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem"), + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), + ]; + + // OK + let response = + proteced_request_with_header(auths, header::COOKIE.as_str(), &format!("ccc={}", common::JWT_RSA1_OK)).await; + assert_eq!(response.status(), StatusCode::OK); + + let auths: Vec> = vec![ + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem"), + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), + ]; + + // Cookie missing + let response = + proteced_request_with_header(auths, header::COOKIE.as_str(), &format!("bad_cookie={}", common::JWT_EC2_OK)) + .await; + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + assert_eq!(response.headers().get(header::WWW_AUTHENTICATE).unwrap(), &"Bearer"); + } } From 50c2ecac38e2967cdc82069afccd8e5ebf903f87 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:02:56 +0200 Subject: [PATCH 06/13] refactor: simplification ToAuthorizationLayer -> IntoLayer --- demo-server/src/main.rs | 6 ++---- jwt-authorizer/src/layer.rs | 20 ++++++++++---------- jwt-authorizer/src/lib.rs | 2 +- jwt-authorizer/tests/integration_tests.rs | 4 ++-- jwt-authorizer/tests/tests.rs | 10 +++++----- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs index 91f7879..a460dad 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -1,7 +1,5 @@ use axum::{routing::get, Router}; -use jwt_authorizer::{ - error::InitError, AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, ToAuthorizationLayer, -}; +use jwt_authorizer::{error::InitError, AuthError, IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; use serde::Deserialize; use std::net::SocketAddr; use tower_http::trace::TraceLayer; @@ -51,7 +49,7 @@ async fn main() -> Result<(), InitError> { let api = Router::new() .route("/protected", get(protected)) // adding the authorizer layer - .layer(jwt_auth.to_layer().await?); + .layer(jwt_auth.into_layer().await?); let app = Router::new() // public endpoint diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index e401f73..7e3884a 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -195,11 +195,11 @@ where } #[async_trait] -impl ToAuthorizationLayer for JwtAuthorizer +impl IntoLayer for JwtAuthorizer where C: Clone + DeserializeOwned + Send + Sync, { - async fn to_layer(self) -> Result, InitError> { + async fn into_layer(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); let auth = Arc::new( Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await?, @@ -209,11 +209,11 @@ where } #[async_trait] -impl ToAuthorizationLayer for Vec> +impl IntoLayer for Vec> where C: Clone + DeserializeOwned + Send + Sync, { - async fn to_layer(self) -> Result, InitError> { + async fn into_layer(self) -> Result, InitError> { let mut errs = Vec::::new(); let mut auths = Vec::>>::new(); let mut auths_futs: FuturesUnordered<_> = self @@ -330,11 +330,11 @@ where } #[async_trait] -pub trait ToAuthorizationLayer +pub trait IntoLayer where C: Clone + DeserializeOwned + Send, { - async fn to_layer(self) -> Result, InitError>; + async fn into_layer(self) -> Result, InitError>; } // ---------- AsyncAuthorizationService -------- @@ -485,12 +485,12 @@ where #[cfg(test)] mod tests { - use crate::{JwtAuthorizer, ToAuthorizationLayer}; + use crate::{IntoLayer, JwtAuthorizer}; #[tokio::test] async fn jwt_auth_to_layer() { let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); - let layer = auth1.to_layer().await; + let layer = auth1.into_layer().await; assert!(layer.is_ok()); } @@ -499,7 +499,7 @@ mod tests { let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); let auth2: JwtAuthorizer = JwtAuthorizer::from_secret("bbb"); let av = vec![auth1, auth2]; - let layer = av.to_layer().await; + let layer = av.into_layer().await; assert!(layer.is_ok()); } @@ -508,7 +508,7 @@ mod tests { let auth1: JwtAuthorizer = JwtAuthorizer::from_ec_pem("aaa"); let auth2: JwtAuthorizer = JwtAuthorizer::from_ed_pem("bbb"); let av = vec![auth1, auth2]; - let layer = av.to_layer().await; + let layer = av.into_layer().await; assert!(layer.is_err()); if let Err(err) = layer { assert_eq!(err.to_string(), "No such file or directory (os error 2)"); diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index e35bbfb..a5f7243 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -8,7 +8,7 @@ use serde::de::DeserializeOwned; pub use self::error::AuthError; pub use claims::{NumericDate, OneOrArray, RegisteredClaims}; pub use jwks::key_store_manager::{Refresh, RefreshStrategy}; -pub use layer::{JwtAuthorizer, ToAuthorizationLayer}; +pub use layer::{IntoLayer, JwtAuthorizer}; pub use validation::Validation; pub mod authorizer; diff --git a/jwt-authorizer/tests/integration_tests.rs b/jwt-authorizer/tests/integration_tests.rs index 36b7704..2fbee09 100644 --- a/jwt-authorizer/tests/integration_tests.rs +++ b/jwt-authorizer/tests/integration_tests.rs @@ -11,7 +11,7 @@ use std::{ use axum::{response::Response, routing::get, Json, Router}; use http::{header::AUTHORIZATION, Request, StatusCode}; use hyper::Body; -use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, ToAuthorizationLayer}; +use jwt_authorizer::{IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -104,7 +104,7 @@ async fn app(jwt_auth: JwtAuthorizer) -> Router { let protected_route: Router = Router::new() .route("/protected", get(protected_handler)) .route("/protected-with-user", get(protected_with_user)) - .layer(jwt_auth.to_layer().await.unwrap()); + .layer(jwt_auth.into_layer().await.unwrap()); Router::new().merge(pub_route).merge(protected_route) } diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index 0811022..74f7162 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -12,7 +12,7 @@ mod tests { BoxError, Router, }; use http::{header, HeaderValue}; - use jwt_authorizer::{layer::JwtSource, validation::Validation, JwtAuthorizer, JwtClaims, ToAuthorizationLayer}; + use jwt_authorizer::{layer::JwtSource, validation::Validation, IntoLayer, JwtAuthorizer, JwtClaims}; use serde::Deserialize; use tower::{util::MapErrLayer, ServiceExt}; @@ -23,7 +23,7 @@ mod tests { sub: String, } - async fn app(jwt_auth: impl ToAuthorizationLayer) -> Router { + async fn app(jwt_auth: impl IntoLayer) -> Router { Router::new().route("/public", get(|| async { "hello" })).route( "/protected", get(|JwtClaims(user): JwtClaims| async move { format!("hello: {}", user.sub) }).layer( @@ -32,14 +32,14 @@ mod tests { tower::buffer::BufferLayer::new(1), MapErrLayer::new(|e: BoxError| -> Infallible { panic!("{}", e) }), ), - jwt_auth.to_layer().await.unwrap(), + jwt_auth.into_layer().await.unwrap(), ), ), ) } async fn proteced_request_with_header( - jwt_auth: impl ToAuthorizationLayer, + jwt_auth: impl IntoLayer, header_name: &str, header_value: &str, ) -> Response { @@ -56,7 +56,7 @@ mod tests { .unwrap() } - async fn make_proteced_request(jwt_auth: impl ToAuthorizationLayer, bearer: &str) -> Response { + async fn make_proteced_request(jwt_auth: impl IntoLayer, bearer: &str) -> Response { proteced_request_with_header(jwt_auth, "Authorization", &format!("Bearer {bearer}")).await } From 36bc0fca7d686bdbaff5af7b98eb0a63b2988559 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:04:53 +0200 Subject: [PATCH 07/13] doc: multi authorizer --- CHANGELOG.md | 5 +++++ jwt-authorizer/docs/README.md | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd86782..e9ec62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 0.11 (2023-xx-xx) + +- support for multiple authorizers + - JwtAuthorizer.layer() deprecated in favor of JwtAuthorizer.into_layer() + ## 0.10.1 (2023-07-11) ### Fixed diff --git a/jwt-authorizer/docs/README.md b/jwt-authorizer/docs/README.md index bed9828..2f89d7f 100644 --- a/jwt-authorizer/docs/README.md +++ b/jwt-authorizer/docs/README.md @@ -14,12 +14,14 @@ JWT authoriser Layer for Axum and Tonic. - Claims extraction - Claims checker - Tracing support (error logging) +- *tonic* support +- multiple authorizers ## Usage Example ```rust -# use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims, RegisteredClaims}; +# use jwt_authorizer::{AuthError, IntoLayer, JwtAuthorizer, JwtClaims, RegisteredClaims}; # use axum::{routing::get, Router}; # use serde::Deserialize; @@ -32,7 +34,7 @@ JWT authoriser Layer for Axum and Tonic. // adding the authorization layer let app = Router::new().route("/protected", get(protected)) - .layer(jwt_auth.layer().await.unwrap()); + .layer(jwt_auth.into_layer().await.unwrap()); // proteced handler with user injection (mapping some jwt claims) async fn protected(JwtClaims(user): JwtClaims) -> Result { From efa378b3bf39f1e39c61806ed049bdab68c8f6e5 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:04:53 +0200 Subject: [PATCH 08/13] feat: implement IntoLayer for slices (IntoIter) --- jwt-authorizer/src/layer.rs | 3 ++- jwt-authorizer/tests/tests.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index 7e3884a..de97bce 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -209,8 +209,9 @@ where } #[async_trait] -impl IntoLayer for Vec> +impl IntoLayer for T where + T: IntoIterator> + Send + Sync, C: Clone + DeserializeOwned + Send + Sync, { async fn into_layer(self) -> Result, InitError> { diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index 74f7162..2a07020 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -352,7 +352,7 @@ mod tests { proteced_request_with_header(auths, header::COOKIE.as_str(), &format!("ccc={}", common::JWT_RSA1_OK)).await; assert_eq!(response.status(), StatusCode::OK); - let auths: Vec> = vec![ + let auths: [JwtAuthorizer; 2] = [ JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem"), JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), ]; From 3d5367da8815122f9111572db788b13382b8c086 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 08:04:53 +0200 Subject: [PATCH 09/13] fix: tonic/clippy errors --- jwt-authorizer/src/error.rs | 4 ++++ jwt-authorizer/tests/tonic.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jwt-authorizer/src/error.rs b/jwt-authorizer/src/error.rs index 6cf663f..ad9bf4b 100644 --- a/jwt-authorizer/src/error.rs +++ b/jwt-authorizer/src/error.rs @@ -116,6 +116,10 @@ impl From for Response { debug!("AuthErrors::InvalidClaims"); tonic::Status::unauthenticated("error=\"insufficient_scope\"") } + AuthError::NoAuthorizer() => { + debug!("AuthErrors::NoAuthorizer"); + tonic::Status::unauthenticated("error=\"invalid_token\"") + } } .to_http() } diff --git a/jwt-authorizer/tests/tonic.rs b/jwt-authorizer/tests/tonic.rs index ac8874b..4d71863 100644 --- a/jwt-authorizer/tests/tonic.rs +++ b/jwt-authorizer/tests/tonic.rs @@ -3,7 +3,7 @@ use std::{sync::Once, task::Poll}; use axum::body::HttpBody; use futures_core::future::BoxFuture; use http::header::AUTHORIZATION; -use jwt_authorizer::{layer::AsyncAuthorizationService, JwtAuthorizer}; +use jwt_authorizer::{layer::AsyncAuthorizationService, IntoLayer, JwtAuthorizer}; use serde::{Deserialize, Serialize}; use tonic::{server::UnaryService, transport::NamedService, IntoRequest, Status}; use tower::{buffer::Buffer, Service}; @@ -83,7 +83,7 @@ async fn app( jwt_auth: JwtAuthorizer, expected_sub: String, ) -> AsyncAuthorizationService>, User> { - let layer = jwt_auth.layer().await.unwrap(); + let layer = jwt_auth.into_layer().await.unwrap(); tonic::transport::Server::builder() .layer(layer) .layer(tower::buffer::BufferLayer::new(1)) From e815d35a5551565d65e53a6a2e6e577db199c7d9 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 14 Aug 2023 11:26:49 +0200 Subject: [PATCH 10/13] refactor: JwtAuthorizer::IntoLayer -> Authorizer::IntoLayer - better error management (avoids composite errors when transforming multiple builder into layer) --- CHANGELOG.md | 2 +- demo-server/src/main.rs | 12 ++- jwt-authorizer/docs/README.md | 8 +- jwt-authorizer/src/authorizer.rs | 40 +++++++- jwt-authorizer/src/layer.rs | 115 +++++++--------------- jwt-authorizer/src/lib.rs | 3 +- jwt-authorizer/tests/integration_tests.rs | 2 +- jwt-authorizer/tests/tests.rs | 73 ++++++++++---- jwt-authorizer/tests/tonic.rs | 2 +- 9 files changed, 140 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ec62b..b052582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.11 (2023-xx-xx) - support for multiple authorizers - - JwtAuthorizer.layer() deprecated in favor of JwtAuthorizer.into_layer() + - JwtAuthorizer::layer() deprecated in favor of JwtAuthorizer::build() and IntoLayer::into_layer() ## 0.10.1 (2023-07-11) diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs index a460dad..df42105 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -1,5 +1,7 @@ use axum::{routing::get, Router}; -use jwt_authorizer::{error::InitError, AuthError, IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; +use jwt_authorizer::{ + error::InitError, AuthError, Authorizer, IntoLayer, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy, +}; use serde::Deserialize; use std::net::SocketAddr; use tower_http::trace::TraceLayer; @@ -37,19 +39,21 @@ async fn main() -> Result<(), InitError> { // First let's create an authorizer builder from a Oidc Discovery // User is a struct deserializable from JWT claims representing the authorized user // let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com/") - let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc(issuer_uri) + let auth: Authorizer = JwtAuthorizer::from_oidc(issuer_uri) // .no_refresh() .refresh(Refresh { strategy: RefreshStrategy::Interval, ..Default::default() }) - .check(claim_checker); + .check(claim_checker) + .build() + .await?; // actual router demo let api = Router::new() .route("/protected", get(protected)) // adding the authorizer layer - .layer(jwt_auth.into_layer().await?); + .layer(auth.into_layer()); let app = Router::new() // public endpoint diff --git a/jwt-authorizer/docs/README.md b/jwt-authorizer/docs/README.md index 2f89d7f..be133c0 100644 --- a/jwt-authorizer/docs/README.md +++ b/jwt-authorizer/docs/README.md @@ -21,7 +21,7 @@ JWT authoriser Layer for Axum and Tonic. ## Usage Example ```rust -# use jwt_authorizer::{AuthError, IntoLayer, JwtAuthorizer, JwtClaims, RegisteredClaims}; +# use jwt_authorizer::{AuthError, Authorizer, JwtAuthorizer, JwtClaims, RegisteredClaims, IntoLayer}; # use axum::{routing::get, Router}; # use serde::Deserialize; @@ -29,12 +29,12 @@ JWT authoriser Layer for Axum and Tonic. // let's create an authorizer builder from a JWKS Endpoint // (a serializable struct can be used to represent jwt claims, JwtAuthorizer is the default) - let jwt_auth: JwtAuthorizer = - JwtAuthorizer::from_jwks_url("http://localhost:3000/oidc/jwks"); + let auth: Authorizer = + JwtAuthorizer::from_jwks_url("http://localhost:3000/oidc/jwks").build().await.unwrap(); // adding the authorization layer let app = Router::new().route("/protected", get(protected)) - .layer(jwt_auth.into_layer().await.unwrap()); + .layer(auth.into_layer()); // proteced handler with user injection (mapping some jwt claims) async fn protected(JwtClaims(user): JwtClaims) -> Result { diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index f5384a3..5f4ac70 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -9,8 +9,8 @@ use serde::de::DeserializeOwned; use crate::{ error::{AuthError, InitError}, jwks::{key_store_manager::KeyStoreManager, KeyData, KeySource}, - layer::{self, JwtSource}, - oidc, Refresh, + layer::{self, AsyncAuthorizationLayer, JwtSource}, + oidc, Refresh, RegisteredClaims, }; pub trait ClaimsChecker { @@ -34,7 +34,7 @@ where } } -pub struct Authorizer +pub struct Authorizer where C: Clone, { @@ -233,6 +233,40 @@ where } } +pub trait IntoLayer +where + C: Clone + DeserializeOwned + Send, +{ + fn into_layer(self) -> AsyncAuthorizationLayer; +} + +impl IntoLayer for Vec> +where + C: Clone + DeserializeOwned + Send, +{ + fn into_layer(self) -> AsyncAuthorizationLayer { + AsyncAuthorizationLayer::new(self.into_iter().map(Arc::new).collect()) + } +} + +impl IntoLayer for [Authorizer; N] +where + C: Clone + DeserializeOwned + Send, +{ + fn into_layer(self) -> AsyncAuthorizationLayer { + AsyncAuthorizationLayer::new(self.into_iter().map(Arc::new).collect()) + } +} + +impl IntoLayer for Authorizer +where + C: Clone + DeserializeOwned + Send, +{ + fn into_layer(self) -> AsyncAuthorizationLayer { + AsyncAuthorizationLayer::new(vec![Arc::new(self)]) + } +} + #[cfg(test)] mod tests { diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index de97bce..e69404d 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -1,8 +1,6 @@ -use axum::async_trait; use axum::http::Request; use futures_core::ready; use futures_util::future::{self, BoxFuture}; -use futures_util::stream::{FuturesUnordered, StreamExt}; use jsonwebtoken::TokenData; use pin_project::pin_project; use serde::de::DeserializeOwned; @@ -184,7 +182,7 @@ where } /// Build axum layer - #[deprecated(since = "0.10.0", note = "please use `to_layer()` instead")] + #[deprecated(since = "0.10.0", note = "please use `IntoLayer::into_layer()` instead")] pub async fn layer(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); let auth = Arc::new( @@ -192,57 +190,11 @@ where ); Ok(AsyncAuthorizationLayer::new(vec![auth])) } -} -#[async_trait] -impl IntoLayer for JwtAuthorizer -where - C: Clone + DeserializeOwned + Send + Sync, -{ - async fn into_layer(self) -> Result, InitError> { + pub async fn build(self) -> Result, InitError> { let val = self.validation.unwrap_or_default(); - let auth = Arc::new( - Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await?, - ); - Ok(AsyncAuthorizationLayer::new(vec![auth])) - } -} -#[async_trait] -impl IntoLayer for T -where - T: IntoIterator> + Send + Sync, - C: Clone + DeserializeOwned + Send + Sync, -{ - async fn into_layer(self) -> Result, InitError> { - let mut errs = Vec::::new(); - let mut auths = Vec::>>::new(); - let mut auths_futs: FuturesUnordered<_> = self - .into_iter() - .map(|a| { - Authorizer::build( - a.key_source_type, - a.claims_checker, - a.refresh, - a.validation.unwrap_or_default(), - a.jwt_source, - ) - }) - .collect(); - - while let Some(a) = auths_futs.next().await { - match a { - Ok(res) => auths.push(Arc::new(res)), - Err(err) => errs.push(err), - } - } - - if let Some(e) = errs.into_iter().next() { - // TODO: composite build error (containing all errors) - Err(e) - } else { - Ok(AsyncAuthorizationLayer::new(auths)) - } + Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await } } @@ -330,14 +282,6 @@ where } } -#[async_trait] -pub trait IntoLayer -where - C: Clone + DeserializeOwned + Send, -{ - async fn into_layer(self) -> Result, InitError>; -} - // ---------- AsyncAuthorizationService -------- /// Source of the bearer token @@ -486,33 +430,40 @@ where #[cfg(test)] mod tests { - use crate::{IntoLayer, JwtAuthorizer}; + use crate::{authorizer::Authorizer, IntoLayer, JwtAuthorizer, RegisteredClaims}; + + use super::AsyncAuthorizationLayer; + + #[tokio::test] + async fn auth_into_layer() { + let auth1: Authorizer = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); + let layer = auth1.into_layer(); + assert_eq!(1, layer.auths.len()); + } + + #[tokio::test] + async fn auths_into_layer() { + let auth1 = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); + let auth2 = JwtAuthorizer::from_secret("bbb").build().await.unwrap(); + + let layer: AsyncAuthorizationLayer = [auth1, auth2].into_layer(); + assert_eq!(2, layer.auths.len()); + } + + #[tokio::test] + async fn vec_auths_into_layer() { + let auth1 = JwtAuthorizer::from_secret("aaa").build().await.unwrap(); + let auth2 = JwtAuthorizer::from_secret("bbb").build().await.unwrap(); + + let layer: AsyncAuthorizationLayer = vec![auth1, auth2].into_layer(); + assert_eq!(2, layer.auths.len()); + } #[tokio::test] async fn jwt_auth_to_layer() { let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); - let layer = auth1.into_layer().await; + #[allow(deprecated)] + let layer = auth1.layer().await; assert!(layer.is_ok()); } - - #[tokio::test] - async fn vec_to_layer() { - let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); - let auth2: JwtAuthorizer = JwtAuthorizer::from_secret("bbb"); - let av = vec![auth1, auth2]; - let layer = av.into_layer().await; - assert!(layer.is_ok()); - } - - #[tokio::test] - async fn vec_to_layer_errors() { - let auth1: JwtAuthorizer = JwtAuthorizer::from_ec_pem("aaa"); - let auth2: JwtAuthorizer = JwtAuthorizer::from_ed_pem("bbb"); - let av = vec![auth1, auth2]; - let layer = av.into_layer().await; - assert!(layer.is_err()); - if let Err(err) = layer { - assert_eq!(err.to_string(), "No such file or directory (os error 2)"); - } - } } diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index a5f7243..2417386 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -6,9 +6,10 @@ use jsonwebtoken::TokenData; use serde::de::DeserializeOwned; pub use self::error::AuthError; +pub use authorizer::{Authorizer, IntoLayer}; pub use claims::{NumericDate, OneOrArray, RegisteredClaims}; pub use jwks::key_store_manager::{Refresh, RefreshStrategy}; -pub use layer::{IntoLayer, JwtAuthorizer}; +pub use layer::JwtAuthorizer; pub use validation::Validation; pub mod authorizer; diff --git a/jwt-authorizer/tests/integration_tests.rs b/jwt-authorizer/tests/integration_tests.rs index 2fbee09..067b8cd 100644 --- a/jwt-authorizer/tests/integration_tests.rs +++ b/jwt-authorizer/tests/integration_tests.rs @@ -104,7 +104,7 @@ async fn app(jwt_auth: JwtAuthorizer) -> Router { let protected_route: Router = Router::new() .route("/protected", get(protected_handler)) .route("/protected-with-user", get(protected_with_user)) - .layer(jwt_auth.into_layer().await.unwrap()); + .layer(jwt_auth.build().await.unwrap().into_layer()); Router::new().merge(pub_route).merge(protected_route) } diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index 2a07020..a1ca239 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -12,7 +12,12 @@ mod tests { BoxError, Router, }; use http::{header, HeaderValue}; - use jwt_authorizer::{layer::JwtSource, validation::Validation, IntoLayer, JwtAuthorizer, JwtClaims}; + use jwt_authorizer::{ + authorizer::Authorizer, + layer::{AsyncAuthorizationLayer, JwtSource}, + validation::Validation, + IntoLayer, JwtAuthorizer, JwtClaims, + }; use serde::Deserialize; use tower::{util::MapErrLayer, ServiceExt}; @@ -23,7 +28,7 @@ mod tests { sub: String, } - async fn app(jwt_auth: impl IntoLayer) -> Router { + async fn app(layer: AsyncAuthorizationLayer) -> Router { Router::new().route("/public", get(|| async { "hello" })).route( "/protected", get(|JwtClaims(user): JwtClaims| async move { format!("hello: {}", user.sub) }).layer( @@ -32,18 +37,22 @@ mod tests { tower::buffer::BufferLayer::new(1), MapErrLayer::new(|e: BoxError| -> Infallible { panic!("{}", e) }), ), - jwt_auth.into_layer().await.unwrap(), + layer, ), ), ) } - async fn proteced_request_with_header( - jwt_auth: impl IntoLayer, + async fn proteced_request_with_header(jwt_auth: JwtAuthorizer, header_name: &str, header_value: &str) -> Response { + proteced_request_with_header_and_layer(jwt_auth.build().await.unwrap().into_layer(), header_name, header_value).await + } + + async fn proteced_request_with_header_and_layer( + layer: AsyncAuthorizationLayer, header_name: &str, header_value: &str, ) -> Response { - app(jwt_auth) + app(layer) .await .oneshot( Request::builder() @@ -56,15 +65,18 @@ mod tests { .unwrap() } - async fn make_proteced_request(jwt_auth: impl IntoLayer, bearer: &str) -> Response { + async fn make_proteced_request(jwt_auth: JwtAuthorizer, bearer: &str) -> Response { proteced_request_with_header(jwt_auth, "Authorization", &format!("Bearer {bearer}")).await } #[tokio::test] async fn protected_without_jwt() { - let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"); + let auth: Authorizer = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") + .build() + .await + .unwrap(); - let response = app(jwt_auth) + let response = app(auth.into_layer()) .await .oneshot(Request::builder().uri("/protected").body(Body::empty()).unwrap()) .await @@ -342,25 +354,46 @@ mod tests { // -------------------------- #[tokio::test] async fn multiple_authorizers() { - let auths: Vec> = vec![ - JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem"), - JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), + let auths: Vec> = vec![ + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") + .build() + .await + .unwrap(), + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") + .jwt_source(JwtSource::Cookie("ccc".to_owned())) + .build() + .await + .unwrap(), ]; // OK - let response = - proteced_request_with_header(auths, header::COOKIE.as_str(), &format!("ccc={}", common::JWT_RSA1_OK)).await; + let response = proteced_request_with_header_and_layer( + auths.into_layer(), + header::COOKIE.as_str(), + &format!("ccc={}", common::JWT_RSA1_OK), + ) + .await; assert_eq!(response.status(), StatusCode::OK); - let auths: [JwtAuthorizer; 2] = [ - JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem"), - JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())), + let auths: [Authorizer; 2] = [ + JwtAuthorizer::from_ec_pem("../config/ecdsa-public1.pem") + .build() + .await + .unwrap(), + JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem") + .jwt_source(JwtSource::Cookie("ccc".to_owned())) + .build() + .await + .unwrap(), ]; // Cookie missing - let response = - proteced_request_with_header(auths, header::COOKIE.as_str(), &format!("bad_cookie={}", common::JWT_EC2_OK)) - .await; + let response = proteced_request_with_header_and_layer( + auths.into_layer(), + header::COOKIE.as_str(), + &format!("bad_cookie={}", common::JWT_EC2_OK), + ) + .await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); assert_eq!(response.headers().get(header::WWW_AUTHENTICATE).unwrap(), &"Bearer"); } diff --git a/jwt-authorizer/tests/tonic.rs b/jwt-authorizer/tests/tonic.rs index 4d71863..da499a8 100644 --- a/jwt-authorizer/tests/tonic.rs +++ b/jwt-authorizer/tests/tonic.rs @@ -83,7 +83,7 @@ async fn app( jwt_auth: JwtAuthorizer, expected_sub: String, ) -> AsyncAuthorizationService>, User> { - let layer = jwt_auth.into_layer().await.unwrap(); + let layer = jwt_auth.build().await.unwrap().into_layer(); tonic::transport::Server::builder() .layer(layer) .layer(tower::buffer::BufferLayer::new(1)) From 20f7eff01eb2143459b4309d3daff8ca6da92481 Mon Sep 17 00:00:00 2001 From: cduvray Date: Sat, 19 Aug 2023 14:20:38 +0200 Subject: [PATCH 11/13] refactor: AsyncAuthorizer::authorize, map - > filter_map --- jwt-authorizer/src/layer.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index e69404d..c50cafe 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -218,24 +218,22 @@ where type Future = BoxFuture<'static, Result, AuthError>>; fn authorize(&self, mut request: Request) -> Self::Future { - let tkns_auths: Vec<(Option, Arc>)> = self + let tkns_auths: Vec<(String, Arc>)> = self .auths .iter() - .map(|a| (a.extract_token(request.headers()), a.clone())) + .filter_map(|a| a.extract_token(request.headers()).map(|t| (t, a.clone()))) .collect(); - if !tkns_auths.iter().any(|(t, _)| t.is_some()) { + if tkns_auths.is_empty() { return Box::pin(future::ready(Err(AuthError::MissingToken()))); } Box::pin(async move { let mut token_data: Result, AuthError> = Err(AuthError::NoAuthorizer()); - for (tkn, auth) in tkns_auths { - if let Some(token) = tkn { - token_data = auth.check_auth(token.as_str()).await; - if token_data.is_ok() { - break; - } + for (token, auth) in tkns_auths { + token_data = auth.check_auth(token.as_str()).await; + if token_data.is_ok() { + break; } } match token_data { From a8ea0288cdf14365259a3d4bdd36e77e4aac7df2 Mon Sep 17 00:00:00 2001 From: cduvray Date: Sun, 20 Aug 2023 08:04:30 +0200 Subject: [PATCH 12/13] tests: (jwt_auth_to_layer) more information in case of falure --- jwt-authorizer/src/layer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index c50cafe..d5f7041 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -461,7 +461,7 @@ mod tests { async fn jwt_auth_to_layer() { let auth1: JwtAuthorizer = JwtAuthorizer::from_secret("aaa"); #[allow(deprecated)] - let layer = auth1.layer().await; - assert!(layer.is_ok()); + let layer = auth1.layer().await.unwrap(); + assert_eq!(1, layer.auths.len()); } } From cf6e3270b2e13b1317f35761348f767ba7327ee5 Mon Sep 17 00:00:00 2001 From: cduvray Date: Wed, 23 Aug 2023 08:16:55 +0200 Subject: [PATCH 13/13] doc: multiple authorizers --- jwt-authorizer/docs/README.md | 5 +++++ jwt-authorizer/src/layer.rs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/jwt-authorizer/docs/README.md b/jwt-authorizer/docs/README.md index be133c0..6e21748 100644 --- a/jwt-authorizer/docs/README.md +++ b/jwt-authorizer/docs/README.md @@ -47,6 +47,11 @@ JWT authoriser Layer for Axum and Tonic. # }; ``` +## Multiple Authorizers + +A layer can be built using multiple authorizers (`IntoLayer` is implemented for `[Authorizer; N]` and for `Vec>`). +The authorizers are sequentially applied until one of them validates the token. If no authorizer validates it the request is rejected. + ## Validation Validation configuration object. diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index d5f7041..a8b8fb5 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -217,6 +217,9 @@ where type RequestBody = B; type Future = BoxFuture<'static, Result, AuthError>>; + /// The authorizers are sequentially applied (check_auth) until one of them validates the token. + /// If no authorizer validates the token the request is rejected. + /// fn authorize(&self, mut request: Request) -> Self::Future { let tkns_auths: Vec<(String, Arc>)> = self .auths @@ -244,7 +247,7 @@ where Ok(request) } - Err(err) => Err(err), // TODO: error containing all errors (not just the last one) + Err(err) => Err(err), // TODO: error containing all errors (not just the last one) or to choose one? } }) }