use base64::Engine; use rand::Rng; use serde::{Deserialize, Serialize}; use uuid::Uuid; fn now_ts() -> i64 { chrono_like_now() } fn chrono_like_now() -> i64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccessClaims { pub sub: Uuid, pub uname: String, pub is_admin: bool, pub is_restricted: bool, pub iat: i64, pub exp: i64, pub iss: String, pub aud: String, pub jti: Uuid, pub scope: String, pub kid: String, /// Snapshot of `users.passwd_version` at issuance. A password reset /// (or any future bump) increments the DB counter; refresh- and /// access-token validation MUST reject tokens whose pwd_version lags /// behind the live value. pub pwd_version: i64, } impl AccessClaims { #[allow(clippy::too_many_arguments)] pub fn new( sub: Uuid, uname: String, is_admin: bool, is_restricted: bool, pwd_version: i64, ttl: i64, issuer: &str, audience: &str, kid: &str, ) -> Self { let iat = now_ts(); Self { sub, uname, is_admin, is_restricted, iat, exp: iat + ttl, iss: issuer.to_string(), aud: audience.to_string(), jti: Uuid::new_v4(), scope: String::new(), kid: kid.to_string(), pwd_version, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshClaims { pub sub: Uuid, pub family_id: Uuid, pub jti: Uuid, pub iat: i64, pub exp: i64, pub iss: String, pub aud: String, pub kid: String, pub parent_id: Option, /// Snapshot of `users.passwd_version` at issuance. See AccessClaims. pub pwd_version: i64, } impl RefreshClaims { #[allow(clippy::too_many_arguments)] pub fn new( sub: Uuid, family_id: Uuid, parent_id: Option, pwd_version: i64, ttl: i64, issuer: &str, audience: &str, kid: &str, ) -> Self { let iat = now_ts(); Self { sub, family_id, jti: Uuid::new_v4(), iat, exp: iat + ttl, iss: issuer.to_string(), aud: audience.to_string(), kid: kid.to_string(), parent_id, pwd_version, } } } pub trait HasUserExt { fn user_id(&self) -> Uuid; fn jti(&self) -> Uuid; } pub trait HasJti { fn jti(&self) -> Uuid; } impl HasUserExt for AccessClaims { fn user_id(&self) -> Uuid { self.sub } fn jti(&self) -> Uuid { self.jti } } impl HasUserExt for RefreshClaims { fn user_id(&self) -> Uuid { self.sub } fn jti(&self) -> Uuid { self.jti } } impl HasJti for AccessClaims { fn jti(&self) -> Uuid { self.jti } } impl HasJti for RefreshClaims { fn jti(&self) -> Uuid { self.jti } } pub fn random_bytes(n: usize) -> Vec { let mut buf = vec![0u8; n]; rand::rng().fill_bytes(&mut buf); buf } pub fn sha256_hex(data: &[u8]) -> String { use sha2::{Digest, Sha256}; let mut h = Sha256::new(); h.update(data); base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(h.finalize()) } pub fn b64_encode(data: &[u8]) -> String { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data) } pub fn b64_decode(s: &str) -> Result, base64::DecodeError> { base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(s) }