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}