fix(jwt source): cookie from request (#18)

- JwtSource:Bearer renamed to AuthorizationHeader for more consistency with jwt terminology
- documentation added
- the token cookie should be taken from request not from the tower-cookies middleware jar
  - dependency on tower-cookies is no longer needed
- tests added
This commit is contained in:
cduvray 2023-04-09 08:31:39 +02:00 committed by GitHub
parent 3292d59d1c
commit bad5ad18f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 74 additions and 58 deletions

29
Cargo.lock generated
View file

@ -178,17 +178,6 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "cookie"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.3"
@ -751,7 +740,6 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"tower", "tower",
"tower-cookies",
"tower-http", "tower-http",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
@ -1572,23 +1560,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-cookies"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40f38d941a2ffd8402b36e02ae407637a9caceb693aaf2edc910437db0f36984"
dependencies = [
"async-trait",
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.4.0" version = "0.4.0"

View file

@ -25,7 +25,6 @@ tokio = { version = "1.25", features = ["full"] }
tower-http = { version = "0.4", features = ["trace", "auth"] } tower-http = { version = "0.4", features = ["trace", "auth"] }
tower-layer = "0.3" tower-layer = "0.3"
tower-service = "0.3" tower-service = "0.3"
tower-cookies = "0.9.0"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View file

@ -11,7 +11,6 @@ use std::future::Future;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use tower_cookies::Cookies;
use tower_layer::Layer; use tower_layer::Layer;
use tower_service::Service; use tower_service::Service;
@ -33,7 +32,7 @@ where
refresh: Option<Refresh>, refresh: Option<Refresh>,
claims_checker: Option<FnClaimsChecker<C>>, claims_checker: Option<FnClaimsChecker<C>>,
validation: Option<Validation>, validation: Option<Validation>,
pub jwt_source: JwtSource, jwt_source: JwtSource,
} }
/// authorization layer builder /// authorization layer builder
@ -48,7 +47,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -59,7 +58,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -70,7 +69,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -81,7 +80,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -92,7 +91,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -103,7 +102,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -114,7 +113,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -125,7 +124,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -136,7 +135,7 @@ where
refresh: Default::default(), refresh: Default::default(),
claims_checker: None, claims_checker: None,
validation: None, validation: None,
jwt_source: JwtSource::Bearer, jwt_source: JwtSource::AuthorizationHeader,
} }
} }
@ -175,6 +174,9 @@ where
self self
} }
/// configures the source of the bearer token
///
/// (default: AuthorizationHeader)
pub fn jwt_source(mut self, src: JwtSource) -> JwtAuthorizer<C> { pub fn jwt_source(mut self, src: JwtSource) -> JwtAuthorizer<C> {
self.jwt_source = src; self.jwt_source = src;
@ -214,20 +216,13 @@ where
let h = request.headers(); let h = request.headers();
let token = match &self.jwt_source { let token = match &self.jwt_source {
layer::JwtSource::Bearer => { layer::JwtSource::AuthorizationHeader => {
let bearer_o: Option<Authorization<Bearer>> = h.typed_get(); let bearer_o: Option<Authorization<Bearer>> = h.typed_get();
bearer_o.map(|b| String::from(b.0.token())) bearer_o.map(|b| String::from(b.0.token()))
} }
layer::JwtSource::Cookie(name) => { layer::JwtSource::Cookie(name) => h
if let Some(c) = request.extensions().get::<Cookies>() { .typed_get::<headers::Cookie>()
c.get(name.as_str()).map(|c| String::from(c.value())) .and_then(|c| c.get(name.as_str()).map(String::from)),
} else {
tracing::warn!(
"You have to add the tower_cookies::CookieManagerLayer middleware to use Cookies as JWT source."
);
None
}
}
}; };
Box::pin(async move { Box::pin(async move {
if let Some(token) = token { if let Some(token) = token {
@ -281,10 +276,19 @@ where
// ---------- AsyncAuthorizationService -------- // ---------- AsyncAuthorizationService --------
/// Source of the bearer token
#[derive(Clone)] #[derive(Clone)]
pub enum JwtSource { pub enum JwtSource {
Bearer, /// Storing the bearer token in Authorization header
///
/// (default)
AuthorizationHeader,
/// Cookies
///
/// (be careful when using cookies, some precautions must be taken, cf. RFC6750)
Cookie(String), Cookie(String),
// TODO: "Form-Encoded Content Parameter" may be added in the future (OAuth 2.1 / 5.2.1.2)
// FormParam,
} }
#[derive(Clone)] #[derive(Clone)]

View file

@ -9,7 +9,7 @@ use std::{
}; };
use axum::{response::Response, routing::get, Json, Router}; use axum::{response::Response, routing::get, Json, Router};
use http::{Request, StatusCode}; use http::{header::AUTHORIZATION, Request, StatusCode};
use hyper::Body; use hyper::Body;
use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; use jwt_authorizer::{JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy};
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -129,7 +129,7 @@ async fn make_proteced_request(app: &mut Router, bearer: &str) -> Response {
.call( .call(
Request::builder() Request::builder()
.uri("/protected") .uri("/protected")
.header("Authorization", format!("Bearer {bearer}")) .header(AUTHORIZATION.as_str(), format!("Bearer {bearer}"))
.body(Body::empty()) .body(Body::empty())
.unwrap(), .unwrap(),
) )

View file

@ -10,7 +10,7 @@ mod tests {
Router, Router,
}; };
use http::{header, HeaderValue}; use http::{header, HeaderValue};
use jwt_authorizer::{validation::Validation, JwtAuthorizer, JwtClaims}; use jwt_authorizer::{layer::JwtSource, validation::Validation, JwtAuthorizer, JwtClaims};
use serde::Deserialize; use serde::Deserialize;
use tower::ServiceExt; use tower::ServiceExt;
@ -29,13 +29,13 @@ mod tests {
) )
} }
async fn make_proteced_request(jwt_auth: JwtAuthorizer<User>, bearer: &str) -> Response { async fn proteced_request_with_header(jwt_auth: JwtAuthorizer<User>, header_name: &str, header_value: &str) -> Response {
app(jwt_auth) app(jwt_auth)
.await .await
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri("/protected") .uri("/protected")
.header("Authorization", format!("Bearer {bearer}")) .header(header_name, header_value)
.body(Body::empty()) .body(Body::empty())
.unwrap(), .unwrap(),
) )
@ -43,6 +43,10 @@ mod tests {
.unwrap() .unwrap()
} }
async fn make_proteced_request(jwt_auth: JwtAuthorizer<User>, bearer: &str) -> Response {
proteced_request_with_header(jwt_auth, "Authorization", &format!("Bearer {bearer}")).await
}
#[tokio::test] #[tokio::test]
async fn protected_without_jwt() { async fn protected_without_jwt() {
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem"); let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem");
@ -281,4 +285,42 @@ mod tests {
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
// --------------------
// jwt_source
// ---------------------
#[tokio::test]
async fn jwt_source_cookie() {
// OK
let response = proteced_request_with_header(
JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())),
header::COOKIE.as_str(),
&format!("ccc={}", common::JWT_RSA1_OK),
)
.await;
assert_eq!(response.status(), StatusCode::OK);
// Cookie missing
let response = proteced_request_with_header(
JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())),
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");
// Invalid Token
let response = proteced_request_with_header(
JwtAuthorizer::from_rsa_pem("../config/rsa-public1.pem").jwt_source(JwtSource::Cookie("ccc".to_owned())),
header::COOKIE.as_str(),
&format!("ccc={}", common::JWT_EC2_OK),
)
.await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(
response.headers().get(header::WWW_AUTHENTICATE).unwrap(),
&"Bearer error=\"invalid_token\""
);
}
} }