1use crate::types::{AppError, Claims, Result, TokenResponse};
2use argon2::{
3 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
4 Argon2,
5};
6use chrono::{Duration, Utc};
7use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
8
9pub struct AuthService {
14 jwt_secret: String,
15 access_expiry: i64,
16 refresh_expiry: i64,
17}
18
19impl AuthService {
20 pub fn new(jwt_secret: String, access_expiry: i64, refresh_expiry: i64) -> Self {
27 Self {
28 jwt_secret,
29 access_expiry,
30 refresh_expiry,
31 }
32 }
33
34 pub fn hash_password(&self, password: &str) -> Result<String> {
38 let salt = SaltString::generate(&mut OsRng);
39 let argon2 = Argon2::default();
40
41 argon2
42 .hash_password(password.as_bytes(), &salt)
43 .map(|hash| hash.to_string())
44 .map_err(|e| AppError::Auth(format!("Failed to hash password: {}", e)))
45 }
46
47 pub fn verify_password(&self, password: &str, hash: &str) -> Result<bool> {
49 let parsed_hash = PasswordHash::new(hash)
50 .map_err(|e| AppError::Auth(format!("Invalid password hash: {}", e)))?;
51
52 Ok(Argon2::default()
53 .verify_password(password.as_bytes(), &parsed_hash)
54 .is_ok())
55 }
56
57 pub fn generate_tokens(&self, user_id: &str, email: &str) -> Result<TokenResponse> {
59 let access_token = self.generate_access_token(user_id, email)?;
60 let refresh_token = self.generate_refresh_token(user_id, email)?;
61
62 Ok(TokenResponse {
63 access_token,
64 refresh_token,
65 expires_in: self.access_expiry,
66 })
67 }
68
69 fn generate_access_token(&self, user_id: &str, email: &str) -> Result<String> {
70 let claims = Claims {
71 sub: user_id.to_string(),
72 email: email.to_string(),
73 exp: (Utc::now() + Duration::seconds(self.access_expiry)).timestamp() as usize,
74 iat: Utc::now().timestamp() as usize,
75 };
76
77 encode(
78 &Header::new(Algorithm::HS256),
79 &claims,
80 &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
81 )
82 .map_err(|e| AppError::Auth(format!("Failed to generate token: {}", e)))
83 }
84
85 fn generate_refresh_token(&self, user_id: &str, email: &str) -> Result<String> {
86 let claims = Claims {
87 sub: user_id.to_string(),
88 email: email.to_string(),
89 exp: (Utc::now() + Duration::seconds(self.refresh_expiry)).timestamp() as usize,
90 iat: Utc::now().timestamp() as usize,
91 };
92
93 encode(
94 &Header::new(Algorithm::HS256),
95 &claims,
96 &EncodingKey::from_secret(self.jwt_secret.as_bytes()),
97 )
98 .map_err(|e| AppError::Auth(format!("Failed to generate refresh token: {}", e)))
99 }
100
101 pub fn verify_token(&self, token: &str) -> Result<Claims> {
103 let validation = Validation::new(Algorithm::HS256);
104
105 decode::<Claims>(
106 token,
107 &DecodingKey::from_secret(self.jwt_secret.as_bytes()),
108 &validation,
109 )
110 .map(|data| data.claims)
111 .map_err(|e| AppError::Auth(format!("Invalid token: {}", e)))
112 }
113
114 pub fn hash_token(&self, token: &str) -> String {
116 use sha2::{Digest, Sha256};
117 let mut hasher = Sha256::new();
118 hasher.update(token.as_bytes());
119 let result = hasher.finalize();
120 result
121 .iter()
122 .map(|b| format!("{:02x}", b))
123 .collect::<String>()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn create_test_service() -> AuthService {
132 AuthService::new(
133 "test-secret-key-that-is-at-least-32-chars".to_string(),
134 900, 604800, )
137 }
138
139 #[test]
140 fn test_password_hashing() {
141 let service = create_test_service();
142 let password = "test_password_123";
143
144 let hash = service
145 .hash_password(password)
146 .expect("should hash password");
147
148 assert_ne!(hash, password);
150
151 assert!(hash.starts_with("$argon2"), "hash should be in PHC format");
153 }
154
155 #[test]
156 fn test_password_verification_success() {
157 let service = create_test_service();
158 let password = "secure_password_456";
159
160 let hash = service
161 .hash_password(password)
162 .expect("should hash password");
163 let is_valid = service
164 .verify_password(password, &hash)
165 .expect("should verify");
166
167 assert!(is_valid, "correct password should verify successfully");
168 }
169
170 #[test]
171 fn test_password_verification_failure() {
172 let service = create_test_service();
173 let password = "correct_password";
174 let wrong_password = "wrong_password";
175
176 let hash = service
177 .hash_password(password)
178 .expect("should hash password");
179 let is_valid = service
180 .verify_password(wrong_password, &hash)
181 .expect("should verify");
182
183 assert!(!is_valid, "wrong password should fail verification");
184 }
185
186 #[test]
187 fn test_token_generation() {
188 let service = create_test_service();
189 let user_id = "user-123";
190 let email = "test@example.com";
191
192 let tokens = service
193 .generate_tokens(user_id, email)
194 .expect("should generate tokens");
195
196 assert!(
197 !tokens.access_token.is_empty(),
198 "access token should not be empty"
199 );
200 assert!(
201 !tokens.refresh_token.is_empty(),
202 "refresh token should not be empty"
203 );
204 assert_eq!(
205 tokens.expires_in, 900,
206 "expires_in should match configured access expiry"
207 );
208
209 assert_ne!(
211 tokens.access_token, tokens.refresh_token,
212 "access and refresh tokens should differ"
213 );
214 }
215
216 #[test]
217 fn test_token_verification_success() {
218 let service = create_test_service();
219 let user_id = "user-456";
220 let email = "user@test.com";
221
222 let tokens = service
223 .generate_tokens(user_id, email)
224 .expect("should generate tokens");
225 let claims = service
226 .verify_token(&tokens.access_token)
227 .expect("should verify token");
228
229 assert_eq!(claims.sub, user_id, "subject should match user_id");
230 assert_eq!(claims.email, email, "email should match");
231 }
232
233 #[test]
234 fn test_token_verification_invalid_token() {
235 let service = create_test_service();
236
237 let result = service.verify_token("invalid.token.here");
238
239 assert!(result.is_err(), "invalid token should fail verification");
240 }
241
242 #[test]
243 fn test_token_verification_wrong_secret() {
244 let service1 =
245 AuthService::new("secret-one-that-is-32-chars-long".to_string(), 900, 604800);
246 let service2 =
247 AuthService::new("secret-two-that-is-32-chars-long".to_string(), 900, 604800);
248
249 let tokens = service1
250 .generate_tokens("user-789", "test@example.com")
251 .expect("should generate");
252 let result = service2.verify_token(&tokens.access_token);
253
254 assert!(result.is_err(), "token from different secret should fail");
255 }
256
257 #[test]
258 fn test_hash_token() {
259 let service = create_test_service();
260 let token = "some-refresh-token";
261
262 let hash1 = service.hash_token(token);
263 let hash2 = service.hash_token(token);
264
265 assert_eq!(hash1, hash2, "same token should hash to same value");
267
268 assert_eq!(hash1.len(), 64, "SHA256 hash should be 64 hex characters");
270 assert!(
271 hash1.chars().all(|c| c.is_ascii_hexdigit()),
272 "hash should be hex"
273 );
274 }
275
276 #[test]
277 fn test_hash_token_different_inputs() {
278 let service = create_test_service();
279
280 let hash1 = service.hash_token("token-a");
281 let hash2 = service.hash_token("token-b");
282
283 assert_ne!(
284 hash1, hash2,
285 "different tokens should have different hashes"
286 );
287 }
288
289 #[test]
290 fn test_claims_expiration() {
291 let service = create_test_service();
292 let tokens = service
293 .generate_tokens("user", "user@example.com")
294 .expect("should generate");
295 let claims = service
296 .verify_token(&tokens.access_token)
297 .expect("should verify");
298
299 let now = chrono::Utc::now().timestamp() as usize;
300
301 assert!(
303 claims.iat <= now && claims.iat >= now - 5,
304 "iat should be current timestamp"
305 );
306
307 let expected_exp = claims.iat + 900;
309 assert!(
310 claims.exp >= expected_exp - 5 && claims.exp <= expected_exp + 5,
311 "exp should be iat + 900 seconds"
312 );
313 }
314}