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 hex_token(32)
26}
27
28/// 16 random bytes, hex-encoded (32 chars). Used for public share links.
29pub fn share_token() -> String {
30 hex_token(16)
31}
32
33fn 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
44pub 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
54pub 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.
63pub 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