auth.rs
⎇
Raw
1use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
2use argon2::Argon2;
3
4pub const COOKIE_NAME: &str = "fbng_session";
5/// 30 days.
6pub const SESSION_MAX_AGE: u64 = 60 * 60 * 24 * 30;
7
8pub 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
16pub 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).
24pub fn random_token() -> String {
25 use rand::RngCore;
26 let mut b = [0u8; 32];
27 rand::thread_rng().fill_bytes(&mut b);
28 let mut s = String::with_capacity(64);
29 for x in b {
30 s.push_str(&format!("{x:02x}"));
31 }
32 s
33}
34
35pub fn session_cookie(token: &str, https: bool) -> String {
36 let mut c = format!(
37 "{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}"
38 );
39 if https {
40 c.push_str("; Secure");
41 }
42 c
43}
44
45pub fn clear_session_cookie(https: bool) -> String {
46 let mut c = format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
47 if https {
48 c.push_str("; Secure");
49 }
50 c
51}
52
53/// Extract the session token from the Cookie header, if present.
54pub fn parse_session_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
55 let header = headers.get(axum::http::header::COOKIE)?.to_str().ok()?;
56 for part in header.split(';') {
57 let part = part.trim();
58 if let Some((k, v)) = part.split_once('=') {
59 if k == COOKIE_NAME && !v.is_empty() {
60 return Some(v.to_string());
61 }
62 }
63 }
64 None
65}
66