From 7583df586ed40b9202e7585fa5fd795ab2e3315b Mon Sep 17 00:00:00 2001 From: msi Date: Thu, 13 Nov 2025 18:04:52 -0300 Subject: Add csfr layer --- web/README.md | 2 +- web/template/Cargo.toml | 3 ++- web/template/src/main.rs | 1 + web/template/src/router.rs | 42 ++++++++++++++++++++++++++++++++++++- web/template/templates/csrf.jinja | 10 +++++++++ web/template/templates/layout.jinja | 1 + 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 web/template/templates/csrf.jinja diff --git a/web/README.md b/web/README.md index 3fdf1a4..21363f3 100644 --- a/web/README.md +++ b/web/README.md @@ -11,5 +11,5 @@ * [x] Tracing * [x] Messages (like flask) * [x] Sessions -* [ ] CSRF +* [x] CSRF * [ ] 404 diff --git a/web/template/Cargo.toml b/web/template/Cargo.toml index 007e47f..bfb4153 100644 --- a/web/template/Cargo.toml +++ b/web/template/Cargo.toml @@ -8,8 +8,9 @@ edition = "2024" [dependencies] anyhow = "=1.0.100" -axum = "=0.8.6" +axum = { version = "=0.8.6", features = ["macros"] } axum-messages = "0.8.0" +axum_csrf = { version = "0.11.0", features = ["layer"] } metrics = { version = "=0.24.2", default-features = false } metrics-exporter-prometheus = { version = "=0.17.2", default-features = false } minijinja = "=2.12.0" diff --git a/web/template/src/main.rs b/web/template/src/main.rs index ba99019..315a365 100644 --- a/web/template/src/main.rs +++ b/web/template/src/main.rs @@ -40,6 +40,7 @@ async fn start_main_server() -> anyhow::Result<()> { env.add_template("home", include_str!("../templates/home.jinja"))?; env.add_template("content", include_str!("../templates/content.jinja"))?; env.add_template("about", include_str!("../templates/about.jinja"))?; + env.add_template("csrf", include_str!("../templates/csrf.jinja"))?; let app_state = Arc::new(state::AppState { env }); diff --git a/web/template/src/router.rs b/web/template/src/router.rs index aaa8b25..170890a 100644 --- a/web/template/src/router.rs +++ b/web/template/src/router.rs @@ -16,13 +16,14 @@ use std::sync::Arc; use axum::{ - Router, + Form, Router, extract::State, http::{HeaderName, Request, StatusCode}, middleware, response::{Html, IntoResponse, Redirect}, routing::get, }; +use axum_csrf::{CsrfConfig, CsrfLayer, CsrfToken, Key}; use axum_messages::{Messages, MessagesManagerLayer}; use minijinja::context; use serde::{Deserialize, Serialize}; @@ -47,10 +48,19 @@ const REQUEST_ID_HEADER: &str = "x-request-id"; #[derive(Default, Deserialize, Serialize)] struct Counter(usize); +#[derive(Deserialize, Serialize)] +struct Keys { + authenticity_token: String, +} + pub(crate) fn route(app_state: Arc) -> Router { let x_request_id = HeaderName::from_static(REQUEST_ID_HEADER); let session_store = MemoryStore::default(); + let cookie_key = Key::generate(); + let config = CsrfConfig::default() + .with_key(Some(cookie_key)) + .with_cookie_domain(Some("127.0.0.1")); Router::new() .route("/", get(handler_home)) @@ -59,6 +69,7 @@ pub(crate) fn route(app_state: Arc) -> Router { .route("/session", get(handler_session)) .route("/message", get(set_messages_handler)) .route("/read-messages", get(read_messages_handler)) + .route("/csrf", get(csrf_root).post(csrf_check_key)) .layer(MessagesManagerLayer) // TODO(msi): from config folder asssets .nest_service("/assets", ServeDir::new("assets")) @@ -85,6 +96,7 @@ pub(crate) fn route(app_state: Arc) -> Router { .with_secure(false) .with_expiry(Expiry::OnInactivity(Duration::seconds(10))), MessagesManagerLayer, + CsrfLayer::new(config), // TODO(msi): from config TimeoutLayer::new(std::time::Duration::from_secs(10)), PropagateRequestIdLayer::new(x_request_id), @@ -94,6 +106,34 @@ pub(crate) fn route(app_state: Arc) -> Router { .with_state(app_state) } +async fn csrf_root( + token: CsrfToken, + State(state): State>, +) -> impl IntoResponse { + let template = state.env.get_template("csrf").unwrap(); + + let rendered = template + .render(context! { + title => "Csrf", + authenticity_token => token.authenticity_token().unwrap(), + }) + .unwrap(); + // We must return the token so that into_response will run and add it to our response cookies. + (token, Html(rendered)).into_response() +} + +async fn csrf_check_key( + token: CsrfToken, + Form(payload): Form, +) -> &'static str { + // Verfiy the Hash and return the String message. + if token.verify(&payload.authenticity_token).is_err() { + "Token is invalid" + } else { + "Token is Valid lets do stuff!" + } +} + async fn set_messages_handler(messages: Messages) -> impl IntoResponse { messages.info("Hello, world!").debug("This is a debug message."); diff --git a/web/template/templates/csrf.jinja b/web/template/templates/csrf.jinja new file mode 100644 index 0000000..2a9ab2e --- /dev/null +++ b/web/template/templates/csrf.jinja @@ -0,0 +1,10 @@ +{% extends "layout" %} +{% block title %}{{ super() }} | {{ title }} {% endblock %} +{% block body %} +

{{ title }}

+

{{ about_text }}

+
+ + +
+{% endblock %} diff --git a/web/template/templates/layout.jinja b/web/template/templates/layout.jinja index 946dcdc..f162c30 100644 --- a/web/template/templates/layout.jinja +++ b/web/template/templates/layout.jinja @@ -11,6 +11,7 @@
  • Session
  • Set Message
  • Read Messages
  • +
  • Csrf
  • Hello, World web =]

    -- cgit v1.2.3