1use argon2::{
2 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
3 Argon2,
4};
5use pasetors::keys::SymmetricKey;
6use pasetors::token::UntrustedToken;
7use pasetors::version4::V4;
8use pasetors::{claims::Claims, claims::ClaimsValidationRules, Local};
9use secrecy::{ExposeSecret, Secret};
10use serde::{Deserialize, Serialize};
11use time::{Duration, OffsetDateTime};
12use ferrox_errors::AppError;
13
14pub mod auth_middleware;
15pub mod dual_token;
16pub mod public_id;
17
18pub fn hash_password(password: Secret<String>) -> Result<String, AppError> {
21 let salt = SaltString::generate(&mut OsRng);
22 let argon2 = Argon2::default();
23
24 let password_hash = argon2
25 .hash_password(password.expose_secret().as_bytes(), &salt)
26 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
27
28 Ok(password_hash.to_string())
29}
30
31pub fn verify_password(password: Secret<String>, hash: &str) -> Result<bool, AppError> {
33 let parsed_hash = PasswordHash::new(hash)
34 .map_err(|e| AppError::ValidationError(format!("Invalid hash format: {}", e)))?;
35
36 let argon2 = Argon2::default();
37 Ok(argon2
38 .verify_password(password.expose_secret().as_bytes(), &parsed_hash)
39 .is_ok())
40}
41
42pub struct PasetoAuth {
44 key: SymmetricKey<V4>,
45}
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct AuthPayload {
49 pub user_id: String,
50 pub role: String,
51}
52
53impl PasetoAuth {
54 pub fn new(secret: Secret<String>) -> Result<Self, AppError> {
55 let key = SymmetricKey::<V4>::from(secret.expose_secret().as_bytes())
56 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
57 Ok(Self { key })
58 }
59
60 pub fn generate_token(&self, payload: &AuthPayload, duration: Duration) -> Result<String, AppError> {
62 let mut claims = Claims::new().map_err(|e| AppError::InternalServerError(Box::new(e)))?;
63
64 let exp = OffsetDateTime::now_utc() + duration;
66 let exp_iso = exp.format(&time::format_description::well_known::Rfc3339)
67 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
68 claims.expiration(&exp_iso).map_err(|e| AppError::InternalServerError(Box::new(e)))?;
69
70 let user_id_val = serde_json::to_value(&payload.user_id)
72 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
73 claims.add_additional("user_id", user_id_val)
74 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
75
76 let role_val = serde_json::to_value(&payload.role)
77 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
78 claims.add_additional("role", role_val)
79 .map_err(|e| AppError::InternalServerError(Box::new(e)))?;
80
81 pasetors::local::encrypt(&self.key, &claims, None, Some(b"ferrox-auth-footer"))
83 .map_err(|e| AppError::InternalServerError(Box::new(e)))
84 }
85
86 pub fn validate_token(&self, token: &str) -> Result<AuthPayload, AppError> {
88 let validation_rules = ClaimsValidationRules::new();
89 let untrusted_token = UntrustedToken::<Local, V4>::try_from(token)
90 .map_err(|_| AppError::Unauthorized("Invalid token format".into()))?;
91
92 let claims = pasetors::local::decrypt(
93 &self.key,
94 &untrusted_token,
95 &validation_rules,
96 None,
97 Some(b"ferrox-auth-footer"),
98 )
99 .map_err(|_| AppError::Unauthorized("Token validation failed".into()))?;
100
101 let user_id = claims.get_claim("user_id")
102 .and_then(|v| v.as_str())
103 .ok_or_else(|| AppError::Unauthorized("Missing user_id".into()))?;
104
105 let role = claims.get_claim("role")
106 .and_then(|v| v.as_str())
107 .ok_or_else(|| AppError::Unauthorized("Missing role".into()))?;
108
109 Ok(AuthPayload {
110 user_id: user_id.to_string(),
111 role: role.to_string(),
112 })
113 }
114}
115
116pub fn setup() {
117 println!("ferrox-security initialized: Argon2 Hashing and PASETO Authentication ready.");
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_argon2_hashing() {
126 let password = Secret::new("SuperSecureP@ssw0rd!".to_string());
127
128 let hash = hash_password(password.clone()).unwrap();
130 assert!(hash.starts_with("$argon2"));
131
132 let is_valid = verify_password(password, &hash).unwrap();
134 assert!(is_valid);
135
136 let wrong_password = Secret::new("WrongPassword!".to_string());
138 let is_valid_wrong = verify_password(wrong_password, &hash).unwrap();
139 assert!(!is_valid_wrong);
140 }
141
142 #[test]
143 fn test_paseto_token_lifecycle() {
144 let secret = Secret::new("12345678901234567890123456789012".to_string());
146 let auth = PasetoAuth::new(secret).unwrap();
147
148 let payload = AuthPayload {
149 user_id: "user-123".into(),
150 role: "admin".into(),
151 };
152
153 let token = auth.generate_token(&payload, Duration::hours(1)).unwrap();
155 assert!(token.starts_with("v4.local.")); let validated = auth.validate_token(&token).unwrap();
159 assert_eq!(validated.user_id, "user-123");
160 assert_eq!(validated.role, "admin");
161
162 let bad_token = "v4.local.bad_data_here";
164 assert!(auth.validate_token(bad_token).is_err());
165 }
166}