Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CSRF Token Handling

State-changing requests (POST, PUT, DELETE, PATCH) require a valid CSRF token. This guide explains how to implement CSRF protection for your custom pages.

Overview

Token Acquisition

Client TypeEmbeddingResponse HeaderEndpoint
JavaScriptOptionalAvailableAvailable
HTML FormRequiredNot availableNot available

JavaScript clients can use response headers, the endpoint, or embedded values. HTML Forms must embed tokens at render time.

Token Usage

Client TypeToken LocationVerification
JavaScriptX-CSRF-Token headerAutomatic (middleware)
HTML FormHidden field in bodyManual (your handler)

Middleware can read headers but not the body, so form tokens require manual verification.

JavaScript (Automatic Verification)

1. Get the Token

Option A: Embed in template from AuthUser.csrf_token:

<script>
    const csrfToken = '{{ csrf_token }}';
</script>

Option B: Read from response header of any authenticated request:

const csrfToken = response.headers.get('X-CSRF-Token');

Option C: Fetch from endpoint:

const response = await fetch(`${O2P_ROUTE_PREFIX}/user/csrf_token`, {
    credentials: 'include'
});
const { csrf_token: csrfToken } = await response.json();

2. Include in Requests

Add X-CSRF-Token header to all state-changing requests:

fetch(`${O2P_ROUTE_PREFIX}/user/update`, {
    method: 'PUT',
    headers: {
        'X-CSRF-Token': csrfToken,
        'Content-Type': 'application/json'
    },
    credentials: 'include',
    body: JSON.stringify({ user_id, account, label })
});

That’s it! The middleware verifies the token automatically. No handler code needed.


HTML Form (Manual Verification)

Step 1: Get Token and Embed in Form

Handler (GET):

use askama::Template;
use axum::{Extension, response::{Html, IntoResponse}};
use oauth2_passkey_axum::CsrfToken;

#[derive(Template)]
#[template(path = "form.j2")]
struct FormTemplate<'a> {
    csrf_token: &'a str,
}

pub async fn form_page(Extension(csrf_token): Extension<CsrfToken>) -> impl IntoResponse {
    let template = FormTemplate { csrf_token: csrf_token.as_str() };
    Html(template.render().unwrap())
}

Template (form.j2):

<form method="POST" action="/submit">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    <input type="text" name="message">
    <button type="submit">Submit</button>
</form>

Step 2: Define Form Data Structure

use serde::Deserialize;

#[derive(Deserialize)]
pub struct FormData {
    message: String,
    csrf_token: String,
}

Step 3: Verify Token in Handler

use axum::{Extension, extract::Form, response::{Html, IntoResponse}, http::StatusCode};
use oauth2_passkey_axum::{CsrfToken, CsrfHeaderVerified};
use subtle::ConstantTimeEq;

pub async fn form_post(
    Extension(csrf_token): Extension<CsrfToken>,
    Extension(csrf_header_verified): Extension<CsrfHeaderVerified>,
    Form(data): Form<FormData>,
) -> impl IntoResponse {
    // Skip if already verified via header (AJAX request)
    if !csrf_header_verified.0 {
        // Verify form token with constant-time comparison
        if !data.csrf_token.as_bytes().ct_eq(csrf_token.as_str().as_bytes()).into() {
            return (StatusCode::FORBIDDEN, "Invalid CSRF token").into_response();
        }
    }

    Html(format!("Success: {}", data.message)).into_response()
}

Step 4: Register Routes

use axum::{Router, routing::get, middleware::from_fn};
use oauth2_passkey_axum::is_authenticated_redirect;

let app = Router::new()
    .route("/form", get(form_page).post(form_post))
    .route_layer(from_fn(is_authenticated_redirect));

Key Points

  • JavaScript: Include X-CSRF-Token header → automatic verification
  • HTML Form: Embed token in hidden field → verify manually with subtle::ConstantTimeEq
  • Always use constant-time comparison (ct_eq) - never ==
  • Add subtle to your Cargo.toml: subtle = "2"

For security best practices and troubleshooting, see CSRF Protection Guide.