Skip to content

Commit 0d86f2b

Browse files
author
Andrew Wheeler(Genusis)
authored
Addlayer (#8)
* Added layer feature * Release 0.7.1
1 parent 266f5a8 commit 0d86f2b

File tree

11 files changed

+376
-80
lines changed

11 files changed

+376
-80
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66

7+
## 0.7.1 (13. July, 2023)
8+
### Added
9+
- Layer Feature to allow getting CsrfTokens using a service.
10+
- Example for middleware usage.
11+
712
## 0.7.0 (12. July, 2023)
813
### Changed
914
- Replaced Bcrypt with Argon2.

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
members = [
33
".",
44
"example/minimal",
5+
"example/middleware",
56
]
67

78
[package]
89
name = "axum_csrf"
9-
version = "0.7.0"
10+
version = "0.7.1"
1011
authors = ["Andrew Wheeler <[email protected]>"]
1112
description = "Library to Provide a CSRF (Cross-Site Request Forgery) protection layer."
1213
edition = "2021"
@@ -16,6 +17,10 @@ documentation = "https://docs.rs/axum_csrf"
1617
keywords = ["Axum", "CSRF", "Cookies"]
1718
repository = "https://github.com/AscendingCreations/AxumCSRF"
1819

20+
[features]
21+
default = []
22+
layer = ["tower-layer", "tower-service"]
23+
1924
[dependencies]
2025
axum-core = "0.3.3"
2126
http = "0.2.9"
@@ -29,6 +34,8 @@ cookie = { version = "0.17.0", features = [
2934
] }
3035
argon2 = "0.5.0"
3136
thiserror = "1.0.43"
37+
tower-layer = {version = "0.3.2", optional = true}
38+
tower-service = {version = "0.3.2", optional = true}
3239

3340
[dev-dependencies]
3441
anyhow = "1.0.70"

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@ If you need help with this library please join our [Discord Group](https://disco
1919
```toml
2020
# Cargo.toml
2121
[dependencies]
22-
axum_csrf = "0.7.0"
22+
axum_csrf = "0.7.1"
2323
```
2424

25+
#### Cargo Feature Flags
26+
`default`: []
27+
28+
`layer`: Disables the state and enables a service layer. Useful for middleware interations.
29+
2530
# Example
2631

2732
Add it to axum via shared state:
@@ -100,6 +105,64 @@ The Template File
100105
</html>
101106
```
102107

108+
Or use the "layer" feature if you dont want to use state:
109+
```rust
110+
use askama::Template;
111+
use axum::{Form, response::IntoResponse, routing::get, Router};
112+
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken };
113+
use serde::{Deserialize, Serialize};
114+
use std::net::SocketAddr;
115+
116+
#[derive(Template, Deserialize, Serialize)]
117+
#[template(path = "template.html")]
118+
struct Keys {
119+
authenticity_token: String,
120+
// Your attributes...
121+
}
122+
123+
#[tokio::main]
124+
async fn main() {
125+
// initialize tracing
126+
tracing_subscriber::fmt::init();
127+
let config = CsrfConfig::default();
128+
129+
// build our application with a route
130+
let app = Router::new()
131+
// `GET /` goes to `root` and Post Goes to check key
132+
.route("/", get(root).post(check_key))
133+
.layer(CsrfLayer::new(config));
134+
135+
// run our app with hyper
136+
// `axum::Server` is a re-export of `hyper::Server`
137+
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
138+
tracing::debug!("listening on {}", addr);
139+
axum::Server::bind(&addr)
140+
.serve(app.into_make_service())
141+
.await
142+
.unwrap();
143+
}
144+
145+
// root creates the CSRF Token and sends it into the page for return.
146+
async fn root(token: CsrfToken) -> impl IntoResponse {
147+
let keys = Keys {
148+
//this Token is a hashed Token. it is returned and the original token is hashed for comparison.
149+
authenticity_token: token.authenticity_token().unwrap(),
150+
};
151+
152+
// We must return the token so that into_response will run and add it to our response cookies.
153+
(token, keys).into_response()
154+
}
155+
156+
async fn check_key(token: CsrfToken, Form(payload): Form<Keys>) -> &'static str {
157+
// Verfiy the Hash and return the String message.
158+
if token.verify(&payload.authenticity_token).is_err() {
159+
"Token is invalid"
160+
} else {
161+
"Token is Valid lets do stuff!"
162+
}
163+
}
164+
```
165+
103166
If you already have an encryption key for private cookies, build the CSRF configuration a different way:
104167
```rust
105168
let cookie_key = cookie::Key::generate();

example/middleware/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "middleware"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
axum = "0.6.12"
8+
serde = { version = "1.0.159", features = ["derive"] }
9+
tokio = { version = "1.29.1", features = ["full"] }
10+
askama = "0.12.0"
11+
askama_axum = "0.3.0"
12+
tracing = "0.1.37"
13+
tracing-subscriber = "0.3.16"
14+
serde_urlencoded = "0.7.1"
15+
hyper = "0.14.27"
16+
tower = "0.4"
17+
tower-http = { version = "0.4.0", features = ["map-request-body", "util"] }
18+
19+
[dependencies.axum_csrf]
20+
path = "../.."
21+
features = ["layer"]

example/middleware/src/main.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use askama::Template;
2+
use axum::{
3+
body::{self, BoxBody, Full},
4+
http::{Method, Request, StatusCode},
5+
middleware::Next,
6+
response::{IntoResponse, Response},
7+
routing::{get, post},
8+
Form, Router,
9+
};
10+
use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken, Key};
11+
12+
use serde::{Deserialize, Serialize};
13+
use std::net::SocketAddr;
14+
use tower::ServiceBuilder;
15+
use tower_http::ServiceBuilderExt;
16+
17+
#[derive(Template, Deserialize, Serialize)]
18+
#[template(path = "template.html")]
19+
pub struct Keys {
20+
authenticity_token: String,
21+
// Your attributes...
22+
}
23+
24+
#[tokio::main]
25+
async fn main() {
26+
// initialize tracing
27+
tracing_subscriber::fmt::init();
28+
let cookie_key = Key::generate();
29+
let config = CsrfConfig::default().with_key(Some(cookie_key));
30+
31+
// build our application with a route
32+
let app = Router::new()
33+
.route("/", post(check_key))
34+
.layer(
35+
ServiceBuilder::new()
36+
.map_request_body(body::boxed)
37+
.layer(axum::middleware::from_fn(auth_middleware)),
38+
)
39+
// `GET /` goes to `root` and Post Goes to check key
40+
.route("/", get(root))
41+
.layer(CsrfLayer::new(config));
42+
43+
// run our app with hyper
44+
// `axum::Server` is a re-export of `hyper::Server`
45+
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
46+
tracing::debug!("listening on {}", addr);
47+
axum::Server::bind(&addr)
48+
.serve(app.into_make_service())
49+
.await
50+
.unwrap();
51+
}
52+
53+
// basic handler that responds with a static string
54+
async fn root(token: CsrfToken) -> impl IntoResponse {
55+
let keys = Keys {
56+
authenticity_token: token.authenticity_token().unwrap(),
57+
};
58+
59+
// We must return the token so that into_response will run and add it to our response cookies.
60+
(token, keys).into_response()
61+
}
62+
63+
/// Can only be done with the feature layer enabled
64+
pub async fn auth_middleware(
65+
token: CsrfToken,
66+
method: Method,
67+
mut request: Request<BoxBody>,
68+
next: Next<BoxBody>,
69+
) -> Result<Response, StatusCode> {
70+
if method == Method::POST {
71+
let (parts, body) = request.into_parts();
72+
let bytes = hyper::body::to_bytes(body)
73+
.await
74+
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
75+
76+
let value = serde_urlencoded::from_bytes(&bytes)
77+
.map_err(|_| -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR })?;
78+
let payload: Form<Keys> = Form(value);
79+
if token.verify(&payload.authenticity_token).is_err() {
80+
return Err(StatusCode::UNAUTHORIZED);
81+
}
82+
83+
request = Request::from_parts(parts, body::boxed(Full::from(bytes)));
84+
}
85+
86+
Ok(next.run(request).await)
87+
}
88+
89+
async fn check_key() -> &'static str {
90+
"Token is Valid lets do stuff!"
91+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<title>Minimal</title>
7+
</head>
8+
9+
<body>
10+
<form method="post" action="/">
11+
<input type="hidden" name="authenticity_token" value="{{ authenticity_token }}"/>
12+
<input id="button" type="submit" value="Submit" tabindex="4" />
13+
</form>
14+
</body>
15+
</html>

src/cookies.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::CsrfConfig;
2+
use cookie::{Cookie, CookieJar, Key};
3+
use http::{
4+
self,
5+
header::{COOKIE, SET_COOKIE},
6+
HeaderMap,
7+
};
8+
use rand::{distributions::Alphanumeric, thread_rng, Rng};
9+
10+
pub(crate) trait CookiesExt {
11+
fn get_cookie(&self, name: &str, key: &Option<Key>) -> Option<Cookie<'static>>;
12+
fn add_cookie(&mut self, cookie: Cookie<'static>, key: &Option<Key>);
13+
}
14+
15+
impl CookiesExt for CookieJar {
16+
fn get_cookie(&self, name: &str, key: &Option<Key>) -> Option<Cookie<'static>> {
17+
if let Some(key) = key {
18+
self.private(key).get(name)
19+
} else {
20+
self.get(name).cloned()
21+
}
22+
}
23+
24+
fn add_cookie(&mut self, cookie: Cookie<'static>, key: &Option<Key>) {
25+
if let Some(key) = key {
26+
self.private_mut(key).add(cookie)
27+
} else {
28+
self.add(cookie)
29+
}
30+
}
31+
}
32+
33+
pub(crate) fn get_cookies(headers: &mut HeaderMap) -> CookieJar {
34+
let mut jar = CookieJar::new();
35+
36+
let cookie_iter = headers
37+
.get_all(COOKIE)
38+
.into_iter()
39+
.filter_map(|value| value.to_str().ok())
40+
.flat_map(|value| value.split(';'))
41+
.filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok());
42+
43+
for cookie in cookie_iter {
44+
jar.add_original(cookie);
45+
}
46+
47+
jar
48+
}
49+
50+
pub(crate) fn set_cookies(jar: CookieJar, headers: &mut HeaderMap) {
51+
for cookie in jar.delta() {
52+
if let Ok(header_value) = cookie.encoded().to_string().parse() {
53+
headers.append(SET_COOKIE, header_value);
54+
}
55+
}
56+
}
57+
58+
pub(crate) fn get_token(config: &CsrfConfig, headers: &mut HeaderMap) -> String {
59+
let cookie_jar = get_cookies(headers);
60+
61+
//We check if the Cookie Exists as a signed Cookie or not. If so we use the value of the cookie.
62+
//If not we create a new one.
63+
if let Some(cookie) = cookie_jar.get_cookie(&config.cookie_name, &config.key) {
64+
cookie.value().to_owned()
65+
} else {
66+
thread_rng()
67+
.sample_iter(&Alphanumeric)
68+
.take(config.cookie_len)
69+
.map(char::from)
70+
.collect()
71+
}
72+
}

src/layer.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use crate::{AxumCsrfService, CsrfConfig};
2+
use tower_layer::Layer;
3+
4+
/// CSRF layer struct used to pass key and CsrfConfig around.
5+
#[derive(Clone)]
6+
pub struct CsrfLayer {
7+
pub(crate) config: CsrfConfig,
8+
}
9+
10+
impl CsrfLayer {
11+
/// Creates the CSRF Protection Layer.
12+
pub fn new(config: CsrfConfig) -> Self {
13+
Self { config }
14+
}
15+
}
16+
17+
impl<S> Layer<S> for CsrfLayer {
18+
type Service = AxumCsrfService<S>;
19+
20+
fn layer(&self, inner: S) -> Self::Service {
21+
AxumCsrfService {
22+
config: self.config.clone(),
23+
inner,
24+
}
25+
}
26+
}

src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ mod config;
33
mod error;
44
mod token;
55

6+
pub(crate) mod cookies;
7+
8+
#[cfg(feature = "layer")]
9+
mod layer;
10+
#[cfg(feature = "layer")]
11+
mod service;
12+
13+
#[cfg(feature = "layer")]
14+
pub use layer::CsrfLayer;
15+
#[cfg(feature = "layer")]
16+
pub(crate) use service::AxumCsrfService;
17+
618
pub use config::{CsrfConfig, Key, SameSite};
719
pub use error::CsrfError;
820
pub use token::CsrfToken;

0 commit comments

Comments
 (0)