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        // v1.2.1 修复 High H-1(CWE-1188 不安全默认初始化 / CWE-287 认证不当):
188        // 未配置 `password_verifier` 时直接返回 `Err`,拒绝签发 JWT。
189        // 原实现(v0.2.1)仅 `eprintln!` 警告后接受任意凭证并签发 `user_id=0` 的 JWT,
190        // 开发者遗漏配置时将导致完全认证绕过。stderr 警告在生产环境常被忽略。
191        let user_id: i64 = match &self.password_verifier {
192            Some(verifier) => {
193                verifier.verify_password(&credentials.username, &credentials.password)?
194            }
195            None => {
196                return Err(AuthError::Config(
197                    "JwtAuthenticator.password_verifier not configured; \
198                     call with_password_verifier() before authenticate() (H-1)"
199                        .to_string(),
200                ));
201            }
202        };
203
204        let exp = current_timestamp_secs() + (self.expiration as i64);
205        let claims = JwtClaims::new(credentials.username.clone(), exp)
206            .with_issuer(self.issuer.clone())
207            .with_roles(vec!["user".to_string()])
208            .with_user_id(user_id);
209
210        let access_token = self.encoder.encode(&claims)?;
211        let refresh_claims = JwtClaims::new(credentials.username.clone(), exp + 86400)
212            .with_issuer(self.issuer.clone())
213            .with_user_id(user_id);
214        let refresh_token = self.encoder.encode(&refresh_claims)?;
215
216        Ok(Token::new(access_token, self.expiration).with_refresh(refresh_token))
217    }
218
219    pub fn verify_token(&self, token: &str) -> Result<User, AuthError> {
220        if token.is_empty() {
221            return Err(AuthError::TokenInvalid("Token is empty".to_string()));
222        }
223
224        let claims = self.encoder.decode(token)?;
225        // v0.2.1 修复 Critical S-2:从 claims.user_id 恢复正确的 user.id
226        let user_id = claims.user_id.unwrap_or_else(|| {
227            eprintln!(
228                "[warn] JwtAuthenticator::verify_token: token has no user_id claim \
229                 (legacy token); falling back to user.id=0 (Critical S-2)"
230            );
231            0
232        });
233        let user = User::new(user_id, claims.sub.clone())
234            .with_roles(claims.roles.clone())
235            .with_permissions(claims.permissions.clone());
236        Ok(user)
237    }
238
239    pub fn refresh_token(&self, refresh_token: &str) -> Result<Token, AuthError> {
240        if refresh_token.is_empty() {
241            return Err(AuthError::TokenInvalid(
242                "Refresh token is empty".to_string(),
243            ));
244        }
245
246        let claims = self.encoder.decode(refresh_token)?;
247        let now = current_timestamp_secs();
248        let new_exp = now + (self.expiration as i64);
249        let new_claims = JwtClaims::new(claims.sub, new_exp)
250            .with_issuer(self.issuer.clone())
251            .with_roles(claims.roles)
252            .with_permissions(claims.permissions)
253            .with_user_id(claims.user_id.unwrap_or(0));
254        let new_access_token = self.encoder.encode(&new_claims)?;
255
256        Ok(Token::new(new_access_token, self.expiration))
257    }
258}
259
260/// Legacy claims struct kept for backward compatibility with the public API.
261pub struct Claims {
262    pub sub: String,
263    pub exp: i64,
264    pub iat: i64,
265    pub roles: Vec<String>,
266    pub permissions: Vec<String>,
267}
268
269impl Claims {
270    pub fn new(subject: impl Into<String>) -> Self {
271        Self {
272            sub: subject.into(),
273            exp: 0,
274            iat: current_timestamp(),
275            roles: Vec::new(),
276            permissions: Vec::new(),
277        }
278    }
279
280    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
281        self.roles = roles;
282        self
283    }
284
285    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
286        self.permissions = permissions;
287        self
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    /// 测试用密码验证器:用户名哈希后取低 32 位作为 user_id(非 0)。
296    ///
297    /// v1.2.1 修复 H-1 后,`JwtAuthenticator::authenticate` 在未配置 verifier 时
298    /// 返回 `Err(AuthError::Config)`,所有调用 `authenticate` 的测试必须配置 verifier。
299    struct MockPasswordVerifier;
300    impl PasswordVerifier for MockPasswordVerifier {
301        fn verify_password(&self, username: &str, _password: &str) -> Result<i64, AuthError> {
302            // 简单确定性映射:用户名首字节 + 长度,保证 > 0
303            let id = (username.bytes().next().unwrap_or(b'x') as i64) + (username.len() as i64);
304            Ok(id)
305        }
306    }
307
308    /// 构造配置了 MockPasswordVerifier 的 JwtAuthenticator
309    fn auth_with_verifier(secret: &str, issuer: &str, exp: u64) -> JwtAuthenticator {
310        JwtAuthenticator::new(secret, issuer, exp)
311            .with_password_verifier(std::sync::Arc::new(MockPasswordVerifier))
312    }
313
314    #[test]
315    fn test_credentials_new() {
316        let creds = Credentials::new("user", "pass");
317        assert_eq!(creds.username, "user");
318        assert_eq!(creds.password, "pass");
319    }
320
321    #[test]
322    fn test_token_new() {
323        let token = Token::new("access_token", 3600);
324        assert_eq!(token.access_token, "access_token");
325        assert_eq!(token.expires_in, 3600);
326        assert_eq!(token.token_type, "Bearer");
327        assert!(token.refresh_token.is_none());
328    }
329
330    #[test]
331    fn test_token_with_refresh() {
332        let token = Token::new("access", 3600).with_refresh("refresh_token");
333        assert_eq!(token.refresh_token, Some("refresh_token".to_string()));
334    }
335
336    #[test]
337    fn test_token_is_expired() {
338        // 使用 expires_in=1(秒)避免时序竞争:expires_in=0 时 expiry=issued_at,
339        // 任何毫秒级延迟都会导致 now > expiry,使测试不稳定。
340        let mut token = Token::new("test", 1);
341        assert!(!token.is_expired());
342
343        token.issued_at = current_timestamp() - 100_000;
344        token.expires_in = 1;
345        assert!(token.is_expired());
346    }
347
348    #[test]
349    fn test_user_new() {
350        let user = User::new(1, "username");
351        assert_eq!(user.id, 1);
352        assert_eq!(user.username, "username");
353        assert!(user.email.is_none());
354    }
355
356    #[test]
357    fn test_user_with_email() {
358        let user = User::new(1, "user").with_email("user@test.com");
359        assert_eq!(user.email, Some("user@test.com".to_string()));
360    }
361
362    #[test]
363    fn test_user_with_roles() {
364        let user = User::new(1, "user").with_roles(vec!["admin".to_string(), "user".to_string()]);
365        assert!(user.has_role("admin"));
366        assert!(user.has_role("user"));
367        assert!(!user.has_role("guest"));
368    }
369
370    #[test]
371    fn test_user_has_permission() {
372        let user =
373            User::new(1, "user").with_permissions(vec!["read".to_string(), "write".to_string()]);
374
375        assert!(user.has_permission("read"));
376        assert!(user.has_permission("write"));
377        assert!(!user.has_permission("delete"));
378    }
379
380    #[test]
381    fn test_user_admin_has_all() {
382        let user = User::new(1, "admin").with_roles(vec!["admin".to_string()]);
383
384        assert!(user.has_permission("anything"));
385        assert!(user.has_permission("delete"));
386    }
387
388    // ---- Real JWT authenticator tests ----
389
390    #[test]
391    fn test_jwt_authenticate_issues_real_jwt() {
392        let auth = auth_with_verifier("super-secret", "test-issuer", 3600);
393        let creds = Credentials::new("alice", "password123");
394
395        let token = auth.authenticate(&creds).expect("authenticate");
396        assert!(!token.access_token.is_empty());
397        assert_eq!(token.token_type, "Bearer");
398        assert_eq!(token.expires_in, 3600);
399        assert!(token.refresh_token.is_some());
400
401        // access_token must be a real 3-part JWT
402        let parts: Vec<&str> = token.access_token.split('.').collect();
403        assert_eq!(parts.len(), 3, "access token must be a 3-part JWT");
404        let refresh_parts: Vec<&str> = token.refresh_token.as_ref().unwrap().split('.').collect();
405        assert_eq!(refresh_parts.len(), 3, "refresh token must be a 3-part JWT");
406    }
407
408    #[test]
409    fn test_jwt_authenticate_rejects_empty_credentials() {
410        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
411        let creds = Credentials::new("", "");
412        let result = auth.authenticate(&creds);
413        assert!(matches!(result, Err(AuthError::InvalidCredentials(_))));
414    }
415
416    #[test]
417    fn test_jwt_verify_roundtrip() {
418        let auth = auth_with_verifier("super-secret", "test-issuer", 3600);
419        let creds = Credentials::new("bob", "pw");
420
421        let token = auth.authenticate(&creds).expect("authenticate");
422        let user = auth.verify_token(&token.access_token).expect("verify");
423
424        assert_eq!(user.username, "bob");
425        assert!(user.has_role("user"));
426    }
427
428    #[test]
429    fn test_jwt_verify_rejects_garbage() {
430        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
431        let result = auth.verify_token("not.a.jwt");
432        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
433    }
434
435    #[test]
436    fn test_jwt_verify_rejects_empty() {
437        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
438        let result = auth.verify_token("");
439        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
440    }
441
442    #[test]
443    fn test_jwt_verify_rejects_wrong_secret() {
444        let auth_a = auth_with_verifier("secret-a", "issuer", 3600);
445        let auth_b = JwtAuthenticator::new("secret-b", "issuer", 3600);
446
447        let token = auth_a
448            .authenticate(&Credentials::new("user", "pw"))
449            .unwrap();
450        let result = auth_b.verify_token(&token.access_token);
451        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
452    }
453
454    #[test]
455    fn test_jwt_refresh_roundtrip() {
456        let auth = auth_with_verifier("super-secret", "issuer", 3600);
457        let token = auth.authenticate(&Credentials::new("carol", "pw")).unwrap();
458
459        let refreshed = auth
460            .refresh_token(token.refresh_token.as_ref().unwrap())
461            .expect("refresh");
462        // New access token must be a valid JWT and decode to the same subject
463        let user = auth
464            .verify_token(&refreshed.access_token)
465            .expect("verify refreshed");
466        assert_eq!(user.username, "carol");
467    }
468
469    #[test]
470    fn test_jwt_refresh_rejects_empty() {
471        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
472        let result = auth.refresh_token("");
473        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
474    }
475
476    #[test]
477    fn test_jwt_refresh_rejects_garbage() {
478        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
479        let result = auth.refresh_token("garbage");
480        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
481    }
482
483    #[test]
484    fn test_jwt_claims_carried_to_user() {
485        let auth = auth_with_verifier("secret", "issuer", 3600);
486        let token = auth.authenticate(&Credentials::new("dave", "pw")).unwrap();
487        let user = auth.verify_token(&token.access_token).unwrap();
488        // authenticate() grants "user" role by default
489        assert_eq!(user.roles, vec!["user".to_string()]);
490    }
491
492    /// v1.2.1 新增:验证 H-1 修复 — 未配置 password_verifier 时 authenticate() 必须返回 Err
493    #[test]
494    fn test_jwt_authenticate_rejects_when_verifier_not_configured() {
495        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
496        let creds = Credentials::new("alice", "password123");
497        let result = auth.authenticate(&creds);
498        assert!(
499            matches!(result, Err(AuthError::Config(_))),
500            "expected Err(AuthError::Config) when password_verifier is None, got {:?}",
501            result
502        );
503    }
504}