Skip to main content

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