116 lines
3.4 KiB
Rust
116 lines
3.4 KiB
Rust
use argon2::{
|
|
Argon2,
|
|
password_hash::{
|
|
PasswordHash, PasswordHasher as _, PasswordVerifier, SaltString, rand_core::OsRng,
|
|
},
|
|
};
|
|
use async_trait::async_trait;
|
|
use chrono::{Duration, Utc};
|
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
|
use libertas_core::error::{CoreError, CoreResult};
|
|
use once_cell::sync::Lazy;
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
#[async_trait]
|
|
pub trait PasswordHasher: Send + Sync {
|
|
async fn hash_password(&self, password: &str) -> CoreResult<String>;
|
|
async fn verify_password(&self, password: &str, hash: &str) -> CoreResult<()>;
|
|
}
|
|
|
|
pub struct Argon2Hasher;
|
|
|
|
impl Default for Argon2Hasher {
|
|
fn default() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl PasswordHasher for Argon2Hasher {
|
|
async fn hash_password(&self, password: &str) -> CoreResult<String> {
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
|
|
let password_bytes = password.as_bytes().to_vec();
|
|
|
|
let hash_string = tokio::task::spawn_blocking(move || {
|
|
Argon2::default()
|
|
.hash_password(&password_bytes, &salt)
|
|
.map(|hash| hash.to_string())
|
|
.map_err(|e| CoreError::Auth(format!("Password hashing failed: {}", e)))
|
|
})
|
|
.await
|
|
.unwrap()?;
|
|
|
|
Ok(hash_string)
|
|
}
|
|
|
|
async fn verify_password(&self, password: &str, hash_str: &str) -> CoreResult<()> {
|
|
let hash = PasswordHash::new(hash_str)
|
|
.map_err(|e| CoreError::Auth(format!("Invalid password hash format: {}", e)))?;
|
|
|
|
let (password_bytes, hash_bytes) = (password.as_bytes().to_vec(), hash.to_string());
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
Argon2::default()
|
|
.verify_password(&password_bytes, &PasswordHash::new(&hash_bytes).unwrap())
|
|
.map_err(|_| CoreError::Auth("Invalid password".to_string()))
|
|
})
|
|
.await
|
|
.unwrap()
|
|
}
|
|
}
|
|
|
|
static JWT_HEADER: Lazy<Header> = Lazy::new(Header::default);
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
struct Claims {
|
|
sub: Uuid,
|
|
exp: usize,
|
|
iat: usize,
|
|
}
|
|
|
|
#[async_trait]
|
|
pub trait TokenGenerator: Send + Sync {
|
|
fn generate_token(&self, user_id: Uuid) -> CoreResult<String>;
|
|
fn verify_token(&self, token: &str) -> CoreResult<Uuid>;
|
|
}
|
|
|
|
pub struct JwtGenerator {
|
|
encoding_key: EncodingKey,
|
|
decoding_key: DecodingKey,
|
|
}
|
|
|
|
impl JwtGenerator {
|
|
pub fn new(secret: String) -> Self {
|
|
Self {
|
|
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
|
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl TokenGenerator for JwtGenerator {
|
|
fn generate_token(&self, user_id: Uuid) -> CoreResult<String> {
|
|
let now = Utc::now();
|
|
let iat = now.timestamp() as usize;
|
|
let exp = (now + Duration::days(7)).timestamp() as usize;
|
|
|
|
let claims = Claims {
|
|
sub: user_id,
|
|
iat,
|
|
exp,
|
|
};
|
|
|
|
encode(&JWT_HEADER, &claims, &self.encoding_key)
|
|
.map_err(|e| CoreError::Auth(format!("Token generation failed: {}", e)))
|
|
}
|
|
|
|
fn verify_token(&self, token: &str) -> CoreResult<Uuid> {
|
|
decode::<Claims>(token, &self.decoding_key, &Validation::default())
|
|
.map(|data| data.claims.sub)
|
|
.map_err(|e| CoreError::Auth(format!("Token invalid: {}", e)))
|
|
}
|
|
}
|