actix_csrf_middleware/lib.rs
1use actix_http::{header::HeaderMap, StatusCode};
2#[cfg(feature = "actix-session")]
3use actix_session::SessionExt;
4use actix_utils::future::Either;
5use actix_web::{
6 body::{EitherBody, MessageBody},
7 cookie::{time, Cookie, SameSite},
8 dev::forward_ready,
9 dev::{Service, ServiceRequest, ServiceResponse, Transform},
10 http::{header, Method},
11 web::BytesMut,
12 Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, HttpResponseBuilder,
13};
14use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
15use futures_util::{
16 future::{err, ok, Ready},
17 ready,
18 stream::StreamExt,
19};
20use hmac::{Hmac, Mac};
21use log::{error, warn};
22use pin_project_lite::pin_project;
23use rand::RngCore;
24use sha2::Sha256;
25use std::{
26 collections::HashMap,
27 future::Future,
28 marker::PhantomData,
29 pin::Pin,
30 rc::Rc,
31 task::{Context, Poll},
32};
33use subtle::ConstantTimeEq;
34use url::Url;
35
36/// Default name of the authorized CSRF token bucket.
37///
38/// Usage depends on the chosen pattern:
39/// - Double-Submit Cookie: cookie name that stores the authorized token.
40/// - Synchronizer Token (requires the `actix-session` feature): session key
41/// under which the server stores the authorized token.
42///
43/// Override by setting [`CsrfMiddlewareConfig::token_cookie_name`].
44pub const DEFAULT_CSRF_TOKEN_KEY: &str = "CSRF";
45
46/// Default cookie name for anonymous (pre-session) tokens in the Double-Submit Cookie pattern.
47///
48/// Before a user is authenticated, the middleware may issue an anonymous token so that
49/// clients can perform allowed mutating operations (e.g., registration). This name is used
50/// for that cookie when using the Double-Submit Cookie pattern.
51///
52/// For the Synchronizer Token pattern (with `actix-session`), anonymous tokens are stored
53/// server-side under [`CsrfMiddlewareConfig::anon_session_key_name`] and this cookie is not
54/// used for token storage.
55///
56/// Override by setting [`CsrfMiddlewareConfig::anon_token_cookie_name`].
57pub const DEFAULT_CSRF_ANON_TOKEN_KEY: &str = "CSRF-ANON";
58
59/// Default field name used to extract the CSRF token from request bodies.
60///
61/// When the token is not present in the header, the middleware looks for this field in:
62/// - `application/json` bodies
63/// - `application/x-www-form-urlencoded` bodies
64///
65/// It does not parse `multipart/form-data` unless [`CsrfMiddlewareConfig::with_multipart`]
66/// is enabled, in which case you must handle token extraction manually in your handler.
67///
68/// Override by setting [`CsrfMiddlewareConfig::token_form_field`].
69pub const DEFAULT_CSRF_TOKEN_FIELD: &str = "csrf_token";
70
71/// Default header name that carries the CSRF token.
72///
73/// On mutating requests the middleware checks the header first;
74/// if absent, it attempts to read the token from the body field [`DEFAULT_CSRF_TOKEN_FIELD`].
75///
76/// Override by setting [`CsrfMiddlewareConfig::token_header_name`].
77pub const DEFAULT_CSRF_TOKEN_HEADER: &str = "X-CSRF-Token";
78
79/// Default name of the session id cookie used to bind tokens and detect authorization state.
80///
81/// - Double-Submit Cookie: the session id is mixed into HMAC derivation so the server can
82/// verify the token’s integrity and provenance.
83/// - Synchronizer Token: presence of this cookie indicates an authenticated session; the
84/// actual token value is stored server-side under [`DEFAULT_CSRF_TOKEN_KEY`] (or a custom
85/// `token_cookie_name`).
86///
87/// Override by setting [`CsrfMiddlewareConfig::session_id_cookie_name`].
88pub const DEFAULT_SESSION_ID_KEY: &str = "id";
89
90/// Name of the pre-session cookie minted by the middleware for unauthenticated flows.
91///
92/// The value is HMAC-signed by the server (see `encode_pre_session_cookie` /
93/// `decode_pre_session_cookie`) and serves as a stable identifier before a real session
94/// exists. It enables anonymous tokens and a smooth upgrade to authorized tokens after login.
95///
96/// Security characteristics are intentionally strict and defined by
97/// [`PRE_SESSION_HTTP_ONLY`], [`PRE_SESSION_SECURE`], and [`PRE_SESSION_SAME_SITE`]. These
98/// flags are not configurable.
99///
100/// The cookie is removed automatically once the request is associated with an authorized
101/// session.
102pub const CSRF_PRE_SESSION_KEY: &str = "pre-session";
103
104/// HttpOnly flag for the pre-session cookie used to bootstrap CSRF before login.
105///
106/// This flag is applied to the cookie named [`CSRF_PRE_SESSION_KEY`] whenever it is
107/// issued or expired by the middleware (see the response path in `CsrfResponse`).
108/// It is deliberately strict and not configurable to reduce the risk of token
109/// exfiltration via client-side scripts.
110///
111/// - `true`: client-side scripts cannot read the cookie.
112/// - Used when setting/expiring the pre-session cookie in both safe and mutating flows.
113///
114/// To customize cookie flags for CSRF tokens in the Double-Submit Cookie pattern,
115/// use [`CsrfMiddlewareConfig::with_token_cookie_config`]. The pre-session cookie
116/// always uses the strict defaults defined here.
117const PRE_SESSION_HTTP_ONLY: bool = true;
118
119/// Secure flag for the pre-session cookie; forces HTTPS-only transmission.
120///
121/// Applied to [`CSRF_PRE_SESSION_KEY`] whenever the cookie is set or expired by
122/// the middleware. Not configurable by design.
123const PRE_SESSION_SECURE: bool = true;
124
125/// SameSite policy for the pre-session cookie; defaults to `Strict`.
126///
127/// Applied to [`CSRF_PRE_SESSION_KEY`] to minimize cross-site sending of the
128/// cookie. Not configurable by design.
129const PRE_SESSION_SAME_SITE: SameSite = SameSite::Strict;
130
131/// Size (in bytes) of raw random tokens used by this crate.
132///
133/// Tokens are 32 random bytes encoded as base64url without padding, resulting in a 43-character
134/// ASCII string. Changing this value would alter the public token shape and is not a supported
135/// customization.
136const TOKEN_LEN: usize = 32; // 32 bytes -> 256 bits
137
138type HmacSha256 = Hmac<Sha256>;
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141/// Classification of CSRF tokens by context.
142///
143/// - [`TokenClass::Anonymous`]: Token associated with a pre-session (not yet authenticated).
144/// - [`TokenClass::Authorized`]: Token bound to an authenticated session.
145///
146/// This distinction helps prevent accidental mixing of tokens. For example, an
147/// anonymous token must not be accepted on endpoints that require an authenticated
148/// session.
149pub enum TokenClass {
150 /// Token used before authentication.
151 Anonymous,
152 /// Token used after authentication; tied to a session id.
153 Authorized,
154}
155
156impl TokenClass {
157 fn as_str(&self) -> &'static str {
158 match self {
159 TokenClass::Anonymous => "anon",
160 TokenClass::Authorized => "auth",
161 }
162 }
163}
164
165/// CSRF defense patterns supported by [`CsrfMiddleware`].
166///
167/// - [`CsrfPattern::DoubleSubmitCookie`]: Stores an HMAC-protected token in a cookie and
168/// expects the client to echo it back via header or form/json field. Does not require
169/// server-side session storage.
170/// - [`CsrfPattern::SynchronizerToken`]: Stores a random token server-side in a session
171/// (requires `actix-session`) and expects the client to send back the same value.
172///
173/// See [`CsrfMiddlewareConfig`] constructors for examples.
174#[derive(Clone, PartialEq)]
175pub enum CsrfPattern {
176 /// Store tokens server-side in session storage (requires `actix-session`)
177 #[cfg(feature = "actix-session")]
178 SynchronizerToken,
179 /// Store tokens client-side in cookies and verify with HMAC
180 DoubleSubmitCookie,
181}
182
183#[derive(Clone)]
184/// Cookie flags for tokens when using the Double-Submit Cookie pattern.
185///
186/// - `http_only`: Must be `false` so client code can read the token and mirror it into a
187/// header or form field.
188/// - `secure`: Should be `true` in production to restrict cookies to HTTPS.
189/// - `same_site`: Choose `Strict` or `Lax` depending on your cross-site needs.
190pub struct CsrfDoubleSubmitCookie {
191 /// If true, JavaScript cannot read the cookie
192 pub http_only: bool,
193 /// Restrict cookies to HTTPS in production
194 pub secure: bool,
195 /// SameSite policy controlling cross-site cookie sending
196 pub same_site: SameSite,
197}
198
199#[derive(Clone)]
200/// Configuration for [`CsrfMiddleware`].
201///
202/// Choose a CSRF defense pattern and adjust behavior such as token locations, cookie
203/// names, content-type handling, and origin checks.
204///
205/// - For the Double-Submit Cookie pattern, use [`CsrfMiddlewareConfig::double_submit_cookie`].
206/// - For the Synchronizer Token pattern (requires the `actix-session` feature), use
207/// [`CsrfMiddlewareConfig::synchronizer_token`].
208///
209/// # Defaults
210/// - Token header: [`DEFAULT_CSRF_TOKEN_HEADER`]
211/// - Token form/json field: [`DEFAULT_CSRF_TOKEN_FIELD`]
212/// - Session id cookie name: [`DEFAULT_SESSION_ID_KEY`]
213/// - Max body bytes to scan for token: 2 MiB
214///
215/// # Security
216/// - When using Double-Submit Cookie, ensure the token cookie is readable by the client
217/// (i.e., `http_only` must be `false`) so it can be mirrored into the header.
218/// - Consider enabling strict Origin/Referer enforcement with
219/// [`with_enforce_origin`](Self::with_enforce_origin) to mitigate CSRF even if a token
220/// leaks.
221/// - Avoid allowing `multipart/form-data` unless you can handle token extraction manually.
222pub struct CsrfMiddlewareConfig {
223 pub pattern: CsrfPattern,
224 pub manual_multipart: bool,
225 pub session_id_cookie_name: String,
226 /// Authorized (session-bound) tokens
227 pub token_cookie_name: String,
228 /// Anonymous (pre-session) tokens
229 pub anon_token_cookie_name: String,
230 #[cfg(feature = "actix-session")]
231 /// Anonymous (pre-session) token key for SynchronizerToken
232 pub anon_session_key_name: String,
233 pub token_form_field: String,
234 pub token_header_name: String,
235 pub token_cookie_config: Option<CsrfDoubleSubmitCookie>,
236 pub secret_key: zeroize::Zeroizing<Vec<u8>>,
237 pub skip_for: Vec<String>,
238 /// Enforce Origin/Referer checks for mutating requests
239 pub enforce_origin: bool,
240 /// Allowed origins (scheme://host[:port]) when enforce_origin = true
241 pub allowed_origins: Vec<String>,
242 /// Maximum allowed body bytes to read when extracting
243 /// CSRF tokens from body (POST/PUT/PATCH/DELETE)
244 pub max_body_bytes: usize,
245}
246
247impl CsrfMiddlewareConfig {
248 #[cfg(feature = "actix-session")]
249 /// Constructs a configuration for the Synchronizer Token pattern.
250 ///
251 /// Tokens are stored server-side in the session via `actix-session` and compared against
252 /// the value presented by the client.
253 ///
254 /// # Examples
255 /// Using cookie-based sessions (requires enabling the `actix-session` feature for this crate):
256 /// ```ignore
257 /// use actix_csrf_middleware::{CsrfMiddleware, CsrfMiddlewareConfig};
258 /// use actix_session::{SessionMiddleware, storage::CookieSessionStore};
259 /// use actix_web::{App, cookie::Key};
260 ///
261 /// let secret = b"a-very-long-application-secret-key-of-32+bytes";
262 /// let cfg = CsrfMiddlewareConfig::synchronizer_token(secret);
263 /// let app = App::new()
264 /// .wrap(SessionMiddleware::new(CookieSessionStore::default(), Key::generate()))
265 /// .wrap(CsrfMiddleware::new(cfg));
266 /// ```
267 pub fn synchronizer_token(secret_key: &[u8]) -> Self {
268 check_secret_key(secret_key);
269
270 CsrfMiddlewareConfig {
271 pattern: CsrfPattern::SynchronizerToken,
272 session_id_cookie_name: DEFAULT_SESSION_ID_KEY.to_string(),
273 token_cookie_name: DEFAULT_CSRF_TOKEN_KEY.into(),
274 anon_token_cookie_name: DEFAULT_CSRF_ANON_TOKEN_KEY.into(),
275 #[cfg(feature = "actix-session")]
276 anon_session_key_name: format!("{DEFAULT_CSRF_TOKEN_KEY}-anon"),
277 token_form_field: DEFAULT_CSRF_TOKEN_FIELD.into(),
278 token_header_name: DEFAULT_CSRF_TOKEN_HEADER.into(),
279 token_cookie_config: None,
280 secret_key: zeroize::Zeroizing::new(secret_key.into()),
281 skip_for: vec![],
282 manual_multipart: false,
283 enforce_origin: false,
284 allowed_origins: vec![],
285 max_body_bytes: 2 * 1024 * 1024, // 2 MiB default
286 }
287 }
288
289 /// Constructs a configuration for the Double-Submit Cookie pattern.
290 ///
291 /// The CSRF token is placed in a cookie and echoed by clients in a header or form field.
292 /// The token’s integrity is protected by an HMAC bound to the session id and the token.
293 ///
294 /// # Examples
295 /// ```
296 /// use actix_csrf_middleware::{CsrfMiddleware, CsrfMiddlewareConfig};
297 /// use actix_web::{App};
298 ///
299 /// let secret = b"a-very-long-application-secret-key-of-32+bytes";
300 /// let cfg = CsrfMiddlewareConfig::double_submit_cookie(secret);
301 /// let app = App::new().wrap(CsrfMiddleware::new(cfg));
302 /// ```
303 pub fn double_submit_cookie(secret_key: &[u8]) -> Self {
304 check_secret_key(secret_key);
305
306 CsrfMiddlewareConfig {
307 pattern: CsrfPattern::DoubleSubmitCookie,
308 session_id_cookie_name: DEFAULT_SESSION_ID_KEY.to_string(),
309 token_cookie_name: DEFAULT_CSRF_TOKEN_KEY.into(),
310 anon_token_cookie_name: DEFAULT_CSRF_ANON_TOKEN_KEY.into(),
311 #[cfg(feature = "actix-session")]
312 anon_session_key_name: format!("{DEFAULT_CSRF_TOKEN_KEY}-anon"),
313 token_form_field: DEFAULT_CSRF_TOKEN_FIELD.into(),
314 token_header_name: DEFAULT_CSRF_TOKEN_HEADER.into(),
315 token_cookie_config: Some(CsrfDoubleSubmitCookie {
316 http_only: false, // Should be false for double-submit cookie
317 secure: true,
318 same_site: SameSite::Strict,
319 }),
320 secret_key: zeroize::Zeroizing::new(secret_key.into()),
321 skip_for: vec![],
322 manual_multipart: false,
323 enforce_origin: false,
324 allowed_origins: vec![],
325 max_body_bytes: 2 * 1024 * 1024,
326 }
327 }
328
329 /// Controls whether `multipart/form-data` requests are allowed to pass through.
330 ///
331 /// When set to `true`, the middleware does not attempt to extract the CSRF token from a
332 /// multipart body. Your handler must read and validate the token manually.
333 ///
334 /// Defaults to `false` for safety.
335 pub fn with_multipart(mut self, multipart: bool) -> Self {
336 self.manual_multipart = multipart;
337 self
338 }
339
340 /// Sets the maximum number of request body bytes read when searching for a CSRF token
341 /// in JSON or `application/x-www-form-urlencoded` bodies.
342 ///
343 /// Defaults to 2 MiB.
344 pub fn with_max_body_bytes(mut self, limit: usize) -> Self {
345 self.max_body_bytes = limit;
346 self
347 }
348
349 /// Overrides cookie flags for token cookies (Double-Submit Cookie pattern).
350 ///
351 /// For Double-Submit Cookie, `http_only` must be `false` so client-side code can read
352 /// the cookie value and mirror it into a header or form field.
353 pub fn with_token_cookie_config(mut self, config: CsrfDoubleSubmitCookie) -> Self {
354 self.token_cookie_config = Some(config);
355 self
356 }
357
358 /// Skips CSRF validation for requests whose path starts with any of the given prefixes.
359 ///
360 /// Useful for health checks or public webhooks where CSRF is not applicable.
361 pub fn with_skip_for(mut self, patches: Vec<String>) -> Self {
362 self.skip_for = patches;
363 self
364 }
365
366 /// Enables strict Origin/Referer checks for mutating requests and sets the allowed origins.
367 ///
368 /// Origins are compared strictly by scheme, host, and port. If `allowed` is empty and
369 /// `enforce` is `true`, all mutating requests are rejected.
370 ///
371 /// Example enabling enforcement for a single origin:
372 /// ```
373 /// use actix_csrf_middleware::CsrfMiddlewareConfig;
374 ///
375 /// let cfg = CsrfMiddlewareConfig::double_submit_cookie(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
376 /// .with_enforce_origin(true, vec!["https://example.com".to_string()]);
377 /// ```
378 pub fn with_enforce_origin(mut self, enforce: bool, allowed: Vec<String>) -> Self {
379 self.enforce_origin = enforce;
380 self.allowed_origins = allowed;
381 self
382 }
383}
384
385/// Actix Web middleware providing CSRF protection.
386///
387/// Supports two patterns:
388/// - Double-Submit Cookie (default): a token is stored in a cookie and echoed by the client.
389/// - Synchronizer Token (with `actix-session`): a token is stored server-side in the session.
390///
391/// # How It Works
392/// - For safe methods (GET/HEAD), the middleware ensures a token exists and may set it in
393/// cookies. For the Double-Submit Cookie pattern, an anonymous pre-session cookie may be
394/// issued before the user is authenticated.
395/// - For mutating methods (POST/PUT/PATCH/DELETE), a token is required. The middleware
396/// accepts tokens from the header [`DEFAULT_CSRF_TOKEN_HEADER`] or the body field
397/// [`DEFAULT_CSRF_TOKEN_FIELD`] for JSON or url-encoded bodies. `multipart/form-data`
398/// is rejected unless [`CsrfMiddlewareConfig::with_multipart`] is enabled.
399/// - On successful validation, the token is rotated.
400/// - Optional strict Origin/Referer checks can be enabled via
401/// [`CsrfMiddlewareConfig::with_enforce_origin`].
402///
403/// # Examples
404/// Double-Submit Cookie (no session middleware required):
405/// ```
406/// use actix_csrf_middleware::{CsrfMiddleware, CsrfMiddlewareConfig, CsrfToken};
407/// use actix_web::{web, App, HttpResponse};
408///
409/// let secret = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; // >= 32 bytes
410/// let cfg = CsrfMiddlewareConfig::double_submit_cookie(secret);
411///
412/// let app = App::new()
413/// .wrap(CsrfMiddleware::new(cfg))
414/// .service(
415/// web::resource("/form").route(web::get().to(|csrf: CsrfToken| async move {
416/// HttpResponse::Ok().body(format!("token:{}", csrf.0))
417/// }))
418/// )
419/// .service(
420/// web::resource("/submit").route(web::post().to(|_csrf: CsrfToken| async move {
421/// HttpResponse::Ok()
422/// }))
423/// );
424/// ```
425///
426/// Synchronizer Token (requires `actix-session`) example:
427/// ```ignore
428/// use actix_csrf_middleware::{CsrfMiddleware, CsrfMiddlewareConfig};
429/// use actix_session::{storage::CookieSessionStore, SessionMiddleware};
430/// use actix_web::{App, cookie::Key};
431///
432/// let cfg = CsrfMiddlewareConfig::synchronizer_token(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
433/// let app = App::new()
434/// .wrap(SessionMiddleware::new(CookieSessionStore::default(), Key::generate()))
435/// .wrap(CsrfMiddleware::new(cfg));
436/// ```
437pub struct CsrfMiddleware {
438 config: Rc<CsrfMiddlewareConfig>,
439}
440
441impl CsrfMiddleware {
442 /// Creates a CSRF middleware instance with the given configuration.
443 ///
444 /// See [`CsrfMiddlewareConfig`] for available options and examples.
445 pub fn new(config: CsrfMiddlewareConfig) -> Self {
446 Self {
447 config: Rc::new(config),
448 }
449 }
450}
451
452impl<S, B> Transform<S, ServiceRequest> for CsrfMiddleware
453where
454 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
455 B: MessageBody,
456{
457 type Response = ServiceResponse<EitherBody<B>>;
458 type Error = Error;
459 type Transform = CsrfMiddlewareService<S>;
460 type InitError = ();
461 type Future = Ready<Result<Self::Transform, Self::InitError>>;
462
463 fn new_transform(&self, service: S) -> Self::Future {
464 ok(CsrfMiddlewareService {
465 service: Rc::new(service),
466 config: self.config.clone(),
467 })
468 }
469}
470
471pub struct CsrfMiddlewareService<S> {
472 service: Rc<S>,
473 config: Rc<CsrfMiddlewareConfig>,
474}
475
476impl<S> CsrfMiddlewareService<S> {
477 fn get_session_from_cookie(&self, req: &ServiceRequest) -> (String, bool, TokenClass) {
478 // Try to extract from session id cookie first,
479 // if nothing found then check pre-session or create new one.
480 if let Some(id) = req
481 .cookie(&self.config.session_id_cookie_name)
482 .map(|c| c.value().to_string())
483 {
484 (id, false, TokenClass::Authorized)
485 } else if let Some(val) = req
486 .cookie(CSRF_PRE_SESSION_KEY)
487 .map(|c| c.value().to_string())
488 {
489 // Validate signed/encrypted pre-session value; if invalid, rotate
490 if let Some(pre_id) = decode_pre_session_cookie(&val, self.config.secret_key.as_slice())
491 {
492 (pre_id, false, TokenClass::Anonymous)
493 } else {
494 (generate_random_token(), true, TokenClass::Anonymous)
495 }
496 } else {
497 // Generate pre-session id here
498 (generate_random_token(), true, TokenClass::Anonymous)
499 }
500 }
501
502 fn get_true_token(
503 &self,
504 req: &ServiceRequest,
505 session_id: Option<&str>,
506 class: TokenClass,
507 ) -> (String, bool) {
508 match self.config.pattern {
509 // If corresponding feature enabled then get token from persistent session storage
510 #[cfg(feature = "actix-session")]
511 CsrfPattern::SynchronizerToken => {
512 let session = req.get_session();
513 let key = match class {
514 TokenClass::Authorized => &self.config.token_cookie_name,
515 TokenClass::Anonymous => &self.config.anon_session_key_name,
516 };
517
518 let found = session.get::<String>(key).ok().flatten();
519
520 match found {
521 Some(tok) => (tok, false),
522 None => (generate_random_token(), true),
523 }
524 }
525 // Check for csrf token in request cookies
526 CsrfPattern::DoubleSubmitCookie => {
527 let (cookie_name, ctx) = match class {
528 TokenClass::Authorized => {
529 (&self.config.token_cookie_name, TokenClass::Authorized)
530 }
531 TokenClass::Anonymous => {
532 (&self.config.anon_token_cookie_name, TokenClass::Anonymous)
533 }
534 };
535
536 let token = { req.cookie(cookie_name).map(|c| c.value().to_string()) };
537
538 match token {
539 Some(tok) => (tok, false),
540 None => {
541 let secret = self.config.secret_key.as_slice();
542 let tok = generate_hmac_token_ctx(
543 ctx,
544 session_id.expect("Session or pre-session id is passed"),
545 secret,
546 );
547 (tok, true)
548 }
549 }
550 }
551 }
552 }
553
554 fn should_skip_validation(&self, req: &ServiceRequest) -> bool {
555 let req_path = req.path();
556 self.config
557 .skip_for
558 .iter()
559 .any(|prefix| req_path.starts_with(prefix))
560 }
561}
562
563impl<S, B> Service<ServiceRequest> for CsrfMiddlewareService<S>
564where
565 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
566 B: MessageBody,
567{
568 type Response = ServiceResponse<EitherBody<B>>;
569 type Error = Error;
570 type Future = Either<CsrfTokenValidator<S, B>, Ready<Result<Self::Response, Self::Error>>>;
571
572 forward_ready!(service);
573
574 fn call(&self, req: ServiceRequest) -> Self::Future {
575 if self.should_skip_validation(&req) {
576 let resp = CsrfResponse {
577 fut: self.service.call(req),
578 config: Some(self.config.clone()),
579 set_token: None,
580 set_pre_session: None,
581 token_class: None,
582 remove_pre_session: false,
583 _phantom: PhantomData,
584 };
585 return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
586 }
587
588 // Get current token from cookie or actix-session or generate new one
589 let (true_token, should_set_token, cookie_session, token_class): (
590 String,
591 bool,
592 Option<(String, bool)>,
593 Option<TokenClass>,
594 ) = match self.config.pattern {
595 CsrfPattern::DoubleSubmitCookie => {
596 let (session_id, set_pre_session, token_class) = self.get_session_from_cookie(&req);
597 let (true_token, should_set_token) =
598 self.get_true_token(&req, Some(&session_id), token_class);
599 (
600 true_token,
601 should_set_token,
602 Some((session_id, set_pre_session)),
603 Some(token_class),
604 )
605 }
606 #[cfg(feature = "actix-session")]
607 CsrfPattern::SynchronizerToken => {
608 // Derive class from cookies and set pre-session cookie if needed
609 let (session_id, set_pre_session, token_class) = self.get_session_from_cookie(&req);
610 let (token, should_set_token) = self.get_true_token(&req, None, token_class);
611
612 (
613 token,
614 should_set_token,
615 Some((session_id, set_pre_session)),
616 Some(token_class),
617 )
618 }
619 };
620
621 req.extensions_mut().insert(CsrfToken(true_token.clone()));
622 req.extensions_mut().insert(self.config.clone());
623
624 let is_mutating = matches!(
625 *req.method(),
626 Method::POST | Method::PUT | Method::PATCH | Method::DELETE
627 );
628
629 // Skip validation for read only requests, but csrf token still should be
630 // added to the response when should_set_token flag is set to true.
631 if !is_mutating {
632 let mut set_token_bytes = if should_set_token {
633 Some(true_token.clone())
634 } else {
635 None
636 };
637
638 let session_id = if let Some((ref session_id, set_pre_session)) = cookie_session {
639 if set_pre_session {
640 Some(session_id.clone())
641 } else {
642 None
643 }
644 } else {
645 None
646 };
647
648 // Ensure an authorized token cookie exists after login (DoubleSubmitCookie only)
649 if self.config.pattern == CsrfPattern::DoubleSubmitCookie {
650 if let (Some(TokenClass::Authorized), Some((ref sess_id, _))) =
651 (token_class, cookie_session.as_ref())
652 {
653 // If no authorized token cookie yet, issue one now
654 if req.cookie(&self.config.token_cookie_name).is_none() {
655 let tok = generate_hmac_token_ctx(
656 TokenClass::Authorized,
657 sess_id,
658 self.config.secret_key.as_slice(),
659 );
660 set_token_bytes = Some(tok);
661 }
662 }
663 }
664
665 let remove_pre_session = matches!(token_class, Some(TokenClass::Authorized));
666 let resp = CsrfResponse {
667 fut: self.service.call(req),
668 config: Some(self.config.clone()),
669 set_token: set_token_bytes,
670 set_pre_session: session_id,
671 token_class,
672 remove_pre_session,
673 _phantom: PhantomData,
674 };
675
676 return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
677 }
678
679 // Optionally enforce Origin/Referer before token checks
680 if self.config.enforce_origin && !origin_allowed(req.headers(), &self.config) {
681 let resp = HttpResponse::with_body(StatusCode::FORBIDDEN, "Invalid request origin");
682 return Either::right(ok(req
683 .into_response(resp)
684 .map_into_boxed_body()
685 .map_into_right_body()));
686 }
687
688 // Otherwise, process mutating request with token
689 // extraction from the body and future validation.
690
691 // Handle multipart form data requests
692 if let Some(ct) = req
693 .headers()
694 .get(header::CONTENT_TYPE)
695 .and_then(|hv| hv.to_str().ok())
696 {
697 if ct.starts_with("multipart/form-data") {
698 // Deny any multipart/form-data requests if
699 // it isn't allowed explicitly by the consumer.
700 if !self.config.manual_multipart {
701 let resp = HttpResponse::with_body(
702 StatusCode::BAD_REQUEST,
703 "Multipart form data is not enabled by csrf config",
704 );
705
706 return Either::right(ok(req
707 .into_response(resp)
708 .map_into_boxed_body()
709 .map_into_right_body()));
710 }
711
712 // Then consumer reads body, extracts and
713 // verifies csrf tokens manually in their handlers.
714 let resp = CsrfResponse {
715 fut: self.service.call(req),
716 config: Some(self.config.clone()),
717 set_token: None,
718 set_pre_session: None,
719 token_class: None,
720 remove_pre_session: false,
721 _phantom: PhantomData,
722 };
723
724 return Either::left(CsrfTokenValidator::CsrfResponse { response: resp });
725 }
726 }
727
728 let (session_id, token_class) = if let Some((session_id, _)) = cookie_session {
729 (Some(session_id), token_class)
730 } else {
731 (None, token_class)
732 };
733
734 // Try to extract csrf token from header
735 let header_token = req
736 .headers()
737 .get(&self.config.token_header_name)
738 .and_then(|hv| hv.to_str().ok())
739 .map(|s| s.to_string());
740
741 // Fastest and easiest way when token just received in headers
742 if let Some(token) = header_token {
743 return Either::left(CsrfTokenValidator::MutatingRequest {
744 service: self.service.clone(),
745 config: self.config.clone(),
746 true_token,
747 client_token: token,
748 session_id,
749 token_class,
750 req: Some(req),
751 });
752 }
753
754 // For mutating requests without header token, read body first
755 let mut req2 = req;
756 let payload = req2.take_payload();
757
758 // Pre-allocate body buffer using Content-Length when available and within limit
759 let initial_capacity = req2
760 .headers()
761 .get(header::CONTENT_LENGTH)
762 .and_then(|hv| hv.to_str().ok())
763 .and_then(|s| s.parse::<usize>().ok())
764 .map(|n| n.min(self.config.max_body_bytes))
765 .unwrap_or(0);
766
767 let body_buf = if initial_capacity > 0 {
768 BytesMut::with_capacity(initial_capacity)
769 } else {
770 BytesMut::new()
771 };
772
773 Either::left(CsrfTokenValidator::ReadingBody {
774 req: Some(req2),
775 payload: Some(payload),
776 body_bytes: body_buf,
777 config: self.config.clone(),
778 service: self.service.clone(),
779 true_token,
780 session_id,
781 token_class,
782 })
783 }
784}
785
786pin_project! {
787 #[project = CsrfTokenValidatorProj]
788 pub enum CsrfTokenValidator<S, B>
789 where
790 S: Service<ServiceRequest>,
791 B: MessageBody,
792 {
793 CsrfResponse {
794 #[pin]
795 response: CsrfResponse<S, B>,
796 },
797 MutatingRequest {
798 service: Rc<S>,
799 config: Rc<CsrfMiddlewareConfig>,
800 true_token: String,
801 client_token: String,
802 session_id: Option<String>,
803 token_class: Option<TokenClass>,
804 req: Option<ServiceRequest>
805 },
806 ReadingBody {
807 service: Rc<S>,
808 config: Rc<CsrfMiddlewareConfig>,
809 req: Option<ServiceRequest>,
810 payload: Option<actix_web::dev::Payload>,
811 body_bytes: BytesMut,
812 true_token: String,
813 session_id: Option<String>,
814 token_class: Option<TokenClass>,
815 },
816 }
817}
818
819impl<S, B> Future for CsrfTokenValidator<S, B>
820where
821 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
822 B: MessageBody,
823{
824 type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
825
826 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
827 match self.as_mut().project() {
828 CsrfTokenValidatorProj::CsrfResponse { response } => response.poll(cx),
829 CsrfTokenValidatorProj::MutatingRequest {
830 service,
831 config,
832 true_token,
833 client_token,
834 session_id,
835 token_class,
836 req,
837 } => {
838 #[cfg(not(feature = "actix-session"))]
839 let _ = &true_token;
840
841 if let Some(req) = req.take() {
842 // Session id cannot be empty with DoubleSubmitCookie pattern
843 let session_id = if config.pattern == CsrfPattern::DoubleSubmitCookie {
844 if let Some(id) = session_id.take() {
845 Some(id)
846 } else {
847 let resp = HttpResponse::with_body(
848 StatusCode::INTERNAL_SERVER_ERROR,
849 "Session id is empty in csrf token validator".to_string(),
850 );
851
852 return Poll::Ready(Ok(req
853 .into_response(resp)
854 .map_into_boxed_body()
855 .map_into_right_body()));
856 }
857 } else {
858 None
859 };
860
861 // Validate client token based on the pattern
862 let valid = match &config.pattern {
863 #[cfg(feature = "actix-session")]
864 CsrfPattern::SynchronizerToken => {
865 if eq_tokens(true_token.as_bytes(), client_token.as_bytes()) {
866 true
867 } else {
868 let alt_valid = {
869 let session = req.get_session();
870 let alt_key = match token_class
871 .as_ref()
872 .copied()
873 .unwrap_or(TokenClass::Authorized)
874 {
875 TokenClass::Authorized => &config.anon_session_key_name,
876 TokenClass::Anonymous => &config.token_cookie_name,
877 };
878 let alt = session.get::<String>(alt_key).ok().flatten();
879
880 alt.map(|t| eq_tokens(t.as_bytes(), client_token.as_bytes()))
881 .unwrap_or(false)
882 };
883
884 alt_valid
885 }
886 }
887 CsrfPattern::DoubleSubmitCookie => {
888 let ctx = token_class
889 .as_ref()
890 .copied()
891 .unwrap_or(TokenClass::Anonymous);
892 validate_hmac_token_ctx(
893 ctx,
894 session_id
895 .as_deref()
896 .expect("session id cannot be empty is hmac validation"),
897 client_token.as_bytes(),
898 config.secret_key.as_slice(),
899 )
900 .unwrap_or(false)
901 }
902 };
903
904 if !valid {
905 let resp = HttpResponse::BadRequest().body("Invalid CSRF token");
906 return Poll::Ready(Ok(req
907 .into_response(resp)
908 .map_into_boxed_body()
909 .map_into_right_body()));
910 }
911
912 // Rotate token based on configured pattern after every successful validation
913 let new_token = match &config.pattern {
914 #[cfg(feature = "actix-session")]
915 CsrfPattern::SynchronizerToken => generate_random_token(),
916 CsrfPattern::DoubleSubmitCookie => {
917 let ctx = token_class
918 .as_ref()
919 .copied()
920 .unwrap_or(TokenClass::Anonymous);
921 generate_hmac_token_ctx(
922 ctx,
923 session_id
924 .as_deref()
925 .expect("session id cannot be empty is hmac validation"),
926 config.secret_key.as_ref(),
927 )
928 }
929 };
930
931 let resp = CsrfResponse {
932 fut: service.call(req),
933 config: Some(config.clone()),
934 set_token: Some(new_token),
935 set_pre_session: None,
936 token_class: *token_class,
937 remove_pre_session: false,
938 _phantom: PhantomData,
939 };
940
941 self.set(CsrfTokenValidator::CsrfResponse { response: resp });
942
943 cx.waker().wake_by_ref(); // wake for the next pool
944 Poll::Pending
945 } else {
946 error!("request already taken in csrf validator's state machine");
947
948 Poll::Ready(Err(actix_web::error::ErrorInternalServerError(
949 "Request was already taken",
950 )))
951 }
952 }
953 CsrfTokenValidatorProj::ReadingBody {
954 service,
955 config,
956 req,
957 payload,
958 body_bytes,
959 true_token,
960 session_id,
961 token_class,
962 } => {
963 if req.is_none() {
964 error!("request already taken in csrf validator's state machine");
965 return Poll::Ready(Err(actix_web::error::ErrorInternalServerError(
966 "Request was already taken",
967 )));
968 }
969
970 // Safe: just checked
971 let request_mut = req.as_mut().unwrap();
972 let payload = match payload.as_mut() {
973 Some(p) => p,
974 None => {
975 error!("payload missing in reading body state");
976 return Poll::Ready(Err(actix_web::error::ErrorInternalServerError(
977 "Payload missing",
978 )));
979 }
980 };
981
982 match payload.poll_next_unpin(cx) {
983 Poll::Pending => Poll::Pending,
984 Poll::Ready(Some(Ok(bytes))) => {
985 body_bytes.extend_from_slice(&bytes);
986
987 if body_bytes.len() > config.max_body_bytes {
988 let req_owned = req.take().unwrap();
989 let resp = HttpResponse::with_body(
990 StatusCode::PAYLOAD_TOO_LARGE,
991 "Request body too large for CSRF token extraction",
992 );
993
994 return Poll::Ready(Ok(req_owned
995 .into_response(resp)
996 .map_into_boxed_body()
997 .map_into_right_body()));
998 }
999
1000 cx.waker().wake_by_ref();
1001
1002 Poll::Pending
1003 }
1004 Poll::Ready(Some(Err(e))) => {
1005 Poll::Ready(Err(actix_web::error::ErrorBadRequest(e)))
1006 }
1007 Poll::Ready(None) => {
1008 let body = std::mem::take(&mut *body_bytes).freeze();
1009 let client_token = match sync_read_token_from_body(
1010 request_mut.headers(),
1011 &body,
1012 &config.token_form_field,
1013 ) {
1014 Some(token) => token,
1015 None => {
1016 let req_owned = req.take().unwrap();
1017 let res = HttpResponse::BadRequest().body("CSRF token is required");
1018 return Poll::Ready(Ok(req_owned
1019 .into_response(res)
1020 .map_into_boxed_body()
1021 .map_into_right_body()));
1022 }
1023 };
1024
1025 request_mut.set_payload(actix_web::dev::Payload::from(body.clone()));
1026
1027 let req_owned = req.take().unwrap();
1028 let next_state = {
1029 let service = service.clone();
1030 let config = config.clone();
1031 let true_token = std::mem::take(true_token);
1032 let session_id = session_id.take();
1033 let token_class = token_class.take();
1034 let req = Some(req_owned);
1035
1036 CsrfTokenValidator::MutatingRequest {
1037 service,
1038 config,
1039 true_token,
1040 client_token,
1041 session_id,
1042 token_class,
1043 req,
1044 }
1045 };
1046
1047 self.set(next_state);
1048 cx.waker().wake_by_ref();
1049
1050 Poll::Pending
1051 }
1052 }
1053 }
1054 }
1055 }
1056}
1057
1058fn sync_read_token_from_body(
1059 headers: &HeaderMap,
1060 body: &[u8],
1061 token_field: &str,
1062) -> Option<String> {
1063 if let Some(ct) = headers.get(header::CONTENT_TYPE) {
1064 if let Ok(ct) = ct.to_str() {
1065 if ct.starts_with("application/json") {
1066 if let Ok(json) = serde_json::from_slice::<serde_json::Value>(body) {
1067 return json
1068 .get(token_field)
1069 .and_then(|v| v.as_str().map(String::from));
1070 }
1071 } else if ct.starts_with("application/x-www-form-urlencoded") {
1072 if let Ok(form) = serde_urlencoded::from_bytes::<HashMap<String, String>>(body) {
1073 return form.get(token_field).cloned();
1074 }
1075 } else {
1076 warn!("unsupported request content type, unable to extract and verify csrf token");
1077 }
1078 }
1079 }
1080 None
1081}
1082
1083pin_project! {
1084 pub struct CsrfResponse<S, B>
1085 where
1086 S: Service<ServiceRequest>,
1087 B: MessageBody,
1088 {
1089 #[pin]
1090 fut: S::Future,
1091 config: Option<Rc<CsrfMiddlewareConfig>>,
1092 set_token: Option<String>,
1093 set_pre_session: Option<String>,
1094 token_class: Option<TokenClass>,
1095 remove_pre_session: bool,
1096 _phantom: PhantomData<B>,
1097 }
1098}
1099
1100impl<S, B> Future for CsrfResponse<S, B>
1101where
1102 B: MessageBody,
1103 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
1104{
1105 type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
1106
1107 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1108 let this = self.as_mut().project();
1109 match ready!(this.fut.poll(cx)) {
1110 Ok(mut resp) => {
1111 let config = match &this.config {
1112 Some(config) => config,
1113 None => {
1114 let res = HttpResponse::InternalServerError()
1115 .body("An empty csrf middleware config passed into csrf response");
1116
1117 error!("unable to extract csrf middleware config in csrf response");
1118 return Poll::Ready(Ok(resp
1119 .into_response(res)
1120 .map_into_boxed_body()
1121 .map_into_right_body()));
1122 }
1123 };
1124
1125 // Set pre-session if requested
1126 if let Some(pre_session_id) = this.set_pre_session {
1127 let cookie_val =
1128 encode_pre_session_cookie(pre_session_id, config.secret_key.as_slice());
1129
1130 match resp.response_mut().add_cookie(
1131 &Cookie::build(CSRF_PRE_SESSION_KEY, cookie_val)
1132 .http_only(PRE_SESSION_HTTP_ONLY)
1133 .secure(PRE_SESSION_SECURE)
1134 .same_site(PRE_SESSION_SAME_SITE)
1135 .path("/")
1136 .finish(),
1137 ) {
1138 Ok(_) => {}
1139 Err(e) => {
1140 let res = HttpResponse::InternalServerError()
1141 .body("Unable to set pre-session cookie");
1142
1143 error!("unable to set pre-session cookie in csrf response: {e:?}");
1144 return Poll::Ready(Ok(resp
1145 .into_response(res)
1146 .map_into_boxed_body()
1147 .map_into_right_body()));
1148 }
1149 }
1150 }
1151
1152 // If requested, clear pre-session cookie and anon token cookie
1153 if *this.remove_pre_session {
1154 // Expire pre-session
1155 let mut del = Cookie::new(CSRF_PRE_SESSION_KEY.to_string(), "");
1156 del.set_max_age(time::Duration::seconds(0));
1157 del.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1158 del.set_path("/");
1159 del.set_http_only(PRE_SESSION_HTTP_ONLY);
1160 del.set_secure(PRE_SESSION_SECURE);
1161 del.set_same_site(PRE_SESSION_SAME_SITE);
1162
1163 if let Err(e) = resp.response_mut().add_cookie(&del) {
1164 let res = HttpResponse::InternalServerError()
1165 .body("Failed to expire pre-session cookie");
1166
1167 error!("unable to expire pre-session cookie in csrf response: {e:?}");
1168 return Poll::Ready(Ok(resp
1169 .into_response(res)
1170 .map_into_boxed_body()
1171 .map_into_right_body()));
1172 }
1173
1174 // Expire anonymous token cookie
1175 if matches!(config.pattern, CsrfPattern::DoubleSubmitCookie) {
1176 let mut del_tok = Cookie::new(config.anon_token_cookie_name.clone(), "");
1177 del_tok.set_max_age(time::Duration::seconds(0));
1178 del_tok.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1179 del_tok.set_path("/");
1180
1181 if let Err(e) = resp.response_mut().add_cookie(&del_tok) {
1182 let res = HttpResponse::InternalServerError()
1183 .body("Failed to expire anon token cookie");
1184
1185 error!("unable to expire anon token cookie in csrf response: {e:?}");
1186 return Poll::Ready(Ok(resp
1187 .into_response(res)
1188 .map_into_boxed_body()
1189 .map_into_right_body()));
1190 }
1191 }
1192 }
1193
1194 // Based on configured pattern, set a new token or rotate
1195 // the old one for the service response if pattern is passed.
1196 if let Some(new_token) = this.set_token.take() {
1197 match config.pattern {
1198 #[cfg(feature = "actix-session")]
1199 CsrfPattern::SynchronizerToken => {
1200 if *this.remove_pre_session {
1201 let _ = resp
1202 .request()
1203 .get_session()
1204 .remove(&config.anon_session_key_name);
1205 }
1206
1207 // Set a new token into actix session under key decided by class
1208 let key = match this.token_class.unwrap_or(TokenClass::Authorized) {
1209 TokenClass::Authorized => &config.token_cookie_name,
1210 TokenClass::Anonymous => &config.anon_session_key_name,
1211 };
1212
1213 match resp.request().get_session().insert(key, new_token) {
1214 Ok(()) => {}
1215 Err(e) => {
1216 let res = HttpResponse::with_body(
1217 StatusCode::INTERNAL_SERVER_ERROR,
1218 "Failed to insert CSRF token into session",
1219 );
1220
1221 error!("unable to set a csrf token with actix session in csrf response: {e:?}");
1222 return Poll::Ready(Ok(resp
1223 .into_response(res)
1224 .map_into_boxed_body()
1225 .map_into_right_body()));
1226 }
1227 }
1228 }
1229 CsrfPattern::DoubleSubmitCookie => {
1230 let cookie_config = match &config.token_cookie_config {
1231 Some(config) => config,
1232 None => {
1233 let res = HttpResponse::InternalServerError().body(
1234 "An empty csrf cookie config passed into csrf response",
1235 );
1236
1237 error!(
1238 "unable to extract token_cookie_config in csrf response"
1239 );
1240 return Poll::Ready(Ok(resp
1241 .into_response(res)
1242 .map_into_boxed_body()
1243 .map_into_right_body()));
1244 }
1245 };
1246
1247 // Choose cookie name based on token class
1248 let cookie_name =
1249 match this.token_class.unwrap_or(TokenClass::Anonymous) {
1250 TokenClass::Authorized => &config.token_cookie_name,
1251 TokenClass::Anonymous => &config.anon_token_cookie_name,
1252 };
1253
1254 let new_token_cookie = Cookie::build(cookie_name, new_token)
1255 .http_only(cookie_config.http_only)
1256 .secure(cookie_config.secure)
1257 .same_site(cookie_config.same_site)
1258 .path("/")
1259 .finish();
1260
1261 // Update token cookie with a new token
1262 match resp.response_mut().add_cookie(&new_token_cookie) {
1263 Ok(_) => {}
1264 Err(e) => {
1265 let res = HttpResponse::InternalServerError()
1266 .body("Failed to set a new csrf token");
1267
1268 error!("unable to set a token cookie in csrf response: {e:?}");
1269 return Poll::Ready(Ok(resp
1270 .into_response(res)
1271 .map_into_boxed_body()
1272 .map_into_right_body()));
1273 }
1274 }
1275 }
1276 }
1277 }
1278
1279 Poll::Ready(Ok(resp.map_into_left_body()))
1280 }
1281 Err(err) => Poll::Ready(Err(err)),
1282 }
1283 }
1284}
1285
1286#[derive(Clone)]
1287/// Extractor for the current CSRF token.
1288///
1289/// - On safe requests (GET/HEAD), ensures a token exists and makes it available to handlers.
1290/// - On mutating requests (POST/PUT/PATCH/DELETE), extracting [`CsrfToken`] verifies the
1291/// token before your handler is called; if verification fails, the request is rejected and
1292/// your handler is not executed.
1293///
1294/// # Examples
1295/// Read the token in a form-rendering handler and embed it into HTML or a JSON response.
1296/// ```
1297/// use actix_csrf_middleware::CsrfToken;
1298/// use actix_web::{HttpResponse, Responder};
1299///
1300/// async fn form(csrf: CsrfToken) -> impl Responder {
1301/// HttpResponse::Ok().body(format!("token:{}", csrf.0))
1302/// }
1303/// ```
1304///
1305/// Note: Using this extractor requires the middleware to be installed via
1306/// [`CsrfMiddleware::new`]. If not configured, extraction will fail with an internal error.
1307pub struct CsrfToken(pub String);
1308
1309impl FromRequest for CsrfToken {
1310 type Error = Error;
1311 type Future = Ready<Result<Self, Self::Error>>;
1312
1313 fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future {
1314 match req.extensions().get::<CsrfToken>() {
1315 Some(token) => ok(token.clone()),
1316 None => err(actix_web::error::ErrorInternalServerError(
1317 "CSRF middleware is not configured",
1318 )),
1319 }
1320 }
1321}
1322
1323/// Extension trait for Actix [`HttpRequest`] to rotate the CSRF token in a response.
1324///
1325/// This is a convenience wrapper around [`rotate_csrf_token_in_response`], allowing you to
1326/// rotate tokens without passing configuration explicitly. Typical use-cases include
1327/// rotating the token immediately after login or privilege escalation.
1328///
1329/// # Examples
1330/// Rotate after a successful login:
1331/// ```
1332/// use actix_csrf_middleware::CsrfRequestExt;
1333/// use actix_web::{HttpRequest, HttpResponse};
1334///
1335/// async fn after_login(req: HttpRequest) -> actix_web::Result<HttpResponse> {
1336/// let session_id = "user-session-id";
1337///
1338/// let mut resp = HttpResponse::Ok();
1339/// req.rotate_csrf_token_in_response(session_id, &mut resp)?;
1340///
1341/// Ok(resp.finish())
1342/// }
1343/// ```
1344pub trait CsrfRequestExt {
1345 /// Rotates the CSRF token and writes it to the outgoing response according to the
1346 /// configured pattern.
1347 fn rotate_csrf_token_in_response(
1348 &self,
1349 session_id: &str,
1350 resp: &mut HttpResponseBuilder,
1351 ) -> Result<(), Error>;
1352}
1353
1354impl CsrfRequestExt for HttpRequest {
1355 fn rotate_csrf_token_in_response(
1356 &self,
1357 session_id: &str,
1358 resp: &mut HttpResponseBuilder,
1359 ) -> Result<(), Error> {
1360 let cfg_rc: Rc<CsrfMiddlewareConfig> =
1361 match self.extensions().get::<Rc<CsrfMiddlewareConfig>>() {
1362 Some(cfg_rc_ref) => cfg_rc_ref.clone(),
1363 None => {
1364 return Err(actix_web::error::ErrorInternalServerError(
1365 "CSRF middleware config not found in request extensions",
1366 ))
1367 }
1368 };
1369
1370 rotate_csrf_token_in_response(session_id, self, resp, cfg_rc.as_ref())
1371 }
1372}
1373
1374/// Generates a cryptographically secure random CSRF token.
1375///
1376/// The token is 32 bytes of randomness, encoded with URL-safe base64 without padding
1377/// (aka base64url), resulting in a 43-character ASCII string. The alphabet is limited to
1378/// `A-Z`, `a-z`, `0-9`, `-`, and `_`, making it safe for use in URLs, HTTP headers, and
1379/// HTML form fields without additional escaping.
1380///
1381/// This function returns a standalone random value and does not bind the token to any
1382/// session or identity. For the Double-Submit Cookie pattern used by this crate, prefer
1383/// [`generate_hmac_token_ctx`] which derives an HMAC-protected token from a session id
1384/// and the token, making it unforgeable by clients.
1385///
1386/// # Security
1387/// - The token is generated using a CSPRNG and is suitable for CSRF defenses.
1388/// - When using the Double-Submit Cookie pattern, do not place this raw token into a
1389/// cookie by itself. Use [`generate_hmac_token_ctx`] so the server can verify integrity.
1390/// - When using the Synchronizer Token pattern (feature `actix-session`), this raw token
1391/// may be stored server-side in session and compared using constant-time equality.
1392///
1393/// # Examples
1394/// Generate a token and validate its shape.
1395/// ```
1396/// let tok = actix_csrf_middleware::generate_random_token();
1397/// assert_eq!(tok.len(), 43, "32 bytes base64url-encoded -> 43 chars");
1398/// assert!(tok.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'));
1399/// ```
1400///
1401/// Produce an HMAC-protected token for Double-Submit Cookie flows.
1402/// ```
1403/// use actix_csrf_middleware::{generate_random_token, generate_hmac_token_ctx, TokenClass};
1404///
1405/// let session_id = "user-session-id";
1406/// let secret = b"an-application-wide-secret-at-least-32-bytes-long";
1407/// let raw = generate_random_token();
1408///
1409/// // In typical flows you would call `generate_hmac_token_ctx` directly without
1410/// // generating the raw token yourself; shown here for illustration.
1411/// let hmac_token = generate_hmac_token_ctx(TokenClass::Authorized, session_id, secret);
1412/// assert!(hmac_token.contains('.'));
1413///
1414/// let parts: Vec<_> = hmac_token.split('.').collect();
1415/// assert_eq!(parts.len(), 2);
1416/// ```
1417pub fn generate_random_token() -> String {
1418 let mut buf = [0u8; TOKEN_LEN];
1419 rand::rng().fill_bytes(&mut buf);
1420 URL_SAFE_NO_PAD.encode(buf)
1421}
1422
1423/// Generates an HMAC-protected CSRF token bound to a context and identifier.
1424///
1425/// The produced token has the shape `HEX_HMAC.RANDOM`, where:
1426/// - `RANDOM` is a fresh value from [`generate_random_token`].
1427/// - `HEX_HMAC` is a hex-encoded HMAC-SHA256 over the message
1428/// `"{class}|{id}|{RANDOM}"`, using the provided `secret`.
1429///
1430/// This format is designed for the Double-Submit Cookie pattern:
1431/// - The server sets the token as a cookie and expects clients to echo it back via a
1432/// form field or header.
1433/// - On receipt, the server recomputes the HMAC with the same `class`, `id`, and `secret`.
1434/// If the HMAC matches, the token is authentic and not forgeable by the client.
1435///
1436/// The `class` determines which logical bucket the token belongs to:
1437/// - [`TokenClass::Authorized`]: token that is bound to an authenticated session.
1438/// - [`TokenClass::Anonymous`]: token used before authentication (pre-session).
1439///
1440/// # Parameters
1441/// - `class`: Distinguishes the token namespace (authorized vs. anonymous).
1442/// - `id`: Identifier bound into the token (e.g., a session id); must be the same value
1443/// used later for verification.
1444/// - `secret`: Application-wide secret key (>= 32 bytes recommended). Changing the secret
1445/// invalidates all existing tokens immediately.
1446///
1447/// # Security
1448/// - Tokens are unforgeable without knowledge of `secret` and `id`.
1449/// - Use different `class` values to prevent confusion between anonymous and authorized
1450/// tokens; they are not interchangeable.
1451/// - Choose a high-entropy `secret` with at least 32 bytes.
1452///
1453/// # Examples
1454/// Generate and verify an authorized token.
1455/// ```
1456/// use actix_csrf_middleware::{generate_hmac_token_ctx, validate_hmac_token_ctx, TokenClass};
1457///
1458/// let session_id = "user-session-id";
1459/// let secret = b"an-application-wide-secret-at-least-32-bytes!";
1460/// let tok = generate_hmac_token_ctx(TokenClass::Authorized, session_id, secret);
1461///
1462/// assert!(tok.contains('.'));
1463/// assert!(validate_hmac_token_ctx(TokenClass::Authorized, session_id, tok.as_bytes(), secret).unwrap());
1464/// ```
1465///
1466/// Generate an anonymous token (pre-session) and verify it with the same `id` and `class`.
1467/// ```
1468/// use actix_csrf_middleware::{generate_hmac_token_ctx, validate_hmac_token_ctx, TokenClass};
1469///
1470/// let pre_session_id = "pre-session";
1471/// let secret = b"an-application-wide-secret-at-least-32-bytes!";
1472/// let tok = generate_hmac_token_ctx(TokenClass::Anonymous, pre_session_id, secret);
1473///
1474/// assert!(validate_hmac_token_ctx(TokenClass::Anonymous, pre_session_id, tok.as_bytes(), secret).unwrap());
1475/// ```
1476pub fn generate_hmac_token_ctx(class: TokenClass, id: &str, secret: &[u8]) -> String {
1477 let tok = generate_random_token();
1478 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
1479 mac.update(class.as_str().as_bytes());
1480 mac.update(b"|");
1481 mac.update(id.as_bytes());
1482 mac.update(b"|");
1483 mac.update(tok.as_bytes());
1484
1485 let hmac_hex = hex::encode(mac.finalize().into_bytes());
1486 format!("{hmac_hex}.{tok}")
1487}
1488
1489/// Constant-time equality for token byte slices.
1490///
1491/// Uses a timing-attack resistant comparison to avoid leaking information about token
1492/// values. Prefer using higher-level helpers for CSRF validation, but this function is
1493/// useful when verifying raw secrets or signatures.
1494///
1495/// # Examples
1496/// ```
1497/// use actix_csrf_middleware::eq_tokens;
1498/// assert!(eq_tokens(b"abc", b"abc"));
1499/// assert!(!eq_tokens(b"abc", b"abcd"));
1500/// ```
1501pub fn eq_tokens(token_a: &[u8], token_b: &[u8]) -> bool {
1502 token_a.ct_eq(token_b).unwrap_u8() == 1
1503}
1504
1505fn encode_pre_session_cookie(id: &str, secret: &[u8]) -> String {
1506 let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
1507 mac.update(b"pre|");
1508 mac.update(id.as_bytes());
1509
1510 let sig = hex::encode(mac.finalize().into_bytes());
1511 format!("{sig}.{id}")
1512}
1513
1514fn decode_pre_session_cookie(val: &str, secret: &[u8]) -> Option<String> {
1515 let parts: Vec<&str> = val.split('.').collect();
1516 if parts.len() != 2 {
1517 return None;
1518 }
1519
1520 let (sig_hex, id) = (parts[0], parts[1]);
1521 let sig_bytes = hex::decode(sig_hex).ok()?;
1522
1523 let mut mac = Hmac::<Sha256>::new_from_slice(secret).ok()?;
1524 mac.update(b"pre|");
1525 mac.update(id.as_bytes());
1526
1527 let expected = mac.finalize().into_bytes();
1528
1529 if eq_tokens(&expected, &sig_bytes) {
1530 Some(id.to_string())
1531 } else {
1532 None
1533 }
1534}
1535
1536/// Verifies an HMAC-protected CSRF token for a given class and identifier.
1537///
1538/// Accepts tokens in the `HEX_HMAC.RANDOM` format produced by
1539/// [`generate_hmac_token_ctx`]. Returns `Ok(true)` on a valid token and `Ok(false)` on
1540/// structural or verification failure. Returns an `Err` only for malformed UTF-8 or
1541/// hex-decoding errors while parsing.
1542///
1543/// The token is recomputed as HMAC-SHA256 over `"{class}|{id}|{RANDOM}"` using the
1544/// provided `secret` and compared in constant time.
1545///
1546/// # Errors
1547/// - Returns `Err` if `token` is not valid UTF-8.
1548/// - Returns `Err` if the HMAC hex part cannot be decoded.
1549///
1550/// # Examples
1551/// ```
1552/// use actix_csrf_middleware::{
1553/// generate_hmac_token_ctx, validate_hmac_token_ctx, TokenClass
1554/// };
1555///
1556/// let sid = "SID-xyz";
1557/// let secret = b"application-secret-at-least-32-bytes-long";
1558/// let token = generate_hmac_token_ctx(TokenClass::Authorized, sid, secret);
1559///
1560/// assert!(validate_hmac_token_ctx(TokenClass::Authorized, sid, token.as_bytes(), secret).unwrap());
1561///
1562/// // Wrong class or id will fail verification
1563/// assert!(!validate_hmac_token_ctx(TokenClass::Anonymous, sid, token.as_bytes(), secret).unwrap());
1564/// assert!(!validate_hmac_token_ctx(TokenClass::Authorized, "SID-other", token.as_bytes(), secret).unwrap());
1565/// ```
1566pub fn validate_hmac_token_ctx(
1567 class: TokenClass,
1568 id: &str,
1569 token: &[u8],
1570 secret: &[u8],
1571) -> Result<bool, Error> {
1572 let token_str = std::str::from_utf8(token)?;
1573 let parts: Vec<&str> = token_str.split('.').collect();
1574 if parts.len() != 2 {
1575 return Ok(false);
1576 }
1577
1578 let (hmac_hex, csrf_token) = (parts[0], parts[1]);
1579 let hmac_bytes = hex::decode(hmac_hex).map_err(actix_web::error::ErrorInternalServerError)?;
1580
1581 let mut mac = Hmac::<Sha256>::new_from_slice(secret)
1582 .map_err(actix_web::error::ErrorInternalServerError)?;
1583 mac.update(class.as_str().as_bytes());
1584 mac.update(b"|");
1585 mac.update(id.as_bytes());
1586 mac.update(b"|");
1587 mac.update(csrf_token.as_bytes());
1588
1589 let expected_hmac = mac.finalize().into_bytes();
1590
1591 Ok(eq_tokens(&expected_hmac, &hmac_bytes))
1592}
1593
1594/// Convenience helper to validate an authorized-class CSRF token.
1595///
1596/// This is a thin wrapper around [`validate_hmac_token_ctx`] that always uses
1597/// [`TokenClass::Authorized`]. It is intended for tests and simple validation flows
1598/// where only authorized tokens are expected.
1599///
1600/// # Examples
1601/// ```
1602/// use actix_csrf_middleware::{
1603/// generate_hmac_token_ctx, validate_hmac_token, TokenClass
1604/// };
1605///
1606/// let sid = "SID-xyz";
1607/// let secret = b"application-secret-at-least-32-bytes-long";
1608/// let token = generate_hmac_token_ctx(TokenClass::Authorized, sid, secret);
1609///
1610/// assert!(validate_hmac_token(sid, token.as_bytes(), secret).unwrap());
1611/// ```
1612pub fn validate_hmac_token(session_id: &str, token: &[u8], secret: &[u8]) -> Result<bool, Error> {
1613 validate_hmac_token_ctx(TokenClass::Authorized, session_id, token, secret)
1614}
1615
1616/// Rotates the CSRF token and writes any necessary cookie updates to the response.
1617///
1618/// - Double-Submit Cookie: requires a session id cookie to be present; sets a fresh
1619/// HMAC-protected authorized token cookie and expires any anonymous token.
1620/// - Synchronizer Token: sets a fresh random token in server-side session and expires
1621/// pre-session markers.
1622///
1623/// This function is safe to call on both safe and mutating handlers, but it is commonly
1624/// used after authentication to immediately upgrade from anonymous to authorized tokens.
1625///
1626/// # Errors
1627/// - Returns `BadRequest` when required inputs are missing (e.g., session id cookie for
1628/// Double-Submit Cookie).
1629/// - Returns `InternalServerError` if session updates fail (Synchronizer Token) or cookies
1630/// cannot be set.
1631pub fn rotate_csrf_token_in_response(
1632 session_id: &str,
1633 req: &HttpRequest,
1634 resp: &mut HttpResponseBuilder,
1635 config: &CsrfMiddlewareConfig,
1636) -> Result<(), Error> {
1637 // Always expire the pre-session cookie (best-effort) with strict flags
1638 let mut del = Cookie::new(CSRF_PRE_SESSION_KEY.to_string(), "");
1639 del.set_max_age(time::Duration::seconds(0));
1640 del.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1641 del.set_path("/");
1642 del.set_http_only(PRE_SESSION_HTTP_ONLY);
1643 del.set_secure(PRE_SESSION_SECURE);
1644 del.set_same_site(PRE_SESSION_SAME_SITE);
1645 resp.cookie(del);
1646
1647 match config.pattern {
1648 #[cfg(feature = "actix-session")]
1649 CsrfPattern::SynchronizerToken => {
1650 let session = req.get_session();
1651 let new_token = generate_random_token();
1652
1653 let _ = session.remove(&config.anon_session_key_name);
1654
1655 session
1656 .insert(&config.token_cookie_name, new_token)
1657 .map_err(|_| {
1658 actix_web::error::ErrorInternalServerError(
1659 "Failed to rotate CSRF token in session",
1660 )
1661 })?;
1662
1663 Ok(())
1664 }
1665 CsrfPattern::DoubleSubmitCookie => {
1666 let token = generate_hmac_token_ctx(
1667 TokenClass::Authorized,
1668 session_id,
1669 config.secret_key.as_slice(),
1670 );
1671
1672 let (http_only, secure, same_site) = match &config.token_cookie_config {
1673 Some(cfg) => (cfg.http_only, cfg.secure, cfg.same_site),
1674 None => (true, true, SameSite::Lax),
1675 };
1676
1677 let csrf_cookie = Cookie::build(&config.token_cookie_name, token)
1678 .http_only(http_only)
1679 .secure(secure)
1680 .same_site(same_site)
1681 .path("/")
1682 .finish();
1683
1684 // Also expire anonymous token cookie
1685 let mut del_anon = Cookie::new(config.anon_token_cookie_name.clone(), "");
1686 del_anon.set_max_age(time::Duration::seconds(0));
1687 del_anon.set_expires(time::OffsetDateTime::UNIX_EPOCH);
1688 del_anon.set_path("/");
1689
1690 resp.cookie(csrf_cookie);
1691 resp.cookie(del_anon);
1692
1693 Ok(())
1694 }
1695 }
1696}
1697
1698fn check_secret_key(secret_key: &[u8]) {
1699 if secret_key.len() < 32 {
1700 panic!("csrf secret_key too short: require >=32 bytes");
1701 }
1702}
1703
1704fn origin_allowed(headers: &HeaderMap, cfg: &CsrfMiddlewareConfig) -> bool {
1705 if !cfg.enforce_origin {
1706 return true;
1707 }
1708
1709 if cfg.allowed_origins.is_empty() {
1710 return false;
1711 }
1712
1713 // Helper to compare origins strictly (scheme, host, port)
1714 let is_allowed_origin = |u: &Url| -> bool {
1715 cfg.allowed_origins.iter().any(|allowed| {
1716 if let Ok(au) = Url::parse(allowed) {
1717 au.scheme() == u.scheme()
1718 && au.host_str() == u.host_str()
1719 && au.port_or_known_default() == u.port_or_known_default()
1720 } else {
1721 false
1722 }
1723 })
1724 };
1725
1726 // Try Origin header first (preferred)
1727 if let Some(origin) = headers.get(header::ORIGIN).and_then(|hv| hv.to_str().ok()) {
1728 if let Ok(u) = Url::parse(origin) {
1729 return is_allowed_origin(&u);
1730 }
1731
1732 return false;
1733 }
1734
1735 // Fallback: Referer header, use its origin
1736 if let Some(referer) = headers.get(header::REFERER).and_then(|hv| hv.to_str().ok()) {
1737 if let Ok(u) = Url::parse(referer) {
1738 let origin = format!(
1739 "{}://{}{}",
1740 u.scheme(),
1741 u.host_str().unwrap_or(""),
1742 u.port().map(|p| format!(":{p}")).unwrap_or_default()
1743 );
1744
1745 if let Ok(o) = Url::parse(&origin) {
1746 return is_allowed_origin(&o);
1747 }
1748 }
1749
1750 return false;
1751 }
1752
1753 false
1754}