auth.rs
| 1 | use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; |
| 2 | use argon2::Argon2; |
| 3 | |
| 4 | pub const COOKIE_NAME: &str = "fbng_session"; |
| 5 | /// 30 days. |
| 6 | pub const SESSION_MAX_AGE: u64 = 60 * 60 * 24 * 30; |
| 7 | |
| 8 | pub fn hash_password(password: &str) -> anyhow::Result<String> { |
| 9 | let salt = SaltString::generate(&mut rand::thread_rng()); |
| 10 | let hash = Argon2::default() |
| 11 | .hash_password(password.as_bytes(), &salt) |
| 12 | .map_err(|e| anyhow::anyhow!("password hashing failed: {e}"))?; |
| 13 | Ok(hash.to_string()) |
| 14 | } |
| 15 | |
| 16 | pub fn verify_password(password: &str, hash: &str) -> bool { |
| 17 | let Ok(parsed) = PasswordHash::new(hash) else { |
| 18 | return false; |
| 19 | }; |
| 20 | Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok() |
| 21 | } |
| 22 | |
| 23 | /// 32 random bytes, hex-encoded (64 chars). |
| 24 | pub fn random_token() -> String { |
| 25 | hex_token(32) |
| 26 | } |
| 27 | |
| 28 | /// 16 random bytes, hex-encoded (32 chars). Used for public share links. |
| 29 | pub fn share_token() -> String { |
| 30 | hex_token(16) |
| 31 | } |
| 32 | |
| 33 | fn hex_token(bytes: usize) -> String { |
| 34 | use rand::RngCore; |
| 35 | let mut b = [0u8; 64]; |
| 36 | rand::thread_rng().fill_bytes(&mut b[..bytes.min(64)]); |
| 37 | let mut s = String::with_capacity(bytes * 2); |
| 38 | for x in b[..bytes.min(64)].iter() { |
| 39 | s.push_str(&format!("{x:02x}")); |
| 40 | } |
| 41 | s |
| 42 | } |
| 43 | |
| 44 | pub fn session_cookie(token: &str, https: bool) -> String { |
| 45 | let mut c = format!( |
| 46 | "{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}" |
| 47 | ); |
| 48 | if https { |
| 49 | c.push_str("; Secure"); |
| 50 | } |
| 51 | c |
| 52 | } |
| 53 | |
| 54 | pub fn clear_session_cookie(https: bool) -> String { |
| 55 | let mut c = format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"); |
| 56 | if https { |
| 57 | c.push_str("; Secure"); |
| 58 | } |
| 59 | c |
| 60 | } |
| 61 | |
| 62 | /// Extract the session token from the Cookie header, if present. |
| 63 | pub fn parse_session_cookie(headers: &axum::http::HeaderMap) -> Option<String> { |
| 64 | let header = headers.get(axum::http::header::COOKIE)?.to_str().ok()?; |
| 65 | for part in header.split(';') { |
| 66 | let part = part.trim(); |
| 67 | if let Some((k, v)) = part.split_once('=') { |
| 68 | if k == COOKIE_NAME && !v.is_empty() { |
| 69 | return Some(v.to_string()); |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | None |
| 74 | } |
| 75 |