1use actix_web::{
12 HttpRequest,
13 dev::ServiceRequest,
14 http::{
15 Method, header,
16 header::{HeaderName, InvalidHeaderName},
17 },
18};
19use rand::RngExt;
20use std::sync::OnceLock;
21use subtle::ConstantTimeEq;
22
23pub const CSRF_COOKIE: &str = "aster_csrf";
27pub const CSRF_HEADER: &str = "X-CSRF-Token";
31const DEFAULT_CSRF_HEADER_LOWER: &str = "x-csrf-token";
32
33const MAX_REQUEST_SCHEME_LEN: usize = 16;
34const MAX_REQUEST_HOST_LEN: usize = 512;
35const MAX_REFERER_AUTHORITY_LEN: usize = MAX_REQUEST_HOST_LEN + 16;
36const MAX_SOURCE_HEADER_LEN: usize = 2048;
37const MAX_SEC_FETCH_SITE_LEN: usize = 64;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum RequestSourceMode {
42 OptionalWhenPresent,
44 Required,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum CsrfErrorKind {
51 TokenNameInvalid,
56 CookieMissing,
58 HeaderMissing,
60 TokenInvalid,
62 RequestSourceUntrusted,
64 RequestOriginUntrusted,
66 RequestRefererUntrusted,
68 RequestSourceMissing,
70 RequestSchemeInvalid,
72 RequestHostInvalid,
74 RequestOriginInvalid,
76 RequestRefererInvalid,
78 RequestHeaderValueInvalid,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
84#[error("{message}")]
85pub struct CsrfError {
86 kind: CsrfErrorKind,
87 message: String,
88}
89
90impl CsrfError {
91 fn new(kind: CsrfErrorKind, message: impl Into<String>) -> Self {
92 Self {
93 kind,
94 message: message.into(),
95 }
96 }
97
98 #[must_use]
100 pub fn kind(&self) -> CsrfErrorKind {
101 self.kind
102 }
103
104 #[must_use]
106 pub fn message(&self) -> &str {
107 &self.message
108 }
109}
110
111pub type Result<T> = std::result::Result<T, CsrfError>;
113
114#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct CsrfTokenNames {
123 cookie_name: String,
124 header_name: HeaderName,
125}
126
127impl CsrfTokenNames {
128 pub fn new(cookie_name: impl Into<String>, header_name: impl AsRef<str>) -> Result<Self> {
138 let cookie_name = cookie_name.into();
139 validate_cookie_name(&cookie_name)?;
140 let header_name = parse_header_name(header_name.as_ref())?;
141 Ok(Self {
142 cookie_name,
143 header_name,
144 })
145 }
146
147 pub fn cookie_name(&self) -> &str {
149 &self.cookie_name
150 }
151
152 pub fn header_name(&self) -> &HeaderName {
154 &self.header_name
155 }
156
157 pub fn header_name_str(&self) -> &str {
162 self.header_name.as_str()
163 }
164}
165
166impl Default for CsrfTokenNames {
167 fn default() -> Self {
168 Self {
169 cookie_name: CSRF_COOKIE.to_string(),
170 header_name: HeaderName::from_static(DEFAULT_CSRF_HEADER_LOWER),
171 }
172 }
173}
174
175pub fn default_csrf_token_names() -> &'static CsrfTokenNames {
180 static DEFAULT_NAMES: OnceLock<CsrfTokenNames> = OnceLock::new();
181 DEFAULT_NAMES.get_or_init(CsrfTokenNames::default)
182}
183
184#[must_use]
186pub fn is_unsafe_method(method: &Method) -> bool {
187 !matches!(
188 *method,
189 Method::GET | Method::HEAD | Method::OPTIONS | Method::TRACE
190 )
191}
192
193#[must_use]
195pub fn build_csrf_token() -> String {
196 use base64::Engine;
197
198 let mut bytes = [0_u8; 32];
199 rand::rng().fill(&mut bytes);
200 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
201}
202
203pub fn ensure_double_submit_token(req: &HttpRequest) -> Result<()> {
212 ensure_double_submit_token_with_names(req, default_csrf_token_names())
213}
214
215pub fn ensure_double_submit_token_with_names(
225 req: &HttpRequest,
226 names: &CsrfTokenNames,
227) -> Result<()> {
228 let cookie_token = req
229 .cookie(names.cookie_name())
230 .map(|cookie| cookie.value().to_string())
231 .ok_or_else(|| CsrfError::new(CsrfErrorKind::CookieMissing, "missing CSRF cookie"))?;
232 let header_token = req
233 .headers()
234 .get(names.header_name())
235 .and_then(|value| value.to_str().ok())
236 .map(str::trim)
237 .filter(|value| !value.is_empty())
238 .ok_or_else(|| {
239 CsrfError::new(
240 CsrfErrorKind::HeaderMissing,
241 format!("missing {} header", names.header_name_str()),
242 )
243 })?;
244
245 let tokens_match = header_token.len() == cookie_token.len()
249 && bool::from(header_token.as_bytes().ct_eq(cookie_token.as_bytes()));
250 if !tokens_match {
251 return Err(CsrfError::new(
252 CsrfErrorKind::TokenInvalid,
253 "invalid CSRF token",
254 ));
255 }
256
257 Ok(())
258}
259
260pub fn ensure_service_double_submit_token(req: &ServiceRequest) -> Result<()> {
269 ensure_double_submit_token(req.request())
270}
271
272pub fn ensure_service_double_submit_token_with_names(
279 req: &ServiceRequest,
280 names: &CsrfTokenNames,
281) -> Result<()> {
282 ensure_double_submit_token_with_names(req.request(), names)
283}
284
285pub fn ensure_request_source_allowed(
291 req: &HttpRequest,
292 public_site_origins: &[String],
293 mode: RequestSourceMode,
294) -> Result<()> {
295 let conn = req.connection_info();
296 let request_origin = request_origin(conn.scheme(), conn.host())?;
297 ensure_headers_allowed(
298 header_value(req, header::ORIGIN),
299 header_value(req, header::REFERER),
300 header_value(req, header::HeaderName::from_static("sec-fetch-site")),
301 &request_origin,
302 public_site_origins,
303 mode,
304 )
305}
306
307pub fn ensure_service_request_source_allowed(
313 req: &ServiceRequest,
314 public_site_origins: &[String],
315 mode: RequestSourceMode,
316) -> Result<()> {
317 let conn = req.connection_info();
318 let request_origin = request_origin(conn.scheme(), conn.host())?;
319 ensure_headers_allowed(
320 header_value(req.request(), header::ORIGIN),
321 header_value(req.request(), header::REFERER),
322 header_value(
323 req.request(),
324 header::HeaderName::from_static("sec-fetch-site"),
325 ),
326 &request_origin,
327 public_site_origins,
328 mode,
329 )
330}
331
332pub fn ensure_headers_allowed(
338 origin: Option<&str>,
339 referer: Option<&str>,
340 sec_fetch_site: Option<&str>,
341 request_origin: &str,
342 public_site_origins: &[String],
343 mode: RequestSourceMode,
344) -> Result<()> {
345 let fetch_site = source_header_value(
346 sec_fetch_site,
347 MAX_SEC_FETCH_SITE_LEN,
348 "Sec-Fetch-Site",
349 CsrfErrorKind::RequestHeaderValueInvalid,
350 )?
351 .map(str::to_ascii_lowercase);
352
353 if let Some("cross-site" | "none") = fetch_site.as_deref() {
354 return Err(CsrfError::new(
355 CsrfErrorKind::RequestSourceUntrusted,
356 "untrusted request source for cookie-authenticated action",
357 ));
358 }
359 let same_site_fetch = fetch_site.as_deref() == Some("same-site");
360
361 if let Some(origin) = source_header_value(
362 origin,
363 MAX_SOURCE_HEADER_LEN,
364 "Origin",
365 CsrfErrorKind::RequestOriginInvalid,
366 )?
367 .map(|value| normalize_origin(value, CsrfErrorKind::RequestOriginInvalid))
368 .transpose()?
369 {
370 if origin_is_trusted(&origin, request_origin, public_site_origins) {
371 return Ok(());
372 }
373 return Err(CsrfError::new(
374 CsrfErrorKind::RequestOriginUntrusted,
375 "untrusted request origin for cookie-authenticated action",
376 ));
377 }
378
379 if let Some(referer) = trimmed_header_value(referer) {
380 let referer_origin = origin_from_url(referer)?;
381 if origin_is_trusted(&referer_origin, request_origin, public_site_origins) {
382 return Ok(());
383 }
384 return Err(CsrfError::new(
385 CsrfErrorKind::RequestRefererUntrusted,
386 "untrusted request referer for cookie-authenticated action",
387 ));
388 }
389
390 if same_site_fetch {
391 return Err(CsrfError::new(
392 CsrfErrorKind::RequestSourceUntrusted,
393 "missing trusted request source for same-site cookie-authenticated action",
394 ));
395 }
396
397 match mode {
398 RequestSourceMode::OptionalWhenPresent => Ok(()),
399 RequestSourceMode::Required => Err(CsrfError::new(
400 CsrfErrorKind::RequestSourceMissing,
401 "missing request source for cookie-authenticated action",
402 )),
403 }
404}
405
406fn header_value(req: &HttpRequest, name: header::HeaderName) -> Option<&str> {
407 req.headers()
408 .get(name)
409 .and_then(|value| value.to_str().ok())
410}
411
412fn validate_cookie_name(cookie_name: &str) -> Result<()> {
413 if cookie_name.is_empty() {
414 return Err(CsrfError::new(
415 CsrfErrorKind::TokenNameInvalid,
416 "CSRF cookie name cannot be empty",
417 ));
418 }
419 if cookie_name
420 .bytes()
421 .any(|byte| byte <= 0x20 || byte >= 0x7f || b"()<>@,;:\\\"/[]?={}".contains(&byte))
422 {
423 return Err(CsrfError::new(
424 CsrfErrorKind::TokenNameInvalid,
425 "CSRF cookie name contains invalid characters",
426 ));
427 }
428 Ok(())
429}
430
431fn parse_header_name(header_name: &str) -> Result<HeaderName> {
432 HeaderName::from_bytes(header_name.as_bytes()).map_err(|error| header_name_error(&error))
433}
434
435fn header_name_error(error: &InvalidHeaderName) -> CsrfError {
436 CsrfError::new(
437 CsrfErrorKind::TokenNameInvalid,
438 format!("invalid CSRF header name: {error}"),
439 )
440}
441
442fn request_origin(scheme: &str, host: &str) -> Result<String> {
443 ensure_value_len(
444 scheme,
445 MAX_REQUEST_SCHEME_LEN,
446 "request scheme",
447 CsrfErrorKind::RequestSchemeInvalid,
448 )?;
449 ensure_value_len(
450 host,
451 MAX_REQUEST_HOST_LEN,
452 "request host",
453 CsrfErrorKind::RequestHostInvalid,
454 )?;
455 normalize_origin(
456 &format!("{scheme}://{host}"),
457 CsrfErrorKind::RequestHostInvalid,
458 )
459 .map_err(|_| CsrfError::new(CsrfErrorKind::RequestHostInvalid, "invalid request host"))
460}
461
462fn normalize_origin(origin: &str, kind: CsrfErrorKind) -> Result<String> {
463 aster_forge_utils::url::normalize_origin(origin, false)
464 .map_err(|_| CsrfError::new(kind, "invalid origin"))
465}
466
467fn origin_is_trusted(origin: &str, request_origin: &str, public_site_origins: &[String]) -> bool {
468 origin == request_origin || public_site_origins.iter().any(|allowed| allowed == origin)
469}
470
471fn source_header_value<'a>(
472 value: Option<&'a str>,
473 max_len: usize,
474 label: &str,
475 kind: CsrfErrorKind,
476) -> Result<Option<&'a str>> {
477 let Some(value) = trimmed_header_value(value) else {
478 return Ok(None);
479 };
480 ensure_value_len(value, max_len, label, kind)?;
481 Ok(Some(value))
482}
483
484fn trimmed_header_value(value: Option<&str>) -> Option<&str> {
485 value.map(str::trim).filter(|value| !value.is_empty())
486}
487
488fn ensure_value_len(value: &str, max_len: usize, label: &str, kind: CsrfErrorKind) -> Result<()> {
489 if value.len() > max_len {
490 return Err(CsrfError::new(
491 kind,
492 format!("{label} exceeds {max_len} bytes"),
493 ));
494 }
495 Ok(())
496}
497
498fn origin_from_url(url: &str) -> Result<String> {
499 let scheme_end = url.find("://").ok_or_else(|| {
500 CsrfError::new(
501 CsrfErrorKind::RequestSchemeInvalid,
502 "invalid Referer header",
503 )
504 })?;
505 let scheme = &url[..scheme_end];
506 ensure_value_len(
507 scheme,
508 MAX_REQUEST_SCHEME_LEN,
509 "Referer scheme",
510 CsrfErrorKind::RequestSchemeInvalid,
511 )?;
512
513 let authority_start = scheme_end + 3;
514 let authority_tail = &url[authority_start..];
515 let authority_end = authority_tail
516 .char_indices()
517 .find_map(|(idx, ch)| matches!(ch, '/' | '?' | '#').then_some(authority_start + idx))
518 .unwrap_or(url.len());
519 let authority = &url[authority_start..authority_end];
520 ensure_value_len(
521 authority,
522 MAX_REFERER_AUTHORITY_LEN,
523 "Referer authority",
524 CsrfErrorKind::RequestRefererInvalid,
525 )?;
526
527 normalize_origin(
528 &format!("{}://{}", scheme.to_ascii_lowercase(), authority),
529 CsrfErrorKind::RequestRefererInvalid,
530 )
531 .map_err(|_| {
532 CsrfError::new(
533 CsrfErrorKind::RequestRefererInvalid,
534 "invalid Referer header",
535 )
536 })
537}
538
539#[cfg(test)]
540mod tests {
541 use actix_web::cookie::Cookie;
542
543 use super::{
544 CSRF_COOKIE, CSRF_HEADER, CsrfErrorKind, CsrfTokenNames, RequestSourceMode,
545 build_csrf_token, ensure_double_submit_token, ensure_double_submit_token_with_names,
546 ensure_headers_allowed, ensure_request_source_allowed,
547 };
548
549 fn host_with_len(len: usize) -> String {
550 let suffix = ".example.com";
551 format!("{}{}", "a".repeat(len - suffix.len()), suffix)
552 }
553
554 #[test]
555 fn accepts_same_origin_and_public_site_origin() {
556 assert!(
557 ensure_headers_allowed(
558 Some("http://localhost"),
559 None,
560 Some("same-origin"),
561 "http://localhost",
562 &["https://forge.example.com".to_string()],
563 RequestSourceMode::Required,
564 )
565 .is_ok()
566 );
567
568 assert!(
569 ensure_headers_allowed(
570 Some("https://forge.example.com"),
571 None,
572 Some("same-origin"),
573 "http://127.0.0.1:3000",
574 &["https://forge.example.com".to_string()],
575 RequestSourceMode::Required,
576 )
577 .is_ok()
578 );
579 }
580
581 #[test]
582 fn same_site_fetch_metadata_requires_trusted_origin_or_referer() {
583 assert!(
584 ensure_headers_allowed(
585 Some("https://panel.example.com"),
586 None,
587 Some("same-site"),
588 "https://api.example.com",
589 &[
590 "https://api.example.com".to_string(),
591 "https://panel.example.com".to_string(),
592 ],
593 RequestSourceMode::OptionalWhenPresent,
594 )
595 .is_ok()
596 );
597
598 assert!(
599 ensure_headers_allowed(
600 None,
601 Some("https://panel.example.com/settings"),
602 Some("same-site"),
603 "https://api.example.com",
604 &[
605 "https://api.example.com".to_string(),
606 "https://panel.example.com".to_string(),
607 ],
608 RequestSourceMode::OptionalWhenPresent,
609 )
610 .is_ok()
611 );
612
613 let err = ensure_headers_allowed(
614 None,
615 None,
616 Some("same-site"),
617 "https://api.example.com",
618 &["https://api.example.com".to_string()],
619 RequestSourceMode::OptionalWhenPresent,
620 )
621 .unwrap_err();
622 assert_eq!(err.kind(), CsrfErrorKind::RequestSourceUntrusted);
623 assert!(err.message().contains("missing trusted request source"));
624 }
625
626 #[test]
627 fn rejects_untrusted_fetch_metadata_values() {
628 for fetch_site in ["cross-site", "none"] {
629 let err = ensure_headers_allowed(
630 None,
631 None,
632 Some(fetch_site),
633 "https://forge.example.com",
634 &[],
635 RequestSourceMode::OptionalWhenPresent,
636 )
637 .unwrap_err();
638 assert_eq!(err.kind(), CsrfErrorKind::RequestSourceUntrusted);
639 assert!(err.message().contains("untrusted request source"));
640 }
641 }
642
643 #[test]
644 fn rejects_untrusted_origin_and_missing_required_source() {
645 let err = ensure_headers_allowed(
646 Some("https://evil.example.com"),
647 None,
648 None,
649 "https://forge.example.com",
650 &[],
651 RequestSourceMode::OptionalWhenPresent,
652 )
653 .unwrap_err();
654 assert_eq!(err.kind(), CsrfErrorKind::RequestOriginUntrusted);
655
656 let err = ensure_headers_allowed(
657 None,
658 None,
659 None,
660 "https://forge.example.com",
661 &[],
662 RequestSourceMode::Required,
663 )
664 .unwrap_err();
665 assert_eq!(err.kind(), CsrfErrorKind::RequestSourceMissing);
666 }
667
668 #[test]
669 fn rejects_oversized_request_source_values_before_normalization() {
670 let max_host = host_with_len(512);
671 let req = actix_web::test::TestRequest::post()
672 .insert_header(("Host", max_host.as_str()))
673 .insert_header(("Origin", format!("http://{max_host}")))
674 .to_http_request();
675 assert!(ensure_request_source_allowed(&req, &[], RequestSourceMode::Required).is_ok());
676
677 let long_host = host_with_len(513);
678 let req = actix_web::test::TestRequest::post()
679 .insert_header(("Host", long_host))
680 .insert_header(("Origin", "https://forge.example.com"))
681 .to_http_request();
682 let err =
683 ensure_request_source_allowed(&req, &[], RequestSourceMode::Required).unwrap_err();
684 assert_eq!(err.kind(), CsrfErrorKind::RequestHostInvalid);
685
686 let req = actix_web::test::TestRequest::post()
687 .insert_header(("Host", "forge.example.com"))
688 .insert_header(("X-Forwarded-Proto", "x".repeat(17)))
689 .insert_header(("Origin", "https://forge.example.com"))
690 .to_http_request();
691 let err =
692 ensure_request_source_allowed(&req, &[], RequestSourceMode::Required).unwrap_err();
693 assert_eq!(err.kind(), CsrfErrorKind::RequestSchemeInvalid);
694
695 let max_origin = format!("https://{}", host_with_len(2040));
696 assert_eq!(max_origin.len(), 2048);
697 assert!(
698 ensure_headers_allowed(
699 Some(&max_origin),
700 None,
701 None,
702 "https://forge.example.com",
703 std::slice::from_ref(&max_origin),
704 RequestSourceMode::OptionalWhenPresent,
705 )
706 .is_ok()
707 );
708
709 let long_origin = format!("https://{}", host_with_len(2041));
710 assert_eq!(long_origin.len(), 2049);
711 let err = ensure_headers_allowed(
712 Some(&long_origin),
713 None,
714 None,
715 "https://forge.example.com",
716 &[],
717 RequestSourceMode::OptionalWhenPresent,
718 )
719 .unwrap_err();
720 assert_eq!(err.kind(), CsrfErrorKind::RequestOriginInvalid);
721
722 let max_referer_authority = host_with_len(528);
723 let max_referer_origin = format!("https://{max_referer_authority}");
724 let max_referer = format!("{max_referer_origin}/settings");
725 assert!(
726 ensure_headers_allowed(
727 None,
728 Some(&max_referer),
729 None,
730 "https://forge.example.com",
731 &[max_referer_origin],
732 RequestSourceMode::OptionalWhenPresent,
733 )
734 .is_ok()
735 );
736
737 let long_referer_authority = format!("https://{}.example.com/settings", "a".repeat(600));
738 let err = ensure_headers_allowed(
739 None,
740 Some(&long_referer_authority),
741 None,
742 "https://forge.example.com",
743 &[],
744 RequestSourceMode::OptionalWhenPresent,
745 )
746 .unwrap_err();
747 assert_eq!(err.kind(), CsrfErrorKind::RequestRefererInvalid);
748
749 let max_fetch_site = "x".repeat(64);
750 assert!(
751 ensure_headers_allowed(
752 None,
753 None,
754 Some(&max_fetch_site),
755 "https://forge.example.com",
756 &[],
757 RequestSourceMode::OptionalWhenPresent,
758 )
759 .is_ok()
760 );
761
762 let long_fetch_site = "x".repeat(65);
763 let err = ensure_headers_allowed(
764 None,
765 None,
766 Some(&long_fetch_site),
767 "https://forge.example.com",
768 &[],
769 RequestSourceMode::OptionalWhenPresent,
770 )
771 .unwrap_err();
772 assert_eq!(err.kind(), CsrfErrorKind::RequestHeaderValueInvalid);
773 }
774
775 #[test]
776 fn accepts_ipv6_request_host_origin_match() {
777 let req = actix_web::test::TestRequest::post()
778 .insert_header(("Host", "[2001:db8::1]:8443"))
779 .insert_header(("Origin", "http://[2001:db8::1]:8443"))
780 .to_http_request();
781
782 assert!(ensure_request_source_allowed(&req, &[], RequestSourceMode::Required).is_ok());
783 }
784
785 #[test]
786 fn referer_source_check_ignores_long_path_after_bounded_origin() {
787 let long_referer = format!("https://forge.example.com/settings/{}", "a".repeat(10_000));
788
789 assert!(
790 ensure_headers_allowed(
791 None,
792 Some(&long_referer),
793 Some("same-origin"),
794 "https://forge.example.com",
795 &[],
796 RequestSourceMode::Required,
797 )
798 .is_ok()
799 );
800 }
801
802 #[test]
803 fn invalid_referer_missing_scheme_reports_invalid_scheme() {
804 let err = ensure_headers_allowed(
805 None,
806 Some("forge.example.com/settings"),
807 None,
808 "https://forge.example.com",
809 &[],
810 RequestSourceMode::OptionalWhenPresent,
811 )
812 .unwrap_err();
813
814 assert_eq!(err.kind(), CsrfErrorKind::RequestSchemeInvalid);
815 }
816
817 #[test]
818 fn accepts_missing_optional_source() {
819 assert!(
820 ensure_headers_allowed(
821 None,
822 None,
823 None,
824 "https://forge.example.com",
825 &[],
826 RequestSourceMode::OptionalWhenPresent,
827 )
828 .is_ok()
829 );
830 }
831
832 #[test]
833 fn build_csrf_token_returns_url_safe_random_value() {
834 let token_a = build_csrf_token();
835 let token_b = build_csrf_token();
836
837 assert_ne!(token_a, token_b);
838 assert!(token_a.len() >= 32);
839 assert!(
840 token_a
841 .chars()
842 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
843 );
844 }
845
846 #[test]
847 fn csrf_token_check_requires_cookie_for_cookie_authenticated_writes() {
848 let req = actix_web::test::TestRequest::post()
849 .uri("/api/v1/auth/profile")
850 .to_http_request();
851
852 let err = ensure_double_submit_token(&req).unwrap_err();
853 assert_eq!(err.kind(), CsrfErrorKind::CookieMissing);
854 }
855
856 #[test]
857 fn csrf_token_check_requires_matching_cookie_and_header() {
858 let req = actix_web::test::TestRequest::patch()
859 .uri("/api/v1/auth/profile")
860 .insert_header(("Origin", "http://localhost"))
861 .cookie(Cookie::new(CSRF_COOKIE, "token-a"))
862 .insert_header((CSRF_HEADER, "token-a"))
863 .to_http_request();
864 assert!(ensure_double_submit_token(&req).is_ok());
865
866 let missing_header = actix_web::test::TestRequest::patch()
867 .uri("/api/v1/auth/profile")
868 .insert_header(("Origin", "http://localhost"))
869 .cookie(Cookie::new(CSRF_COOKIE, "token-a"))
870 .to_http_request();
871 let err = ensure_double_submit_token(&missing_header).unwrap_err();
872 assert_eq!(err.kind(), CsrfErrorKind::HeaderMissing);
873
874 let mismatch = actix_web::test::TestRequest::patch()
875 .uri("/api/v1/auth/profile")
876 .insert_header(("Origin", "http://localhost"))
877 .cookie(Cookie::new(CSRF_COOKIE, "token-a"))
878 .insert_header((CSRF_HEADER, "token-b"))
879 .to_http_request();
880 let err = ensure_double_submit_token(&mismatch).unwrap_err();
881 assert_eq!(err.kind(), CsrfErrorKind::TokenInvalid);
882 }
883
884 #[test]
885 fn csrf_token_check_rejects_tokens_of_different_lengths() {
886 let req = actix_web::test::TestRequest::patch()
887 .uri("/api/v1/auth/profile")
888 .cookie(Cookie::new(CSRF_COOKIE, "token-a"))
889 .insert_header((CSRF_HEADER, "token-a-with-a-longer-value"))
890 .to_http_request();
891 let err = ensure_double_submit_token(&req).unwrap_err();
892 assert_eq!(err.kind(), CsrfErrorKind::TokenInvalid);
893 }
894
895 #[test]
896 fn csrf_token_check_accepts_custom_cookie_and_header_names() {
897 let names = CsrfTokenNames::new("aster_yggdrasil_csrf", "X-Yggdrasil-CSRF-Token")
898 .expect("custom CSRF token names should be valid");
899 assert_eq!(names.cookie_name(), "aster_yggdrasil_csrf");
900 assert_eq!(names.header_name_str(), "x-yggdrasil-csrf-token");
901
902 let req = actix_web::test::TestRequest::patch()
903 .cookie(Cookie::new("aster_yggdrasil_csrf", "token-a"))
904 .insert_header(("X-Yggdrasil-CSRF-Token", "token-a"))
905 .to_http_request();
906 assert!(ensure_double_submit_token_with_names(&req, &names).is_ok());
907
908 let default_req = actix_web::test::TestRequest::patch()
909 .cookie(Cookie::new(CSRF_COOKIE, "token-a"))
910 .insert_header((CSRF_HEADER, "token-a"))
911 .to_http_request();
912 let err = ensure_double_submit_token_with_names(&default_req, &names).unwrap_err();
913 assert_eq!(err.kind(), CsrfErrorKind::CookieMissing);
914 }
915
916 #[test]
917 fn csrf_token_names_reject_invalid_cookie_and_header_names() {
918 let err = CsrfTokenNames::new("", "X-CSRF-Token").unwrap_err();
919 assert_eq!(err.kind(), CsrfErrorKind::TokenNameInvalid);
920
921 let err = CsrfTokenNames::new("aster csrf", "X-CSRF-Token").unwrap_err();
922 assert_eq!(err.kind(), CsrfErrorKind::TokenNameInvalid);
923
924 let err = CsrfTokenNames::new("aster_csrf", "bad header").unwrap_err();
925 assert_eq!(err.kind(), CsrfErrorKind::TokenNameInvalid);
926 }
927}