From 700617963f2c7f00ccc07a2086b6516a12284ea7 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Wed, 27 Apr 2022 10:11:16 +0200 Subject: [PATCH] Move private and signed cookies behind feature flags (#949) * move signed cookies into their own module * Require features for private/signed cookies * enable "cookie" feature when enabling private/signed cookies * add `#[cfg]`s in a few more places --- axum-extra/Cargo.toml | 4 +- .../src/extract/{cookie.rs => cookie/mod.rs} | 247 +----------------- axum-extra/src/extract/cookie/signed.rs | 239 +++++++++++++++++ axum-extra/src/extract/mod.rs | 8 +- 4 files changed, 262 insertions(+), 236 deletions(-) rename axum-extra/src/extract/{cookie.rs => cookie/mod.rs} (56%) create mode 100644 axum-extra/src/extract/cookie/signed.rs diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 6381c994..878e2aea 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -15,6 +15,8 @@ default = [] erased-json = ["serde_json", "serde"] typed-routing = ["axum-macros", "serde", "percent-encoding"] cookie = ["cookie-lib"] +cookie-signed = ["cookie", "cookie-lib/signed"] +cookie-private = ["cookie", "cookie-lib/private"] spa = ["tower-http/fs"] [dependencies] @@ -33,7 +35,7 @@ axum-macros = { path = "../axum-macros", version = "0.2", optional = true } serde = { version = "1.0", optional = true } serde_json = { version = "1.0.71", optional = true } percent-encoding = { version = "2.1", optional = true } -cookie-lib = { package = "cookie", version = "0.16", features = ["percent-encode", "signed", "private"], optional = true } +cookie-lib = { package = "cookie", version = "0.16", features = ["percent-encode"], optional = true } [dev-dependencies] axum = { path = "../axum", version = "0.5", features = ["headers"] } diff --git a/axum-extra/src/extract/cookie.rs b/axum-extra/src/extract/cookie/mod.rs similarity index 56% rename from axum-extra/src/extract/cookie.rs rename to axum-extra/src/extract/cookie/mod.rs index 096c6716..85cd421e 100644 --- a/axum-extra/src/extract/cookie.rs +++ b/axum-extra/src/extract/cookie/mod.rs @@ -6,19 +6,27 @@ use axum::{ async_trait, extract::{FromRequest, RequestParts}, response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, - Extension, }; -use cookie_lib::SignedJar; use http::{ header::{COOKIE, SET_COOKIE}, HeaderMap, }; -use std::{convert::Infallible, fmt, marker::PhantomData}; +use std::convert::Infallible; +#[cfg(feature = "cookie-private")] mod private; +#[cfg(feature = "cookie-signed")] +mod signed; +#[cfg(feature = "cookie-private")] pub use self::private::PrivateCookieJar; -pub use cookie_lib::{Cookie, Expiration, Key, SameSite}; +#[cfg(feature = "cookie-signed")] +pub use self::signed::SignedCookieJar; + +pub use cookie_lib::{Cookie, Expiration, SameSite}; + +#[cfg(any(feature = "cookie-signed", feature = "cookie-private"))] +pub use cookie_lib::Key; /// Extractor that grabs cookies from the request and manages the jar. /// @@ -185,210 +193,6 @@ impl IntoResponse for CookieJar { } } -/// Extractor that grabs signed cookies from the request and manages the jar. -/// -/// All cookies will be signed and verified with a [`Key`]. Do not use this to store private data -/// as the values are still transmitted in plaintext. -/// -/// Note that methods like [`SignedCookieJar::add`], [`SignedCookieJar::remove`], etc updates the -/// [`SignedCookieJar`] and returns it. This value _must_ be returned from the handler as part of -/// the response for the changes to be propagated. -/// -/// # Example -/// -/// ```rust -/// use axum::{ -/// Router, -/// Extension, -/// routing::{post, get}, -/// extract::TypedHeader, -/// response::{IntoResponse, Redirect}, -/// headers::authorization::{Authorization, Bearer}, -/// http::StatusCode, -/// }; -/// use axum_extra::extract::cookie::{SignedCookieJar, Cookie, Key}; -/// -/// async fn create_session( -/// TypedHeader(auth): TypedHeader>, -/// jar: SignedCookieJar, -/// ) -> impl IntoResponse { -/// if let Some(session_id) = authorize_and_create_session(auth.token()).await { -/// Ok(( -/// // the updated jar must be returned for the changes -/// // to be included in the response -/// jar.add(Cookie::new("session_id", session_id)), -/// Redirect::to("/me"), -/// )) -/// } else { -/// Err(StatusCode::UNAUTHORIZED) -/// } -/// } -/// -/// async fn me(jar: SignedCookieJar) -> impl IntoResponse { -/// if let Some(session_id) = jar.get("session_id") { -/// // fetch and render user... -/// # Ok(()) -/// } else { -/// Err(StatusCode::UNAUTHORIZED) -/// } -/// } -/// -/// async fn authorize_and_create_session(token: &str) -> Option { -/// // authorize the user and create a session... -/// # todo!() -/// } -/// -/// // Generate a secure key -/// // -/// // You probably don't wanna generate a new one each time the app starts though -/// let key = Key::generate(); -/// -/// let app = Router::new() -/// .route("/sessions", post(create_session)) -/// .route("/me", get(me)) -/// // add extension with the key so `SignedCookieJar` can access it -/// .layer(Extension(key)); -/// # let app: Router = app; -/// ``` -pub struct SignedCookieJar { - jar: cookie_lib::CookieJar, - key: Key, - // The key used to extract the key extension. Allows users to use multiple keys for different - // jars. Maybe a library wants its own key. - _marker: PhantomData, -} - -impl fmt::Debug for SignedCookieJar { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SignedCookieJar") - .field("jar", &self.jar) - .field("key", &"REDACTED") - .finish() - } -} - -#[async_trait] -impl FromRequest for SignedCookieJar -where - B: Send, - K: Into + Clone + Send + Sync + 'static, -{ - type Rejection = as FromRequest>::Rejection; - - async fn from_request(req: &mut RequestParts) -> Result { - let key = Extension::::from_request(req).await?.0.into(); - - let mut jar = cookie_lib::CookieJar::new(); - let mut signed_jar = jar.signed_mut(&key); - for cookie in cookies_from_request(req) { - if let Some(cookie) = signed_jar.verify(cookie) { - signed_jar.add_original(cookie); - } - } - - Ok(Self { - jar, - key, - _marker: PhantomData, - }) - } -} - -impl SignedCookieJar { - /// Get a cookie from the jar. - /// - /// If the cookie exists and its authenticity and integrity can be verified then it is returned - /// in plaintext. - /// - /// # Example - /// - /// ```rust - /// use axum_extra::extract::cookie::SignedCookieJar; - /// use axum::response::IntoResponse; - /// - /// async fn handle(jar: SignedCookieJar) { - /// let value: Option = jar - /// .get("foo") - /// .map(|cookie| cookie.value().to_owned()); - /// } - /// ``` - pub fn get(&self, name: &str) -> Option> { - self.signed_jar().get(name) - } - - /// Remove a cookie from the jar. - /// - /// # Example - /// - /// ```rust - /// use axum_extra::extract::cookie::{SignedCookieJar, Cookie}; - /// use axum::response::IntoResponse; - /// - /// async fn handle(jar: SignedCookieJar) -> impl IntoResponse { - /// jar.remove(Cookie::named("foo")) - /// } - /// ``` - #[must_use] - pub fn remove(mut self, cookie: Cookie<'static>) -> Self { - self.signed_jar_mut().remove(cookie); - self - } - - /// Add a cookie to the jar. - /// - /// The value will automatically be percent-encoded. - /// - /// # Example - /// - /// ```rust - /// use axum_extra::extract::cookie::{SignedCookieJar, Cookie}; - /// use axum::response::IntoResponse; - /// - /// async fn handle(jar: SignedCookieJar) -> impl IntoResponse { - /// jar.add(Cookie::new("foo", "bar")) - /// } - /// ``` - #[must_use] - #[allow(clippy::should_implement_trait)] - pub fn add(mut self, cookie: Cookie<'static>) -> Self { - self.signed_jar_mut().add(cookie); - self - } - - /// Verifies the authenticity and integrity of `cookie`, returning the plaintext version if - /// verification succeeds or `None` otherwise. - pub fn verify(&self, cookie: Cookie<'static>) -> Option> { - self.signed_jar().verify(cookie) - } - - /// Get an iterator over all cookies in the jar. - /// - /// Only cookies with valid authenticity and integrity are yielded by the iterator. - pub fn iter(&self) -> impl Iterator> + '_ { - SignedCookieJarIter { - jar: self, - iter: self.jar.iter(), - } - } - - fn signed_jar(&self) -> SignedJar<&'_ cookie_lib::CookieJar> { - self.jar.signed(&self.key) - } - - fn signed_jar_mut(&mut self) -> SignedJar<&'_ mut cookie_lib::CookieJar> { - self.jar.signed_mut(&self.key) - } -} - -impl IntoResponseParts for SignedCookieJar { - type Error = Infallible; - - fn into_response_parts(self, mut res: ResponseParts) -> Result { - set_cookies(self.jar, res.headers_mut()); - Ok(res) - } -} - fn set_cookies(jar: cookie_lib::CookieJar, headers: &mut HeaderMap) { for cookie in jar.delta() { if let Ok(header_value) = cookie.encoded().to_string().parse() { @@ -400,35 +204,10 @@ fn set_cookies(jar: cookie_lib::CookieJar, headers: &mut HeaderMap) { // jar so it cannot be called multiple times. } -impl IntoResponse for SignedCookieJar { - fn into_response(self) -> Response { - (self, ()).into_response() - } -} - -struct SignedCookieJarIter<'a, K> { - jar: &'a SignedCookieJar, - iter: cookie_lib::Iter<'a>, -} - -impl<'a, K> Iterator for SignedCookieJarIter<'a, K> { - type Item = Cookie<'static>; - - fn next(&mut self) -> Option { - loop { - let cookie = self.iter.next()?; - - if let Some(cookie) = self.jar.get(cookie.name()) { - return Some(cookie); - } - } - } -} - #[cfg(test)] mod tests { use super::*; - use axum::{body::Body, http::Request, routing::get, Router}; + use axum::{body::Body, http::Request, routing::get, Extension, Router}; use tower::ServiceExt; macro_rules! cookie_test { diff --git a/axum-extra/src/extract/cookie/signed.rs b/axum-extra/src/extract/cookie/signed.rs new file mode 100644 index 00000000..000c51e5 --- /dev/null +++ b/axum-extra/src/extract/cookie/signed.rs @@ -0,0 +1,239 @@ +use super::{cookies_from_request, set_cookies}; +use axum::{ + async_trait, + extract::{FromRequest, RequestParts}, + response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, + Extension, +}; +use cookie_lib::SignedJar; +use cookie_lib::{Cookie, Key}; +use std::{convert::Infallible, fmt, marker::PhantomData}; + +/// Extractor that grabs signed cookies from the request and manages the jar. +/// +/// All cookies will be signed and verified with a [`Key`]. Do not use this to store private data +/// as the values are still transmitted in plaintext. +/// +/// Note that methods like [`SignedCookieJar::add`], [`SignedCookieJar::remove`], etc updates the +/// [`SignedCookieJar`] and returns it. This value _must_ be returned from the handler as part of +/// the response for the changes to be propagated. +/// +/// # Example +/// +/// ```rust +/// use axum::{ +/// Router, +/// Extension, +/// routing::{post, get}, +/// extract::TypedHeader, +/// response::{IntoResponse, Redirect}, +/// headers::authorization::{Authorization, Bearer}, +/// http::StatusCode, +/// }; +/// use axum_extra::extract::cookie::{SignedCookieJar, Cookie, Key}; +/// +/// async fn create_session( +/// TypedHeader(auth): TypedHeader>, +/// jar: SignedCookieJar, +/// ) -> impl IntoResponse { +/// if let Some(session_id) = authorize_and_create_session(auth.token()).await { +/// Ok(( +/// // the updated jar must be returned for the changes +/// // to be included in the response +/// jar.add(Cookie::new("session_id", session_id)), +/// Redirect::to("/me"), +/// )) +/// } else { +/// Err(StatusCode::UNAUTHORIZED) +/// } +/// } +/// +/// async fn me(jar: SignedCookieJar) -> impl IntoResponse { +/// if let Some(session_id) = jar.get("session_id") { +/// // fetch and render user... +/// # Ok(()) +/// } else { +/// Err(StatusCode::UNAUTHORIZED) +/// } +/// } +/// +/// async fn authorize_and_create_session(token: &str) -> Option { +/// // authorize the user and create a session... +/// # todo!() +/// } +/// +/// // Generate a secure key +/// // +/// // You probably don't wanna generate a new one each time the app starts though +/// let key = Key::generate(); +/// +/// let app = Router::new() +/// .route("/sessions", post(create_session)) +/// .route("/me", get(me)) +/// // add extension with the key so `SignedCookieJar` can access it +/// .layer(Extension(key)); +/// # let app: Router = app; +/// ``` +pub struct SignedCookieJar { + jar: cookie_lib::CookieJar, + key: Key, + // The key used to extract the key extension. Allows users to use multiple keys for different + // jars. Maybe a library wants its own key. + _marker: PhantomData, +} + +impl fmt::Debug for SignedCookieJar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SignedCookieJar") + .field("jar", &self.jar) + .field("key", &"REDACTED") + .finish() + } +} + +#[async_trait] +impl FromRequest for SignedCookieJar +where + B: Send, + K: Into + Clone + Send + Sync + 'static, +{ + type Rejection = as FromRequest>::Rejection; + + async fn from_request(req: &mut RequestParts) -> Result { + let key = Extension::::from_request(req).await?.0.into(); + + let mut jar = cookie_lib::CookieJar::new(); + let mut signed_jar = jar.signed_mut(&key); + for cookie in cookies_from_request(req) { + if let Some(cookie) = signed_jar.verify(cookie) { + signed_jar.add_original(cookie); + } + } + + Ok(Self { + jar, + key, + _marker: PhantomData, + }) + } +} + +impl SignedCookieJar { + /// Get a cookie from the jar. + /// + /// If the cookie exists and its authenticity and integrity can be verified then it is returned + /// in plaintext. + /// + /// # Example + /// + /// ```rust + /// use axum_extra::extract::cookie::SignedCookieJar; + /// use axum::response::IntoResponse; + /// + /// async fn handle(jar: SignedCookieJar) { + /// let value: Option = jar + /// .get("foo") + /// .map(|cookie| cookie.value().to_owned()); + /// } + /// ``` + pub fn get(&self, name: &str) -> Option> { + self.signed_jar().get(name) + } + + /// Remove a cookie from the jar. + /// + /// # Example + /// + /// ```rust + /// use axum_extra::extract::cookie::{SignedCookieJar, Cookie}; + /// use axum::response::IntoResponse; + /// + /// async fn handle(jar: SignedCookieJar) -> impl IntoResponse { + /// jar.remove(Cookie::named("foo")) + /// } + /// ``` + #[must_use] + pub fn remove(mut self, cookie: Cookie<'static>) -> Self { + self.signed_jar_mut().remove(cookie); + self + } + + /// Add a cookie to the jar. + /// + /// The value will automatically be percent-encoded. + /// + /// # Example + /// + /// ```rust + /// use axum_extra::extract::cookie::{SignedCookieJar, Cookie}; + /// use axum::response::IntoResponse; + /// + /// async fn handle(jar: SignedCookieJar) -> impl IntoResponse { + /// jar.add(Cookie::new("foo", "bar")) + /// } + /// ``` + #[must_use] + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, cookie: Cookie<'static>) -> Self { + self.signed_jar_mut().add(cookie); + self + } + + /// Verifies the authenticity and integrity of `cookie`, returning the plaintext version if + /// verification succeeds or `None` otherwise. + pub fn verify(&self, cookie: Cookie<'static>) -> Option> { + self.signed_jar().verify(cookie) + } + + /// Get an iterator over all cookies in the jar. + /// + /// Only cookies with valid authenticity and integrity are yielded by the iterator. + pub fn iter(&self) -> impl Iterator> + '_ { + SignedCookieJarIter { + jar: self, + iter: self.jar.iter(), + } + } + + fn signed_jar(&self) -> SignedJar<&'_ cookie_lib::CookieJar> { + self.jar.signed(&self.key) + } + + fn signed_jar_mut(&mut self) -> SignedJar<&'_ mut cookie_lib::CookieJar> { + self.jar.signed_mut(&self.key) + } +} + +impl IntoResponseParts for SignedCookieJar { + type Error = Infallible; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + set_cookies(self.jar, res.headers_mut()); + Ok(res) + } +} + +impl IntoResponse for SignedCookieJar { + fn into_response(self) -> Response { + (self, ()).into_response() + } +} + +struct SignedCookieJarIter<'a, K> { + jar: &'a SignedCookieJar, + iter: cookie_lib::Iter<'a>, +} + +impl<'a, K> Iterator for SignedCookieJarIter<'a, K> { + type Item = Cookie<'static>; + + fn next(&mut self) -> Option { + loop { + let cookie = self.iter.next()?; + + if let Some(cookie) = self.jar.get(cookie.name()) { + return Some(cookie); + } + } + } +} diff --git a/axum-extra/src/extract/mod.rs b/axum-extra/src/extract/mod.rs index 3db1248c..ec5fe1ad 100644 --- a/axum-extra/src/extract/mod.rs +++ b/axum-extra/src/extract/mod.rs @@ -7,4 +7,10 @@ pub mod cookie; pub use self::cached::Cached; #[cfg(feature = "cookie")] -pub use self::cookie::{CookieJar, PrivateCookieJar, SignedCookieJar}; +pub use self::cookie::CookieJar; + +#[cfg(feature = "cookie-private")] +pub use self::cookie::PrivateCookieJar; + +#[cfg(feature = "cookie-signed")] +pub use self::cookie::SignedCookieJar;