Merge pull request #23 from cduvray/multiple-authorizers2

feat: multiple authorizers
This commit is contained in:
cduvray 2023-08-24 07:55:13 +02:00 committed by GitHub
commit 5098e34b96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 308 additions and 74 deletions

View file

@ -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::build() and IntoLayer::into_layer()
## 0.10.1 (2023-07-11)
### Fixed

View file

@ -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, 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<User> = JwtAuthorizer::from_oidc("https://accounts.google.com/")
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::from_oidc(issuer_uri)
let auth: Authorizer<User> = 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.layer().await?);
.layer(auth.into_layer());
let app = Router::new()
// public endpoint

View file

@ -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, Authorizer, JwtAuthorizer, JwtClaims, RegisteredClaims, IntoLayer};
# use axum::{routing::get, Router};
# use serde::Deserialize;
@ -27,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<RegisteredClaims> 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.layer().await.unwrap());
.layer(auth.into_layer());
// proteced handler with user injection (mapping some jwt claims)
async fn protected(JwtClaims(user): JwtClaims<RegisteredClaims>) -> Result<String, AuthError> {
@ -45,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<C>; N]` and for `Vec<Authorizer<C>>`).
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.

View file

@ -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,7 +9,8 @@ use serde::de::DeserializeOwned;
use crate::{
error::{AuthError, InitError},
jwks::{key_store_manager::KeyStoreManager, KeyData, KeySource},
oidc, Refresh,
layer::{self, AsyncAuthorizationLayer, JwtSource},
oidc, Refresh, RegisteredClaims,
};
pub trait ClaimsChecker<C> {
@ -31,13 +34,14 @@ where
}
}
pub struct Authorizer<C>
pub struct Authorizer<C = RegisteredClaims>
where
C: Clone,
{
pub key_source: KeySource,
pub claims_checker: Option<FnClaimsChecker<C>>,
pub validation: crate::validation::Validation,
pub jwt_source: JwtSource,
}
fn read_data(path: &str) -> Result<Vec<u8>, InitError> {
@ -65,10 +69,11 @@ where
C: DeserializeOwned + Clone + Send + Sync,
{
pub(crate) async fn build(
key_source_type: &KeySourceType,
key_source_type: KeySourceType,
claims_checker: Option<FnClaimsChecker<C>>,
refresh: Option<Refresh>,
validation: crate::validation::Validation,
jwt_source: JwtSource,
) -> Result<Authorizer<C>, 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,30 +164,33 @@ where
})),
claims_checker,
validation,
jwt_source,
}
}
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 {
key_source: KeySource::SingleKeySource(Arc::new(k)),
claims_checker,
validation,
jwt_source,
}
}
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),
claims_checker,
validation,
jwt_source,
}
}
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());
@ -184,6 +198,7 @@ where
key_source: KeySource::KeyStoreSource(key_store_manager),
claims_checker,
validation,
jwt_source,
}
}
})
@ -204,6 +219,52 @@ where
Ok(token_data)
}
pub fn extract_token(&self, h: &HeaderMap) -> Option<String> {
match &self.jwt_source {
layer::JwtSource::AuthorizationHeader => {
let bearer_o: Option<Authorization<Bearer>> = h.typed_get();
bearer_o.map(|b| String::from(b.0.token()))
}
layer::JwtSource::Cookie(name) => h
.typed_get::<headers::Cookie>()
.and_then(|c| c.get(name.as_str()).map(String::from)),
}
}
}
pub trait IntoLayer<C>
where
C: Clone + DeserializeOwned + Send,
{
fn into_layer(self) -> AsyncAuthorizationLayer<C>;
}
impl<C> IntoLayer<C> for Vec<Authorizer<C>>
where
C: Clone + DeserializeOwned + Send,
{
fn into_layer(self) -> AsyncAuthorizationLayer<C> {
AsyncAuthorizationLayer::new(self.into_iter().map(Arc::new).collect())
}
}
impl<C, const N: usize> IntoLayer<C> for [Authorizer<C>; N]
where
C: Clone + DeserializeOwned + Send,
{
fn into_layer(self) -> AsyncAuthorizationLayer<C> {
AsyncAuthorizationLayer::new(self.into_iter().map(Arc::new).collect())
}
}
impl<C> IntoLayer<C> for Authorizer<C>
where
C: Clone + DeserializeOwned + Send,
{
fn into_layer(self) -> AsyncAuthorizationLayer<C> {
AsyncAuthorizationLayer::new(vec![Arc::new(self)])
}
}
#[cfg(test)]
@ -212,16 +273,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::<Value>::build(&KeySourceType::Secret("xxxxxx".to_owned()), None, None, Validation::new())
.await
.unwrap();
let a = Authorizer::<Value>::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 +305,15 @@ mod tests {
"e": "AQAB"
}]}
"#;
let a = Authorizer::<Value>::build(&KeySourceType::JwksString(jwks.to_owned()), None, None, Validation::new())
.await
.unwrap();
let a = Authorizer::<Value>::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());
}
@ -248,10 +321,11 @@ mod tests {
#[tokio::test]
async fn build_from_file() {
let a = Authorizer::<Value>::build(
&KeySourceType::RSA("../config/rsa-public1.pem".to_owned()),
KeySourceType::RSA("../config/rsa-public1.pem".to_owned()),
None,
None,
Validation::new(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -259,10 +333,11 @@ mod tests {
assert!(k.await.is_ok());
let a = Authorizer::<Value>::build(
&KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()),
KeySourceType::EC("../config/ecdsa-public1.pem".to_owned()),
None,
None,
Validation::new(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -270,10 +345,11 @@ mod tests {
assert!(k.await.is_ok());
let a = Authorizer::<Value>::build(
&KeySourceType::ED("../config/ed25519-public1.pem".to_owned()),
KeySourceType::ED("../config/ed25519-public1.pem".to_owned()),
None,
None,
Validation::new(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -284,10 +360,11 @@ mod tests {
#[tokio::test]
async fn build_from_text() {
let a = Authorizer::<Value>::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(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -295,10 +372,11 @@ mod tests {
assert!(k.await.is_ok());
let a = Authorizer::<Value>::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(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -306,10 +384,11 @@ mod tests {
assert!(k.await.is_ok());
let a = Authorizer::<Value>::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(),
JwtSource::AuthorizationHeader,
)
.await
.unwrap();
@ -320,10 +399,11 @@ mod tests {
#[tokio::test]
async fn build_file_errors() {
let a = Authorizer::<Value>::build(
&KeySourceType::RSA("./config/does-not-exist.pem".to_owned()),
KeySourceType::RSA("./config/does-not-exist.pem".to_owned()),
None,
None,
Validation::new(),
JwtSource::AuthorizationHeader,
)
.await;
println!("{:?}", a.as_ref().err());
@ -332,8 +412,14 @@ mod tests {
#[tokio::test]
async fn build_jwks_url_error() {
let a =
Authorizer::<Value>::build(&KeySourceType::Jwks("://xxxx".to_owned()), None, None, Validation::default()).await;
let a = Authorizer::<Value>::build(
KeySourceType::Jwks("://xxxx".to_owned()),
None,
None,
Validation::default(),
JwtSource::AuthorizationHeader,
)
.await;
println!("{:?}", a.as_ref().err());
assert!(a.is_err());
}
@ -341,10 +427,11 @@ mod tests {
#[tokio::test]
async fn build_discovery_url_error() {
let a = Authorizer::<Value>::build(
&KeySourceType::Discovery("://xxxx".to_owned()),
KeySourceType::Discovery("://xxxx".to_owned()),
None,
None,
Validation::default(),
JwtSource::AuthorizationHeader,
)
.await;
println!("{:?}", a.as_ref().err());

View file

@ -56,6 +56,9 @@ pub enum AuthError {
#[error("Invalid Claim")]
InvalidClaims(),
#[error("No Authorizer")]
NoAuthorizer(),
/// Used when a claim extractor is used and no authorization layer is in front the handler
#[error("No Authorizer Layer")]
NoAuthorizerLayer(),
@ -117,6 +120,10 @@ impl From<AuthError> for Response<tonic::body::BoxBody> {
debug!("AuthErrors::InvalidClaims");
tonic::Status::unauthenticated("error=\"insufficient_scope\"")
}
AuthError::NoAuthorizer() => {
debug!("AuthErrors::NoAuthorizer");
tonic::Status::unauthenticated("error=\"invalid_token\"")
}
AuthError::NoAuthorizerLayer() => {
debug!("AuthErrors::NoAuthorizerLayer");
tonic::Status::unauthenticated("error=\"no_authorizer_layer\"")
@ -174,6 +181,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\"")
}
AuthError::NoAuthorizerLayer() => {
debug!("AuthErrors::NoAuthorizerLayer");
// TODO: should it be a standard error?

View file

@ -1,8 +1,7 @@
use axum::http::Request;
use futures_core::ready;
use futures_util::future::BoxFuture;
use headers::authorization::Bearer;
use headers::{Authorization, HeaderMapExt};
use futures_util::future::{self, BoxFuture};
use jsonwebtoken::TokenData;
use pin_project::pin_project;
use serde::de::DeserializeOwned;
use std::future::Future;
@ -17,7 +16,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
///
@ -183,12 +182,22 @@ where
}
/// Build axum layer
#[deprecated(since = "0.10.0", note = "please use `IntoLayer::into_layer()` instead")]
pub async fn layer(self) -> Result<AsyncAuthorizationLayer<C>, 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, self.jwt_source).await?,
);
Ok(AsyncAuthorizationLayer::new(vec![auth]))
}
pub async fn build(self) -> Result<Authorizer<C>, InitError> {
let val = self.validation.unwrap_or_default();
Authorizer::build(self.key_source_type, self.claims_checker, self.refresh, val, self.jwt_source).await
}
}
/// Trait for authorizing requests.
pub trait AsyncAuthorizer<B> {
type RequestBody;
@ -208,30 +217,37 @@ where
type RequestBody = B;
type Future = BoxFuture<'static, Result<Request<B>, 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<B>) -> Self::Future {
let authorizer = self.auth.clone();
let h = request.headers();
let tkns_auths: Vec<(String, Arc<Authorizer<C>>)> = self
.auths
.iter()
.filter_map(|a| a.extract_token(request.headers()).map(|t| (t, a.clone())))
.collect();
if tkns_auths.is_empty() {
return Box::pin(future::ready(Err(AuthError::MissingToken())));
}
let token = match &self.jwt_source {
layer::JwtSource::AuthorizationHeader => {
let bearer_o: Option<Authorization<Bearer>> = h.typed_get();
bearer_o.map(|b| String::from(b.0.token()))
}
layer::JwtSource::Cookie(name) => h
.typed_get::<headers::Cookie>()
.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| {
let mut token_data: Result<TokenData<C>, AuthError> = Err(AuthError::NoAuthorizer());
for (token, auth) in tkns_auths {
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(token_data);
request.extensions_mut().insert(tdata);
request
})
} else {
Err(AuthError::MissingToken())
Ok(request)
}
Err(err) => Err(err), // TODO: error containing all errors (not just the last one) or to choose one?
}
})
}
@ -244,16 +260,15 @@ pub struct AsyncAuthorizationLayer<C>
where
C: Clone + DeserializeOwned + Send,
{
auth: Arc<Authorizer<C>>,
jwt_source: JwtSource,
auths: Vec<Arc<Authorizer<C>>>,
}
impl<C> AsyncAuthorizationLayer<C>
where
C: Clone + DeserializeOwned + Send,
{
pub fn new(auth: Arc<Authorizer<C>>, jwt_source: JwtSource) -> AsyncAuthorizationLayer<C> {
Self { auth, jwt_source }
pub fn new(auths: Vec<Arc<Authorizer<C>>>) -> AsyncAuthorizationLayer<C> {
Self { auths }
}
}
@ -264,7 +279,7 @@ where
type Service = AsyncAuthorizationService<S, C>;
fn layer(&self, inner: S) -> Self::Service {
AsyncAuthorizationService::new(inner, self.auth.clone(), self.jwt_source.clone())
AsyncAuthorizationService::new(inner, self.auths.clone())
}
}
@ -291,8 +306,7 @@ where
C: Clone + DeserializeOwned + Send + Sync,
{
pub inner: S,
pub auth: Arc<Authorizer<C>>,
pub jwt_source: JwtSource,
pub auths: Vec<Arc<Authorizer<C>>>,
}
impl<S, C> AsyncAuthorizationService<S, C>
@ -321,8 +335,8 @@ where
/// Authorize requests using a custom scheme.
///
/// The `Authorization` header is required to have the value provided.
pub fn new(inner: S, auth: Arc<Authorizer<C>>, jwt_source: JwtSource) -> AsyncAuthorizationService<S, C> {
Self { inner, auth, jwt_source }
pub fn new(inner: S, auths: Vec<Arc<Authorizer<C>>>) -> AsyncAuthorizationService<S, C> {
Self { inner, auths }
}
}
@ -414,3 +428,43 @@ where
}
}
}
#[cfg(test)]
mod tests {
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<RegisteredClaims> = [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<RegisteredClaims> = 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");
#[allow(deprecated)]
let layer = auth1.layer().await.unwrap();
assert_eq!(1, layer.auths.len());
}
}

View file

@ -5,6 +5,7 @@ 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::JwtAuthorizer;

View file

@ -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::{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<User>) -> 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.build().await.unwrap().into_layer());
Router::new().merge(pub_route).merge(protected_route)
}

View file

@ -12,7 +12,12 @@ mod tests {
BoxError, Router,
};
use http::{header, HeaderValue};
use jwt_authorizer::{layer::JwtSource, validation::Validation, 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: JwtAuthorizer<User>) -> Router {
async fn app(layer: AsyncAuthorizationLayer<User>) -> Router {
Router::new().route("/public", get(|| async { "hello" })).route(
"/protected",
get(|JwtClaims(user): JwtClaims<User>| async move { format!("hello: {}", user.sub) }).layer(
@ -32,14 +37,22 @@ mod tests {
tower::buffer::BufferLayer::new(1),
MapErrLayer::new(|e: BoxError| -> Infallible { panic!("{}", e) }),
),
jwt_auth.layer().await.unwrap(),
layer,
),
),
)
}
async fn proteced_request_with_header(jwt_auth: JwtAuthorizer<User>, header_name: &str, header_value: &str) -> Response {
app(jwt_auth)
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<User>,
header_name: &str,
header_value: &str,
) -> Response {
app(layer)
.await
.oneshot(
Request::builder()
@ -58,9 +71,12 @@ mod tests {
#[tokio::test]
async fn protected_without_jwt() {
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem");
let auth: Authorizer<User> = 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
@ -348,4 +364,53 @@ mod tests {
&"Bearer error=\"invalid_token\""
);
}
// --------------------------
// Multiple Authorizers
// --------------------------
#[tokio::test]
async fn multiple_authorizers() {
let auths: Vec<Authorizer<User>> = 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_and_layer(
auths.into_layer(),
header::COOKIE.as_str(),
&format!("ccc={}", common::JWT_RSA1_OK),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
let auths: [Authorizer<User>; 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_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");
}
}

View file

@ -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<User>,
expected_sub: String,
) -> AsyncAuthorizationService<Buffer<tonic::transport::server::Routes, http::Request<tonic::transport::Body>>, User> {
let layer = jwt_auth.layer().await.unwrap();
let layer = jwt_auth.build().await.unwrap().into_layer();
tonic::transport::Server::builder()
.layer(layer)
.layer(tower::buffer::BufferLayer::new(1))