feat: optional claim extraction (fixes #12)

- error 401 rather than INTERNAL_SERVER_ERROR, when no claims exist (no layer in front of the handler)
- do not log error
- tests
This commit is contained in:
cduvray 2023-08-12 17:09:12 +02:00 committed by cduvray
parent 27cce24372
commit 940acb17a1
3 changed files with 33 additions and 6 deletions

View file

@ -55,6 +55,10 @@ pub enum AuthError {
#[error("Invalid Claim")]
InvalidClaims(),
/// Used when a claim extractor is used and no authorization layer is in front the handler
#[error("No Authorizer Layer")]
NoAuthorizerLayer(),
}
fn response_wwwauth(status: StatusCode, bearer: &str) -> Response<BoxBody> {
@ -113,6 +117,10 @@ impl From<AuthError> for Response<tonic::body::BoxBody> {
debug!("AuthErrors::InvalidClaims");
tonic::Status::unauthenticated("error=\"insufficient_scope\"")
}
AuthError::NoAuthorizerLayer() => {
debug!("AuthErrors::NoAuthorizerLayer");
tonic::Status::unauthenticated("error=\"no_authorizer_layer\"")
}
}
.to_http()
}
@ -166,6 +174,11 @@ impl IntoResponse for AuthError {
debug!("AuthErrors::InvalidClaims");
response_wwwauth(StatusCode::FORBIDDEN, "error=\"insufficient_scope\"")
}
AuthError::NoAuthorizerLayer() => {
debug!("AuthErrors::NoAuthorizerLayer");
// TODO: should it be a standard error?
response_wwwauth(StatusCode::UNAUTHORIZED, "error=\"no_authorizer_layer\"")
}
}
}
}

View file

@ -1,7 +1,6 @@
#![doc = include_str!("../docs/README.md")]
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use http::StatusCode;
use jsonwebtoken::TokenData;
use serde::de::DeserializeOwned;
@ -29,14 +28,13 @@ where
T: DeserializeOwned + Send + Sync + Clone + 'static,
S: Send + Sync,
{
type Rejection = StatusCode;
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
if let Some(claims) = parts.extensions.get::<TokenData<T>>() {
Ok(JwtClaims(claims.claims.clone()))
} else {
tracing::error!("JwtClaims extractor must be behind a jwt-authoriser layer!");
Err(StatusCode::INTERNAL_SERVER_ERROR)
Err(AuthError::NoAuthorizerLayer())
}
}
}

View file

@ -146,7 +146,7 @@ mod tests {
}
#[tokio::test]
async fn extract_from_public_500() {
async fn extract_from_public_401() {
let app = Router::new().route(
"/public",
get(|JwtClaims(user): JwtClaims<User>| async move { format!("hello: {}", user.sub) }),
@ -156,7 +156,23 @@ mod tests {
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn extract_from_public_optional() {
let app = Router::new().route(
"/public",
get(|user: Option<JwtClaims<User>>| async move { format!("option: {}", user.is_none()) }),
);
let response = app
.oneshot(Request::builder().uri("/public").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"option: true");
}
// --------------------