use std::env; use argon2::{password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, encode, decode, Validation, Algorithm}; use rand::prelude::*; use rand_chacha::ChaCha20Rng; use serde::{Deserialize, Serialize}; use siren::ServiceError; #[derive(Debug, Serialize, Deserialize)] struct TokenClaims { sub: String, // Subject (User) id: String, // Access Token ID iss: String, // Issuer (Service) exp: i64, // Expiration time iat: i64, // Issued At nbf: i64 // Not Before } #[derive(Debug, Serialize, Deserialize)] pub struct AccessToken { pub token: Option, // Access Token pub id: String, // Access Token ID pub email: String, // Subject (User) pub expiration: Option // Expiration time } impl AccessToken { fn new(email: &str) -> Result { let ttl = env::var("ACCESS_TOKEN_MAXAGE") .expect("ACCESS_TOKEN_MAXAGE must be set") .parse::() .expect("ACCESS_TOKEN_MAXAGE must be an integer"); let keys_dir = env::var("KEYS_DIR_PATH")?; let private_key = std::fs::read_to_string(format!("{}/private_key.pem", keys_dir))?; let now = chrono::Utc::now(); let mut token_details = Self { token: None, id: csprng_128bit(), email: email.to_string(), expiration: Some((now + chrono::Duration::minutes(ttl)).timestamp()) }; let claims = TokenClaims { sub: token_details.email.clone(), iss: "siren".to_string(), id: token_details.id.to_string(), exp: token_details.expiration.unwrap(), iat: now.timestamp(), nbf: now.timestamp() }; let header = Header::new(Algorithm::RS256); let key = EncodingKey::from_rsa_pem(private_key.as_bytes())?; let token = encode(&header, &claims, &key)?; token_details.token = Some(token); Ok(token_details) } pub fn decode(token: &str, public_key: &str) -> Result { let key = DecodingKey::from_rsa_pem(public_key.as_bytes())?; let validation = Validation::new(Algorithm::RS256); let decoded = decode::(token, &key, &validation)?; let email = decoded.claims.sub; let id = decoded.claims.id; Ok(Self { token: None, id, email, expiration: None }) } } #[derive(Debug, Serialize, Deserialize)] pub struct RefreshToken { pub id: String, pub email: String, pub ip_address: String, pub tokens: Vec, pub expiration: i64, } impl RefreshToken { pub fn new(email: &str, ip_address: &str) -> Self { let ttl = env::var("REFRESH_TOKEN_MAXAGE") .expect("REFRESH_TOKEN_MAXAGE must be set") .parse::() .expect("REFRESH_TOKEN_MAXAGE must be an integer"); let now = chrono::Utc::now(); Self { id: csprng_128bit(), email: email.to_string(), ip_address: hash(&ip_address).unwrap(), tokens: vec![], expiration: (now + chrono::Duration::minutes(ttl)).timestamp() } } pub fn create_access_token(&mut self) -> Result { let access_token = AccessToken::new(&self.email)?; self.tokens.push(access_token.id.clone()); Ok(access_token) } } fn csprng_128bit() -> String { // Generate a CSPRNG 128-bit (16 byte) ID using alphanumeric characters (a-z, A-Z, 0-9) let rng = ChaCha20Rng::from_entropy(); rng.sample_iter(rand::distributions::Alphanumeric).take(16).map(char::from).collect() } pub fn hash(str: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let bytes = str.as_bytes(); let hash = Argon2::default().hash_password(bytes, &salt)?.to_string(); Ok(hash) } pub fn verify_hash(str: &str, hash: &str) -> bool { let bytes = str.as_bytes(); let parsed_hash = match PasswordHash::new(hash) { Ok(h) => h, Err(_) => return false }; match Argon2::default().verify_password(bytes, &parsed_hash) { Ok(_) => true, Err(_) => false } }