Skip to main content

sz_orm_auth/
auth.rs

1use crate::error::AuthError;
2use crate::jwt::{JwtClaims, JwtEncoder};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// 密码验证器 trait(v0.2.1 新增,修复 Critical S-1)
8///
9/// `JwtAuthenticator::authenticate` 调用此 trait 验证密码并获取 user_id。
10/// 调用方负责实现真实的密码哈希校验(如 bcrypt/argon2)和用户查询。
11///
12/// # 示例
13///
14/// ```ignore
15/// use sz_orm_auth::{PasswordVerifier, AuthError};
16///
17/// struct DbPasswordVerifier;
18///
19/// impl PasswordVerifier for DbPasswordVerifier {
20///     fn verify_password(&self, username: &str, password: &str) -> Result<i64, AuthError> {
21///         // 1. 查询数据库获取 stored_hash 和 user_id
22///         // 2. 用 bcrypt::verify(password, &stored_hash) 校验
23///         // 3. 校验通过返回 Ok(user_id),否则 Err(AuthError::InvalidCredentials(...))
24///         # unimplemented!()
25///     }
26/// }
27/// ```
28pub trait PasswordVerifier: Send + Sync {
29    /// 验证密码并返回 user_id
30    ///
31    /// # 返回
32    /// - `Ok(user_id)`:密码正确,返回用户 ID
33    /// - `Err(AuthError::InvalidCredentials(_))`:密码错误或用户不存在
34    fn verify_password(&self, username: &str, password: &str) -> Result<i64, AuthError>;
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Credentials {
39    pub username: String,
40    pub password: String,
41}
42
43impl Credentials {
44    pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
45        Self {
46            username: username.into(),
47            password: password.into(),
48        }
49    }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Token {
54    pub access_token: String,
55    pub refresh_token: Option<String>,
56    pub token_type: String,
57    pub expires_in: u64,
58    pub issued_at: i64,
59}
60
61impl Token {
62    pub fn new(access_token: impl Into<String>, expires_in: u64) -> Self {
63        Self {
64            access_token: access_token.into(),
65            refresh_token: None,
66            token_type: "Bearer".to_string(),
67            expires_in,
68            issued_at: current_timestamp(),
69        }
70    }
71
72    pub fn with_refresh(mut self, refresh_token: impl Into<String>) -> Self {
73        self.refresh_token = Some(refresh_token.into());
74        self
75    }
76
77    pub fn is_expired(&self) -> bool {
78        let now = current_timestamp();
79        let expiry = self.issued_at + (self.expires_in as i64 * 1000);
80        now > expiry
81    }
82
83    pub fn expires_at(&self) -> i64 {
84        self.issued_at + (self.expires_in as i64 * 1000)
85    }
86}
87
88fn current_timestamp() -> i64 {
89    use std::time::{SystemTime, UNIX_EPOCH};
90    SystemTime::now()
91        .duration_since(UNIX_EPOCH)
92        .unwrap_or_default()
93        .as_millis() as i64
94}
95
96fn current_timestamp_secs() -> i64 {
97    use std::time::{SystemTime, UNIX_EPOCH};
98    SystemTime::now()
99        .duration_since(UNIX_EPOCH)
100        .unwrap_or_default()
101        .as_secs() as i64
102}
103
104pub struct User {
105    pub id: i64,
106    pub username: String,
107    pub email: Option<String>,
108    pub roles: Vec<String>,
109    pub permissions: Vec<String>,
110    pub metadata: HashMap<String, serde_json::Value>,
111}
112
113impl User {
114    pub fn new(id: i64, username: impl Into<String>) -> Self {
115        Self {
116            id,
117            username: username.into(),
118            email: None,
119            roles: Vec::new(),
120            permissions: Vec::new(),
121            metadata: HashMap::new(),
122        }
123    }
124
125    pub fn with_email(mut self, email: impl Into<String>) -> Self {
126        self.email = Some(email.into());
127        self
128    }
129
130    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
131        self.roles = roles;
132        self
133    }
134
135    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
136        self.permissions = permissions;
137        self
138    }
139
140    pub fn has_role(&self, role: &str) -> bool {
141        self.roles.iter().any(|r| r == role)
142    }
143
144    pub fn has_permission(&self, permission: &str) -> bool {
145        self.permissions.iter().any(|p| p == permission) || self.has_role("admin")
146    }
147}
148
149/// Authenticator that issues and verifies real HS256 JWTs.
150pub struct JwtAuthenticator {
151    encoder: JwtEncoder,
152    issuer: String,
153    expiration: u64,
154    /// 可选密码验证器(v0.2.1 新增,修复 Critical S-1)
155    ///
156    /// - `Some(verifier)`:`authenticate` 调用 `verifier.verify_password` 获取 user_id
157    /// - `None`:保留旧行为(不验证密码),但 `eprintln!` 警告生产环境必须配置
158    password_verifier: Option<Arc<dyn PasswordVerifier>>,
159}
160
161impl JwtAuthenticator {
162    pub fn new(secret: impl Into<String>, issuer: impl Into<String>, expiration: u64) -> Self {
163        Self {
164            encoder: JwtEncoder::new(secret),
165            issuer: issuer.into(),
166            expiration,
167            password_verifier: None,
168        }
169    }
170
171    /// 配置密码验证器(v0.2.1 新增,修复 Critical S-1)
172    ///
173    /// 配置后,`authenticate` 会调用 `verifier.verify_password` 验证密码并获取 user_id。
174    /// **生产环境必须调用此方法**,否则 `authenticate` 不验证密码(Critical S-1)。
175    pub fn with_password_verifier(mut self, verifier: Arc<dyn PasswordVerifier>) -> Self {
176        self.password_verifier = Some(verifier);
177        self
178    }
179
180    pub fn authenticate(&self, credentials: &Credentials) -> Result<Token, AuthError> {
181        if credentials.username.is_empty() || credentials.password.is_empty() {
182            return Err(AuthError::InvalidCredentials(
183                "Username or password is empty".to_string(),
184            ));
185        }
186
187        // v0.2.1 修复 Critical S-1:必须通过 PasswordVerifier 验证密码
188        let user_id: i64 = if let Some(verifier) = &self.password_verifier {
189            verifier.verify_password(&credentials.username, &credentials.password)?
190        } else {
191            // 未配置 verifier:保留旧行为(向后兼容)但警告
192            // 生产环境必须通过 with_password_verifier() 配置 verifier
193            eprintln!(
194                "[warn] JwtAuthenticator::authenticate: password_verifier not configured; \
195                 accepting credentials without password verification (Critical S-1)"
196            );
197            0
198        };
199
200        let exp = current_timestamp_secs() + (self.expiration as i64);
201        let claims = JwtClaims::new(credentials.username.clone(), exp)
202            .with_issuer(self.issuer.clone())
203            .with_roles(vec!["user".to_string()])
204            .with_user_id(user_id);
205
206        let access_token = self.encoder.encode(&claims)?;
207        let refresh_claims = JwtClaims::new(credentials.username.clone(), exp + 86400)
208            .with_issuer(self.issuer.clone())
209            .with_user_id(user_id);
210        let refresh_token = self.encoder.encode(&refresh_claims)?;
211
212        Ok(Token::new(access_token, self.expiration).with_refresh(refresh_token))
213    }
214
215    pub fn verify_token(&self, token: &str) -> Result<User, AuthError> {
216        if token.is_empty() {
217            return Err(AuthError::TokenInvalid("Token is empty".to_string()));
218        }
219
220        let claims = self.encoder.decode(token)?;
221        // v0.2.1 修复 Critical S-2:从 claims.user_id 恢复正确的 user.id
222        let user_id = claims.user_id.unwrap_or_else(|| {
223            eprintln!(
224                "[warn] JwtAuthenticator::verify_token: token has no user_id claim \
225                 (legacy token); falling back to user.id=0 (Critical S-2)"
226            );
227            0
228        });
229        let user = User::new(user_id, claims.sub.clone())
230            .with_roles(claims.roles.clone())
231            .with_permissions(claims.permissions.clone());
232        Ok(user)
233    }
234
235    pub fn refresh_token(&self, refresh_token: &str) -> Result<Token, AuthError> {
236        if refresh_token.is_empty() {
237            return Err(AuthError::TokenInvalid(
238                "Refresh token is empty".to_string(),
239            ));
240        }
241
242        let claims = self.encoder.decode(refresh_token)?;
243        let now = current_timestamp_secs();
244        let new_exp = now + (self.expiration as i64);
245        let new_claims = JwtClaims::new(claims.sub, new_exp)
246            .with_issuer(self.issuer.clone())
247            .with_roles(claims.roles)
248            .with_permissions(claims.permissions)
249            .with_user_id(claims.user_id.unwrap_or(0));
250        let new_access_token = self.encoder.encode(&new_claims)?;
251
252        Ok(Token::new(new_access_token, self.expiration))
253    }
254}
255
256/// Legacy claims struct kept for backward compatibility with the public API.
257pub struct Claims {
258    pub sub: String,
259    pub exp: i64,
260    pub iat: i64,
261    pub roles: Vec<String>,
262    pub permissions: Vec<String>,
263}
264
265impl Claims {
266    pub fn new(subject: impl Into<String>) -> Self {
267        Self {
268            sub: subject.into(),
269            exp: 0,
270            iat: current_timestamp(),
271            roles: Vec::new(),
272            permissions: Vec::new(),
273        }
274    }
275
276    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
277        self.roles = roles;
278        self
279    }
280
281    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
282        self.permissions = permissions;
283        self
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_credentials_new() {
293        let creds = Credentials::new("user", "pass");
294        assert_eq!(creds.username, "user");
295        assert_eq!(creds.password, "pass");
296    }
297
298    #[test]
299    fn test_token_new() {
300        let token = Token::new("access_token", 3600);
301        assert_eq!(token.access_token, "access_token");
302        assert_eq!(token.expires_in, 3600);
303        assert_eq!(token.token_type, "Bearer");
304        assert!(token.refresh_token.is_none());
305    }
306
307    #[test]
308    fn test_token_with_refresh() {
309        let token = Token::new("access", 3600).with_refresh("refresh_token");
310        assert_eq!(token.refresh_token, Some("refresh_token".to_string()));
311    }
312
313    #[test]
314    fn test_token_is_expired() {
315        // 使用 expires_in=1(秒)避免时序竞争:expires_in=0 时 expiry=issued_at,
316        // 任何毫秒级延迟都会导致 now > expiry,使测试不稳定。
317        let mut token = Token::new("test", 1);
318        assert!(!token.is_expired());
319
320        token.issued_at = current_timestamp() - 100_000;
321        token.expires_in = 1;
322        assert!(token.is_expired());
323    }
324
325    #[test]
326    fn test_user_new() {
327        let user = User::new(1, "username");
328        assert_eq!(user.id, 1);
329        assert_eq!(user.username, "username");
330        assert!(user.email.is_none());
331    }
332
333    #[test]
334    fn test_user_with_email() {
335        let user = User::new(1, "user").with_email("user@test.com");
336        assert_eq!(user.email, Some("user@test.com".to_string()));
337    }
338
339    #[test]
340    fn test_user_with_roles() {
341        let user = User::new(1, "user").with_roles(vec!["admin".to_string(), "user".to_string()]);
342        assert!(user.has_role("admin"));
343        assert!(user.has_role("user"));
344        assert!(!user.has_role("guest"));
345    }
346
347    #[test]
348    fn test_user_has_permission() {
349        let user =
350            User::new(1, "user").with_permissions(vec!["read".to_string(), "write".to_string()]);
351
352        assert!(user.has_permission("read"));
353        assert!(user.has_permission("write"));
354        assert!(!user.has_permission("delete"));
355    }
356
357    #[test]
358    fn test_user_admin_has_all() {
359        let user = User::new(1, "admin").with_roles(vec!["admin".to_string()]);
360
361        assert!(user.has_permission("anything"));
362        assert!(user.has_permission("delete"));
363    }
364
365    // ---- Real JWT authenticator tests ----
366
367    #[test]
368    fn test_jwt_authenticate_issues_real_jwt() {
369        let auth = JwtAuthenticator::new("super-secret", "test-issuer", 3600);
370        let creds = Credentials::new("alice", "password123");
371
372        let token = auth.authenticate(&creds).expect("authenticate");
373        assert!(!token.access_token.is_empty());
374        assert_eq!(token.token_type, "Bearer");
375        assert_eq!(token.expires_in, 3600);
376        assert!(token.refresh_token.is_some());
377
378        // access_token must be a real 3-part JWT
379        let parts: Vec<&str> = token.access_token.split('.').collect();
380        assert_eq!(parts.len(), 3, "access token must be a 3-part JWT");
381        let refresh_parts: Vec<&str> = token.refresh_token.as_ref().unwrap().split('.').collect();
382        assert_eq!(refresh_parts.len(), 3, "refresh token must be a 3-part JWT");
383    }
384
385    #[test]
386    fn test_jwt_authenticate_rejects_empty_credentials() {
387        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
388        let creds = Credentials::new("", "");
389        let result = auth.authenticate(&creds);
390        assert!(matches!(result, Err(AuthError::InvalidCredentials(_))));
391    }
392
393    #[test]
394    fn test_jwt_verify_roundtrip() {
395        let auth = JwtAuthenticator::new("super-secret", "test-issuer", 3600);
396        let creds = Credentials::new("bob", "pw");
397
398        let token = auth.authenticate(&creds).expect("authenticate");
399        let user = auth.verify_token(&token.access_token).expect("verify");
400
401        assert_eq!(user.username, "bob");
402        assert!(user.has_role("user"));
403    }
404
405    #[test]
406    fn test_jwt_verify_rejects_garbage() {
407        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
408        let result = auth.verify_token("not.a.jwt");
409        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
410    }
411
412    #[test]
413    fn test_jwt_verify_rejects_empty() {
414        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
415        let result = auth.verify_token("");
416        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
417    }
418
419    #[test]
420    fn test_jwt_verify_rejects_wrong_secret() {
421        let auth_a = JwtAuthenticator::new("secret-a", "issuer", 3600);
422        let auth_b = JwtAuthenticator::new("secret-b", "issuer", 3600);
423
424        let token = auth_a
425            .authenticate(&Credentials::new("user", "pw"))
426            .unwrap();
427        let result = auth_b.verify_token(&token.access_token);
428        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
429    }
430
431    #[test]
432    fn test_jwt_refresh_roundtrip() {
433        let auth = JwtAuthenticator::new("super-secret", "issuer", 3600);
434        let token = auth.authenticate(&Credentials::new("carol", "pw")).unwrap();
435
436        let refreshed = auth
437            .refresh_token(token.refresh_token.as_ref().unwrap())
438            .expect("refresh");
439        // New access token must be a valid JWT and decode to the same subject
440        let user = auth
441            .verify_token(&refreshed.access_token)
442            .expect("verify refreshed");
443        assert_eq!(user.username, "carol");
444    }
445
446    #[test]
447    fn test_jwt_refresh_rejects_empty() {
448        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
449        let result = auth.refresh_token("");
450        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
451    }
452
453    #[test]
454    fn test_jwt_refresh_rejects_garbage() {
455        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
456        let result = auth.refresh_token("garbage");
457        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
458    }
459
460    #[test]
461    fn test_jwt_claims_carried_to_user() {
462        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
463        let token = auth.authenticate(&Credentials::new("dave", "pw")).unwrap();
464        let user = auth.verify_token(&token.access_token).unwrap();
465        // authenticate() grants "user" role by default
466        assert_eq!(user.roles, vec!["user".to_string()]);
467    }
468}