diff --git a/src/config/auth/mod.rs b/src/config/auth/mod.rs index c41d9367..c69be7f5 100644 --- a/src/config/auth/mod.rs +++ b/src/config/auth/mod.rs @@ -1,5 +1,4 @@ use crate::util::serde::UriOrString; -use axum::http::header::AUTHORIZATION; use serde_derive::{Deserialize, Serialize}; use validator::Validate; @@ -15,14 +14,16 @@ pub struct Auth { #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub struct Jwt { - /// Name of the cookie used to pass the JWT access token. If not set, will use - /// [`AUTHORIZATION`] as the cookie name. - #[serde(default = "Jwt::default_cookie_name")] - #[deprecated( - since = "0.5.19", - note = "Using jwt from cookie is/may be a CSRF vulnerability. This functionality is removed for now and this config field is not used." - )] - pub cookie_name: String, + /// Name of the cookie used to pass the JWT access token. If provided, the default + /// [`Jwt`][crate::middleware::http::auth::jwt::Jwt] will extract the access token from the + /// provided cookie name if it wasn't present in the [`axum::http::header::AUTHORIZATION`] + /// request header. If not provided, the extractor will only consider the request header. + /// + /// Warning: Providing this field opens up an application to CSRF vulnerabilities unless the + /// application has the proper protections in place. See the following for more information: + /// - + /// - + pub cookie_name: Option, pub secret: String, @@ -31,12 +32,6 @@ pub struct Jwt { pub claims: JwtClaims, } -impl Jwt { - fn default_cookie_name() -> String { - AUTHORIZATION.as_str().to_string() - } -} - #[derive(Debug, Clone, Default, Validate, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default)] #[non_exhaustive] @@ -94,6 +89,13 @@ mod tests { required-claims = ["baz"] "# )] + #[case( + r#" + [jwt] + secret = "foo" + cookie-name = "authorization" + "# + )] #[cfg_attr(coverage_nightly, coverage(off))] fn auth(_case: TestCase, #[case] config: &str) { let auth: Auth = toml::from_str(config).unwrap(); diff --git a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_1.snap b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_1.snap index 20722d58..201881d8 100644 --- a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_1.snap +++ b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_1.snap @@ -3,7 +3,6 @@ source: src/config/auth/mod.rs expression: auth --- [jwt] -cookie-name = 'authorization' secret = 'foo' [jwt.claims] diff --git a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_2.snap b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_2.snap index c67b01b5..437eb969 100644 --- a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_2.snap +++ b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_2.snap @@ -3,7 +3,6 @@ source: src/config/auth/mod.rs expression: auth --- [jwt] -cookie-name = 'authorization' secret = 'foo' [jwt.claims] diff --git a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_3.snap b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_3.snap index 2e67f70d..de5e49ac 100644 --- a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_3.snap +++ b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_3.snap @@ -3,7 +3,6 @@ source: src/config/auth/mod.rs expression: auth --- [jwt] -cookie-name = 'authorization' secret = 'foo' [jwt.claims] diff --git a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_4.snap b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_4.snap index 8d1e9052..73e9fc85 100644 --- a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_4.snap +++ b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_4.snap @@ -3,7 +3,6 @@ source: src/config/auth/mod.rs expression: auth --- [jwt] -cookie-name = 'authorization' secret = 'foo' [jwt.claims] diff --git a/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_5.snap b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_5.snap new file mode 100644 index 00000000..20722d58 --- /dev/null +++ b/src/config/auth/snapshots/roadster__config__auth__tests__auth@case_5.snap @@ -0,0 +1,11 @@ +--- +source: src/config/auth/mod.rs +expression: auth +--- +[jwt] +cookie-name = 'authorization' +secret = 'foo' + +[jwt.claims] +audience = [] +required-claims = [] diff --git a/src/config/snapshots/roadster__config__app_config__tests__test.snap b/src/config/snapshots/roadster__config__app_config__tests__test.snap index d714f87a..d843dbbe 100644 --- a/src/config/snapshots/roadster__config__app_config__tests__test.snap +++ b/src/config/snapshots/roadster__config__app_config__tests__test.snap @@ -133,7 +133,6 @@ timeout = true max-duration = 60 disable-argument-coercion = false [auth.jwt] -cookie-name = 'authorization' secret = 'secret-test' [auth.jwt.claims] diff --git a/src/middleware/http/auth/jwt/mod.rs b/src/middleware/http/auth/jwt/mod.rs index cfe52d68..c766c297 100644 --- a/src/middleware/http/auth/jwt/mod.rs +++ b/src/middleware/http/auth/jwt/mod.rs @@ -14,7 +14,9 @@ use crate::util::serde::{deserialize_from_str, serialize_to_str}; use async_trait::async_trait; use axum::extract::{FromRef, FromRequestParts}; use axum::http::request::Parts; +use axum::http::HeaderValue; use axum::RequestPartsExt; +use axum_extra::extract::CookieJar; use axum_extra::headers::authorization::Bearer; use axum_extra::headers::Authorization; use axum_extra::TypedHeader; @@ -59,11 +61,24 @@ where .await .ok() .map(|auth_header| auth_header.0.token().to_string()); + let token = if token.is_some() { + token + } else if let Some(cookie_name) = context.config().auth.jwt.cookie_name.as_ref() { + parts + .extract::() + .await + .ok() + .and_then(|cookies| bearer_token_from_cookies(cookie_name, cookies)) + } else { + None + }; let token = if let Some(token) = token { token } else { - return Err(HttpError::unauthorized().into()); + return Err(HttpError::unauthorized() + .error("Authorization token not found.") + .into()); }; let token: TokenData = decode_auth_token( @@ -80,6 +95,20 @@ where } } +fn bearer_token_from_cookies(cookie_name: &str, cookies: CookieJar) -> Option { + cookies + .get(cookie_name) + .map(|cookie| cookie.value()) + .and_then(|token| HeaderValue::from_str(token).ok()) + .and_then(|header_value| { + as axum_extra::headers::Header>::decode( + &mut [&header_value].into_iter(), + ) + .ok() + }) + .map(|auth_header| auth_header.token().to_string()) +} + fn decode_auth_token( token: &str, jwt_secret: &str, @@ -202,6 +231,8 @@ mod tests { use super::*; use crate::testing::snapshot::TestCase; use crate::util::serde::Wrapper; + use axum::http::header::AUTHORIZATION; + use axum_extra::extract::cookie::Cookie; use insta::assert_debug_snapshot; use rstest::{fixture, rstest}; use serde_json::from_str; @@ -213,6 +244,20 @@ mod tests { Default::default() } + #[rstest] + #[case::valid_token("Bearer foo")] + #[case::invalid_token("foo")] + fn bearer_token_from_cookies(_case: TestCase, #[case] cookie_value: &str) { + let cookies = CookieJar::new().add(Cookie::new( + AUTHORIZATION.as_str(), + cookie_value.to_string(), + )); + + let token = super::bearer_token_from_cookies(AUTHORIZATION.as_str(), cookies); + + assert_debug_snapshot!(token); + } + #[test] #[cfg_attr(coverage_nightly, coverage(off))] fn deserialize_subject_as_uri() { diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@invalid_token.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@invalid_token.snap new file mode 100644 index 00000000..947b0212 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@invalid_token.snap @@ -0,0 +1,5 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: token +--- +None diff --git a/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@valid_token.snap b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@valid_token.snap new file mode 100644 index 00000000..97bd52a1 --- /dev/null +++ b/src/middleware/http/auth/jwt/snapshots/roadster__middleware__http__auth__jwt__tests__bearer_token_from_cookies@valid_token.snap @@ -0,0 +1,7 @@ +--- +source: src/middleware/http/auth/jwt/mod.rs +expression: token +--- +Some( + "foo", +)