| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | use axum::extract::FromRequestParts; |
| 14 | use axum::http::HeaderMap; |
| 15 | use axum::http::request::Parts; |
| 16 | use axum::response::{IntoResponse, Redirect, Response}; |
| 17 | use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; |
| 18 | use model::User; |
| 19 | |
| 20 | use crate::AppState; |
| 21 | use crate::error::AppError; |
| 22 | |
| 23 | |
| 24 | pub const CSRF_COOKIE: &str = "fabrica_csrf"; |
| 25 | |
| 26 | pub const THEME_COOKIE: &str = "fabrica_theme"; |
| 27 | |
| 28 | |
| 29 | pub struct MaybeUser(pub Option<User>); |
| 30 | |
| 31 | impl FromRequestParts<AppState> for MaybeUser { |
| 32 | type Rejection = std::convert::Infallible; |
| 33 | |
| 34 | async fn from_request_parts( |
| 35 | parts: &mut Parts, |
| 36 | state: &AppState, |
| 37 | ) -> Result<Self, Self::Rejection> { |
| 38 | let jar = CookieJar::from_headers(&parts.headers); |
| 39 | let Some(cookie) = jar.get(&state.config.auth.cookie_name) else { |
| 40 | return Ok(Self(None)); |
| 41 | }; |
| 42 | let session_id = auth::session_id(cookie.value()); |
| 43 | let user = state |
| 44 | .store |
| 45 | .user_for_session(&session_id) |
| 46 | .await |
| 47 | .ok() |
| 48 | .flatten(); |
| 49 | if user.is_some() { |
| 50 | |
| 51 | let _ = state.store.touch_session(&session_id).await; |
| 52 | } |
| 53 | Ok(Self(user)) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | |
| 58 | pub struct RequireUser(pub User); |
| 59 | |
| 60 | impl FromRequestParts<AppState> for RequireUser { |
| 61 | type Rejection = Response; |
| 62 | |
| 63 | async fn from_request_parts( |
| 64 | parts: &mut Parts, |
| 65 | state: &AppState, |
| 66 | ) -> Result<Self, Self::Rejection> { |
| 67 | let MaybeUser(user) = MaybeUser::from_request_parts(parts, state) |
| 68 | .await |
| 69 | .unwrap_or(MaybeUser(None)); |
| 70 | match user { |
| 71 | Some(user) => Ok(Self(user)), |
| 72 | None => Err(Redirect::to("/login").into_response()), |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | |
| 78 | |
| 79 | pub struct RequireAdmin(pub User); |
| 80 | |
| 81 | impl FromRequestParts<AppState> for RequireAdmin { |
| 82 | type Rejection = Response; |
| 83 | |
| 84 | async fn from_request_parts( |
| 85 | parts: &mut Parts, |
| 86 | state: &AppState, |
| 87 | ) -> Result<Self, Self::Rejection> { |
| 88 | let MaybeUser(user) = MaybeUser::from_request_parts(parts, state) |
| 89 | .await |
| 90 | .unwrap_or(MaybeUser(None)); |
| 91 | match user { |
| 92 | Some(user) if user.is_admin => Ok(Self(user)), |
| 93 | Some(_) => Err(crate::error::AppError::NotFound.into_response()), |
| 94 | None => Err(Redirect::to("/login").into_response()), |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | |
| 100 | |
| 101 | |
| 102 | pub fn ensure_csrf(jar: CookieJar, secure: bool) -> (CookieJar, String) { |
| 103 | if let Some(cookie) = jar.get(CSRF_COOKIE) { |
| 104 | return (jar.clone(), cookie.value().to_string()); |
| 105 | } |
| 106 | let token = auth::new_secret(); |
| 107 | let cookie = Cookie::build((CSRF_COOKIE, token.clone())) |
| 108 | .http_only(false) |
| 109 | .same_site(SameSite::Lax) |
| 110 | .secure(secure) |
| 111 | .path("/") |
| 112 | .build(); |
| 113 | (jar.add(cookie), token) |
| 114 | } |
| 115 | |
| 116 | |
| 117 | |
| 118 | |
| 119 | |
| 120 | |
| 121 | |
| 122 | pub fn verify_csrf( |
| 123 | jar: &CookieJar, |
| 124 | headers: &HeaderMap, |
| 125 | form_token: Option<&str>, |
| 126 | ) -> Result<(), AppError> { |
| 127 | let cookie = jar.get(CSRF_COOKIE).map(|c| c.value().to_string()); |
| 128 | let submitted = headers |
| 129 | .get("x-csrf-token") |
| 130 | .and_then(|v| v.to_str().ok()) |
| 131 | .map(str::to_string) |
| 132 | .or_else(|| form_token.map(str::to_string)); |
| 133 | match (cookie, submitted) { |
| 134 | (Some(a), Some(b)) if !a.is_empty() && a == b => Ok(()), |
| 135 | _ => Err(AppError::Forbidden), |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | |
| 140 | #[must_use] |
| 141 | pub fn session_cookie(name: &str, value: String, ttl_days: u32, secure: bool) -> Cookie<'static> { |
| 142 | Cookie::build((name.to_string(), value)) |
| 143 | .http_only(true) |
| 144 | .same_site(SameSite::Lax) |
| 145 | .secure(secure) |
| 146 | .path("/") |
| 147 | .max_age(time::Duration::days(i64::from(ttl_days))) |
| 148 | .build() |
| 149 | } |
| 150 | |
| 151 | |
| 152 | #[must_use] |
| 153 | pub fn clear_cookie(name: &str) -> Cookie<'static> { |
| 154 | Cookie::build((name.to_string(), String::new())) |
| 155 | .path("/") |
| 156 | .max_age(time::Duration::ZERO) |
| 157 | .build() |
| 158 | } |