fedimint_ui_common/
auth.rs1use axum::extract::{FromRequestParts, Request};
2use axum::http::request::Parts;
3use axum::http::{Method, StatusCode, header};
4use axum::middleware::Next;
5use axum::response::{Redirect, Response};
6use axum_extra::extract::CookieJar;
7use fedimint_core::net::auth::GuardianAuthToken;
8
9use crate::{LOGIN_ROUTE, UiState};
10
11pub struct UserAuth {
13 pub guardian_auth_token: GuardianAuthToken,
18}
19
20impl UserAuth {
21 fn authenticated() -> Self {
22 Self {
23 guardian_auth_token: GuardianAuthToken::new_unchecked(),
24 }
25 }
26}
27
28pub async fn csrf_protection_middleware(
45 request: Request,
46 next: Next,
47) -> Result<Response, StatusCode> {
48 if is_request_origin_allowed(&request) {
49 Ok(next.run(request).await)
50 } else {
51 Err(StatusCode::FORBIDDEN)
52 }
53}
54
55fn is_request_origin_allowed(request: &Request) -> bool {
56 let method = request.method();
57 if method == Method::GET || method == Method::HEAD || method == Method::OPTIONS {
58 return true;
59 }
60
61 let headers = request.headers();
62
63 if let Some(site) = headers.get("sec-fetch-site") {
64 return site
65 .to_str()
66 .is_ok_and(|site| site == "same-origin" || site == "none");
67 }
68
69 if let Some(origin) = headers.get(header::ORIGIN) {
70 let Some(origin_authority) = origin.to_str().ok().and_then(|origin| {
73 origin
74 .strip_prefix("http://")
75 .or_else(|| origin.strip_prefix("https://"))
76 }) else {
77 return false;
78 };
79
80 let Some(host) = headers
81 .get(header::HOST)
82 .and_then(|host| host.to_str().ok())
83 .or_else(|| {
84 request
85 .uri()
86 .authority()
87 .map(|authority| authority.as_str())
88 })
89 else {
90 return false;
91 };
92
93 return origin_authority.eq_ignore_ascii_case(host);
94 }
95
96 true
97}
98
99impl<Api> FromRequestParts<UiState<Api>> for UserAuth
100where
101 Api: Send + Sync + 'static,
102{
103 type Rejection = Redirect;
104
105 async fn from_request_parts(
106 parts: &mut Parts,
107 state: &UiState<Api>,
108 ) -> Result<Self, Self::Rejection> {
109 if !state.requires_auth {
110 return Ok(UserAuth::authenticated());
111 }
112
113 let jar = CookieJar::from_request_parts(parts, state)
114 .await
115 .map_err(|_| Redirect::to(LOGIN_ROUTE))?;
116
117 match jar.get(&state.auth_cookie_name) {
119 Some(cookie) if cookie.value() == state.auth_cookie_value => {
120 Ok(UserAuth::authenticated())
121 }
122 _ => Err(Redirect::to(LOGIN_ROUTE)),
123 }
124 }
125}