actix_csrf_middleware/
lib.rs

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