Skip to content

Commit

Permalink
Add PrivateCookieJar (#900)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidpdrsn authored Apr 1, 2022
1 parent da3ca22 commit 01a4c88
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 3 deletions.
1 change: 1 addition & 0 deletions axum-extra/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning].

- **added:** Re-export `SameSite` and `Expiration` from the `cookie` crate.
- **fixed:** Fix `SignedCookieJar` when using custom key types ([#899])
- **added:** `PrivateCookieJar` for managing private cookies

[#899]: https://github.com/tokio-rs/axum/pull/899

Expand Down
2 changes: 1 addition & 1 deletion axum-extra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,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"], optional = true }
cookie-lib = { package = "cookie", version = "0.16", features = ["percent-encode", "signed", "private"], optional = true }

[dev-dependencies]
axum = { path = "../axum", version = "0.5", features = ["headers"] }
Expand Down
7 changes: 6 additions & 1 deletion axum-extra/src/extract/cookie.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Cookie parsing and cookie jar management.
//!
//! See [`CookieJar`] and [`SignedCookieJar`] for more details.
//! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details.
use axum::{
async_trait,
Expand All @@ -15,6 +15,9 @@ use http::{
};
use std::{convert::Infallible, fmt, marker::PhantomData};

mod private;

pub use self::private::PrivateCookieJar;
pub use cookie_lib::{Cookie, Expiration, Key, SameSite};

/// Extractor that grabs cookies from the request and manages the jar.
Expand Down Expand Up @@ -494,6 +497,8 @@ mod tests {
cookie_test!(plaintext_cookies, CookieJar);
cookie_test!(signed_cookies, SignedCookieJar);
cookie_test!(signed_cookies_with_custom_key, SignedCookieJar<CustomKey>);
cookie_test!(private_cookies, PrivateCookieJar);
cookie_test!(private_cookies_with_custom_key, PrivateCookieJar<CustomKey>);

#[derive(Clone)]
struct CustomKey(Key);
Expand Down
220 changes: 220 additions & 0 deletions axum-extra/src/extract/cookie/private.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
use super::{cookies_from_request, set_cookies, Cookie, Key};
use axum::{
async_trait,
extract::{FromRequest, RequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
Extension,
};
use cookie_lib::PrivateJar;
use std::{convert::Infallible, fmt, marker::PhantomData};

/// Extractor that grabs private cookies from the request and manages the jar.
///
/// All cookies will be private and encrypted with a [`Key`]. This makes it suitable for storing
/// private data.
///
/// Note that methods like [`PrivateCookieJar::add`], [`PrivateCookieJar::remove`], etc updates the
/// [`PrivateCookieJar`] 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::{PrivateCookieJar, Cookie, Key};
///
/// async fn set_secret(
/// jar: PrivateCookieJar,
/// ) -> impl IntoResponse {
/// let updated_jar = jar.add(Cookie::new("secret", "secret-data"));
/// (updated_jar, Redirect::to("/get"))
/// }
///
/// async fn get_secret(jar: PrivateCookieJar) -> impl IntoResponse {
/// if let Some(data) = jar.get("secret") {
/// // ...
/// }
/// }
///
/// // 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("/set", post(set_secret))
/// .route("/get", get(get_secret))
/// // add extension with the key so `PrivateCookieJar` can access it
/// .layer(Extension(key));
/// # let app: Router<axum::body::Body> = app;
/// ```
pub struct PrivateCookieJar<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 PrivateCookieJar<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PrivateCookieJar")
.field("jar", &self.jar)
.field("key", &"REDACTED")
.finish()
}
}

#[async_trait]
impl<B, K> FromRequest<B> for PrivateCookieJar<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 private_jar = jar.private_mut(&key);
for cookie in cookies_from_request(req) {
if let Some(cookie) = private_jar.decrypt(cookie) {
private_jar.add_original(cookie);
}
}

Ok(Self {
jar,
key,
_marker: PhantomData,
})
}
}

impl<K> PrivateCookieJar<K> {
/// Get a cookie from the jar.
///
/// If the cookie exists and can be decrypted then it is returned in plaintext.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::PrivateCookieJar;
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) {
/// let value: Option<String> = jar
/// .get("foo")
/// .map(|cookie| cookie.value().to_owned());
/// }
/// ```
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
self.private_jar().get(name)
}

/// Remove a cookie from the jar.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{PrivateCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) -> impl IntoResponse {
/// jar.remove(Cookie::named("foo"))
/// }
/// ```
#[must_use]
pub fn remove(mut self, cookie: Cookie<'static>) -> Self {
self.private_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::{PrivateCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) -> 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.private_jar_mut().add(cookie);
self
}

/// Authenticates and decrypts `cookie`, returning the plaintext version if decryption succeeds
/// or `None` otherwise.
pub fn decrypt(&self, cookie: Cookie<'static>) -> Option<Cookie<'static>> {
self.private_jar().decrypt(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>> + '_ {
PrivateCookieJarIter {
jar: self,
iter: self.jar.iter(),
}
}

fn private_jar(&self) -> PrivateJar<&'_ cookie_lib::CookieJar> {
self.jar.private(&self.key)
}

fn private_jar_mut(&mut self) -> PrivateJar<&'_ mut cookie_lib::CookieJar> {
self.jar.private_mut(&self.key)
}
}

impl<K> IntoResponseParts for PrivateCookieJar<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 PrivateCookieJar<K> {
fn into_response(self) -> Response {
(self, ()).into_response()
}
}

struct PrivateCookieJarIter<'a, K> {
jar: &'a PrivateCookieJar<K>,
iter: cookie_lib::Iter<'a>,
}

impl<'a, K> Iterator for PrivateCookieJarIter<'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);
}
}
}
}
2 changes: 1 addition & 1 deletion axum-extra/src/extract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ pub mod cookie;
pub use self::cached::Cached;

#[cfg(feature = "cookie")]
pub use self::cookie::{CookieJar, SignedCookieJar};
pub use self::cookie::{CookieJar, PrivateCookieJar, SignedCookieJar};

0 comments on commit 01a4c88

Please sign in to comment.