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}