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