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))