mirror of
https://github.com/tokio-rs/axum.git
synced 2024-10-24 01:46:51 +02:00
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
This commit is contained in:
parent
a723ed1453
commit
700617963f
4 changed files with 262 additions and 236 deletions
|
@ -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"] }
|
||||
|
|
|
@ -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<Authorization<Bearer>>,
|
||||
/// 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<String> {
|
||||
/// // 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<axum::body::Body> = app;
|
||||
/// ```
|
||||
pub struct SignedCookieJar<K = Key> {
|
||||
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<K>,
|
||||
}
|
||||
|
||||
impl<K> fmt::Debug for SignedCookieJar<K> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("SignedCookieJar")
|
||||
.field("jar", &self.jar)
|
||||
.field("key", &"REDACTED")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, K> FromRequest<B> for SignedCookieJar<K>
|
||||
where
|
||||
B: Send,
|
||||
K: Into<Key> + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Rejection = <axum::Extension<K> as FromRequest<B>>::Rejection;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
let key = Extension::<K>::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<K> SignedCookieJar<K> {
|
||||
/// 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<String> = jar
|
||||
/// .get("foo")
|
||||
/// .map(|cookie| cookie.value().to_owned());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
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<Cookie<'static>> {
|
||||
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<Item = Cookie<'static>> + '_ {
|
||||
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<K> IntoResponseParts for SignedCookieJar<K> {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
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<K> IntoResponse for SignedCookieJar<K> {
|
||||
fn into_response(self) -> Response {
|
||||
(self, ()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
struct SignedCookieJarIter<'a, K> {
|
||||
jar: &'a SignedCookieJar<K>,
|
||||
iter: cookie_lib::Iter<'a>,
|
||||
}
|
||||
|
||||
impl<'a, K> Iterator for SignedCookieJarIter<'a, K> {
|
||||
type Item = Cookie<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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 {
|
239
axum-extra/src/extract/cookie/signed.rs
Normal file
239
axum-extra/src/extract/cookie/signed.rs
Normal file
|
@ -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<Authorization<Bearer>>,
|
||||
/// 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<String> {
|
||||
/// // 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<axum::body::Body> = app;
|
||||
/// ```
|
||||
pub struct SignedCookieJar<K = Key> {
|
||||
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<K>,
|
||||
}
|
||||
|
||||
impl<K> fmt::Debug for SignedCookieJar<K> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("SignedCookieJar")
|
||||
.field("jar", &self.jar)
|
||||
.field("key", &"REDACTED")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<B, K> FromRequest<B> for SignedCookieJar<K>
|
||||
where
|
||||
B: Send,
|
||||
K: Into<Key> + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Rejection = <axum::Extension<K> as FromRequest<B>>::Rejection;
|
||||
|
||||
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
|
||||
let key = Extension::<K>::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<K> SignedCookieJar<K> {
|
||||
/// 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<String> = jar
|
||||
/// .get("foo")
|
||||
/// .map(|cookie| cookie.value().to_owned());
|
||||
/// }
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
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<Cookie<'static>> {
|
||||
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<Item = Cookie<'static>> + '_ {
|
||||
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<K> IntoResponseParts for SignedCookieJar<K> {
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
|
||||
set_cookies(self.jar, res.headers_mut());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> IntoResponse for SignedCookieJar<K> {
|
||||
fn into_response(self) -> Response {
|
||||
(self, ()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
struct SignedCookieJarIter<'a, K> {
|
||||
jar: &'a SignedCookieJar<K>,
|
||||
iter: cookie_lib::Iter<'a>,
|
||||
}
|
||||
|
||||
impl<'a, K> Iterator for SignedCookieJarIter<'a, K> {
|
||||
type Item = Cookie<'static>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let cookie = self.iter.next()?;
|
||||
|
||||
if let Some(cookie) = self.jar.get(cookie.name()) {
|
||||
return Some(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue