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    /// HS256 最小密钥长度(v4.8.0 修复 M-15)
163    ///
164    /// 修复前默认路径无密钥强度校验——短密钥/常见口令可被 GPU 离线爆破
165    /// (数十亿次/秒),攻击者获取任一令牌即可枚举密钥并任意伪造 claims。
166    pub const MIN_SECRET_LEN: usize = 32;
167
168    /// 创建认证器(兼容旧签名,不做密钥强度校验)
169    ///
170    /// **生产环境必须使用 [`JwtAuthenticator::try_new`]**——本构造器保留
171    /// 兼容性(测试/低风险场景),弱密钥风险见 M-15。
172    pub fn new(secret: impl Into<String>, issuer: impl Into<String>, expiration: u64) -> Self {
173        Self {
174            encoder: JwtEncoder::new(secret),
175            issuer: issuer.into(),
176            expiration,
177            password_verifier: None,
178        }
179    }
180
181    /// 创建认证器并强制校验密钥强度(v4.8.0 修复 M-15)
182    ///
183    /// secret 长度必须 ≥ [`JwtAuthenticator::MIN_SECRET_LEN`](32 字节),
184    /// 否则返回 [`AuthError::SecretTooShort`]。生产路径应使用本构造器。
185    pub fn try_new(
186        secret: impl Into<String>,
187        issuer: impl Into<String>,
188        expiration: u64,
189    ) -> Result<Self, AuthError> {
190        let secret = secret.into();
191        if secret.len() < Self::MIN_SECRET_LEN {
192            return Err(AuthError::SecretTooShort(format!(
193                "JWT secret must be at least {} bytes (got {})",
194                Self::MIN_SECRET_LEN,
195                secret.len()
196            )));
197        }
198        Ok(Self {
199            encoder: JwtEncoder::new(secret),
200            issuer: issuer.into(),
201            expiration,
202            password_verifier: None,
203        })
204    }
205
206    /// 配置密码验证器(v0.2.1 新增,修复 Critical S-1)
207    ///
208    /// 配置后,`authenticate` 会调用 `verifier.verify_password` 验证密码并获取 user_id。
209    /// **生产环境必须调用此方法**,否则 `authenticate` 不验证密码(Critical S-1)。
210    pub fn with_password_verifier(mut self, verifier: Arc<dyn PasswordVerifier>) -> Self {
211        self.password_verifier = Some(verifier);
212        self
213    }
214
215    pub fn authenticate(&self, credentials: &Credentials) -> Result<Token, AuthError> {
216        if credentials.username.is_empty() || credentials.password.is_empty() {
217            return Err(AuthError::InvalidCredentials(
218                "Username or password is empty".to_string(),
219            ));
220        }
221
222        // v1.2.1 修复 High H-1(CWE-1188 不安全默认初始化 / CWE-287 认证不当):
223        // 未配置 `password_verifier` 时直接返回 `Err`,拒绝签发 JWT。
224        // 原实现(v0.2.1)仅 `eprintln!` 警告后接受任意凭证并签发 `user_id=0` 的 JWT,
225        // 开发者遗漏配置时将导致完全认证绕过。stderr 警告在生产环境常被忽略。
226        let user_id: i64 = match &self.password_verifier {
227            Some(verifier) => {
228                verifier.verify_password(&credentials.username, &credentials.password)?
229            }
230            None => {
231                return Err(AuthError::Config(
232                    "JwtAuthenticator.password_verifier not configured; \
233                     call with_password_verifier() before authenticate() (H-1)"
234                        .to_string(),
235                ));
236            }
237        };
238
239        let exp = current_timestamp_secs() + (self.expiration as i64);
240        // v4.8.0 修复 Critical C-2:访问/刷新令牌必须带 token_use 类型声明,
241        // 防止窃取的访问令牌被 refresh 端点接受导致无限续期(黑帽实证)。
242        let claims = JwtClaims::new(credentials.username.clone(), exp)
243            .with_issuer(self.issuer.clone())
244            .with_roles(vec!["user".to_string()])
245            .with_user_id(user_id)
246            .with_token_use("access");
247
248        let access_token = self.encoder.encode(&claims)?;
249        let refresh_claims = JwtClaims::new(credentials.username.clone(), exp + 86400)
250            .with_issuer(self.issuer.clone())
251            .with_user_id(user_id)
252            .with_token_use("refresh");
253        let refresh_token = self.encoder.encode(&refresh_claims)?;
254
255        Ok(Token::new(access_token, self.expiration).with_refresh(refresh_token))
256    }
257
258    pub fn verify_token(&self, token: &str) -> Result<User, AuthError> {
259        if token.is_empty() {
260            return Err(AuthError::TokenInvalid("Token is empty".to_string()));
261        }
262
263        let claims = self.encoder.decode(token)?;
264        // v0.2.1 修复 Critical S-2:从 claims.user_id 恢复正确的 user.id
265        let user_id = claims.user_id.unwrap_or_else(|| {
266            eprintln!(
267                "[warn] JwtAuthenticator::verify_token: token has no user_id claim \
268                 (legacy token); falling back to user.id=0 (Critical S-2)"
269            );
270            0
271        });
272        let user = User::new(user_id, claims.sub.clone())
273            .with_roles(claims.roles.clone())
274            .with_permissions(claims.permissions.clone());
275        Ok(user)
276    }
277
278    pub fn refresh_token(&self, refresh_token: &str) -> Result<Token, AuthError> {
279        if refresh_token.is_empty() {
280            return Err(AuthError::TokenInvalid(
281                "Refresh token is empty".to_string(),
282            ));
283        }
284
285        let claims = self.encoder.decode(refresh_token)?;
286        // v4.8.0 修复 Critical C-2:严格校验令牌类型——仅接受显式声明
287        // token_use="refresh" 的令牌。旧版无类型声明的令牌一律拒绝
288        //(访问令牌送入 refresh 端点的类型混淆攻击面被切断)。
289        if !claims.is_token_use("refresh") {
290            return Err(AuthError::TokenInvalid(
291                "Token is not a refresh token (missing token_use=refresh)".to_string(),
292            ));
293        }
294
295        let now = current_timestamp_secs();
296        let new_exp = now + (self.expiration as i64);
297        let new_claims = JwtClaims::new(claims.sub, new_exp)
298            .with_issuer(self.issuer.clone())
299            .with_roles(claims.roles)
300            .with_permissions(claims.permissions)
301            .with_user_id(claims.user_id.unwrap_or(0))
302            .with_token_use("access");
303        let new_access_token = self.encoder.encode(&new_claims)?;
304
305        Ok(Token::new(new_access_token, self.expiration))
306    }
307}
308
309/// Legacy claims struct kept for backward compatibility with the public API.
310pub struct Claims {
311    pub sub: String,
312    pub exp: i64,
313    pub iat: i64,
314    pub roles: Vec<String>,
315    pub permissions: Vec<String>,
316}
317
318impl Claims {
319    pub fn new(subject: impl Into<String>) -> Self {
320        Self {
321            sub: subject.into(),
322            exp: 0,
323            iat: current_timestamp(),
324            roles: Vec::new(),
325            permissions: Vec::new(),
326        }
327    }
328
329    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
330        self.roles = roles;
331        self
332    }
333
334    pub fn with_permissions(mut self, permissions: Vec<String>) -> Self {
335        self.permissions = permissions;
336        self
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    /// 测试用密码验证器:用户名哈希后取低 32 位作为 user_id(非 0)。
345    ///
346    /// v1.2.1 修复 H-1 后,`JwtAuthenticator::authenticate` 在未配置 verifier 时
347    /// 返回 `Err(AuthError::Config)`,所有调用 `authenticate` 的测试必须配置 verifier。
348    struct MockPasswordVerifier;
349    impl PasswordVerifier for MockPasswordVerifier {
350        fn verify_password(&self, username: &str, _password: &str) -> Result<i64, AuthError> {
351            // 简单确定性映射:用户名首字节 + 长度,保证 > 0
352            let id = (username.bytes().next().unwrap_or(b'x') as i64) + (username.len() as i64);
353            Ok(id)
354        }
355    }
356
357    /// 构造配置了 MockPasswordVerifier 的 JwtAuthenticator
358    fn auth_with_verifier(secret: &str, issuer: &str, exp: u64) -> JwtAuthenticator {
359        JwtAuthenticator::new(secret, issuer, exp)
360            .with_password_verifier(std::sync::Arc::new(MockPasswordVerifier))
361    }
362
363    #[test]
364    fn test_credentials_new() {
365        let creds = Credentials::new("user", "pass");
366        assert_eq!(creds.username, "user");
367        assert_eq!(creds.password, "pass");
368    }
369
370    #[test]
371    fn test_token_new() {
372        let token = Token::new("access_token", 3600);
373        assert_eq!(token.access_token, "access_token");
374        assert_eq!(token.expires_in, 3600);
375        assert_eq!(token.token_type, "Bearer");
376        assert!(token.refresh_token.is_none());
377    }
378
379    #[test]
380    fn test_token_with_refresh() {
381        let token = Token::new("access", 3600).with_refresh("refresh_token");
382        assert_eq!(token.refresh_token, Some("refresh_token".to_string()));
383    }
384
385    #[test]
386    fn test_token_is_expired() {
387        // 使用 expires_in=1(秒)避免时序竞争:expires_in=0 时 expiry=issued_at,
388        // 任何毫秒级延迟都会导致 now > expiry,使测试不稳定。
389        let mut token = Token::new("test", 1);
390        assert!(!token.is_expired());
391
392        token.issued_at = current_timestamp() - 100_000;
393        token.expires_in = 1;
394        assert!(token.is_expired());
395    }
396
397    #[test]
398    fn test_user_new() {
399        let user = User::new(1, "username");
400        assert_eq!(user.id, 1);
401        assert_eq!(user.username, "username");
402        assert!(user.email.is_none());
403    }
404
405    #[test]
406    fn test_user_with_email() {
407        let user = User::new(1, "user").with_email("user@test.com");
408        assert_eq!(user.email, Some("user@test.com".to_string()));
409    }
410
411    #[test]
412    fn test_user_with_roles() {
413        let user = User::new(1, "user").with_roles(vec!["admin".to_string(), "user".to_string()]);
414        assert!(user.has_role("admin"));
415        assert!(user.has_role("user"));
416        assert!(!user.has_role("guest"));
417    }
418
419    #[test]
420    fn test_user_has_permission() {
421        let user =
422            User::new(1, "user").with_permissions(vec!["read".to_string(), "write".to_string()]);
423
424        assert!(user.has_permission("read"));
425        assert!(user.has_permission("write"));
426        assert!(!user.has_permission("delete"));
427    }
428
429    #[test]
430    fn test_user_admin_has_all() {
431        let user = User::new(1, "admin").with_roles(vec!["admin".to_string()]);
432
433        assert!(user.has_permission("anything"));
434        assert!(user.has_permission("delete"));
435    }
436
437    // ---- Real JWT authenticator tests ----
438
439    #[test]
440    fn test_jwt_authenticate_issues_real_jwt() {
441        let auth = auth_with_verifier("super-secret", "test-issuer", 3600);
442        let creds = Credentials::new("alice", "password123");
443
444        let token = auth.authenticate(&creds).expect("authenticate");
445        assert!(!token.access_token.is_empty());
446        assert_eq!(token.token_type, "Bearer");
447        assert_eq!(token.expires_in, 3600);
448        assert!(token.refresh_token.is_some());
449
450        // access_token must be a real 3-part JWT
451        let parts: Vec<&str> = token.access_token.split('.').collect();
452        assert_eq!(parts.len(), 3, "access token must be a 3-part JWT");
453        let refresh_parts: Vec<&str> = token.refresh_token.as_ref().unwrap().split('.').collect();
454        assert_eq!(refresh_parts.len(), 3, "refresh token must be a 3-part JWT");
455    }
456
457    #[test]
458    fn test_jwt_authenticate_rejects_empty_credentials() {
459        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
460        let creds = Credentials::new("", "");
461        let result = auth.authenticate(&creds);
462        assert!(matches!(result, Err(AuthError::InvalidCredentials(_))));
463    }
464
465    #[test]
466    fn test_jwt_verify_roundtrip() {
467        let auth = auth_with_verifier("super-secret", "test-issuer", 3600);
468        let creds = Credentials::new("bob", "pw");
469
470        let token = auth.authenticate(&creds).expect("authenticate");
471        let user = auth.verify_token(&token.access_token).expect("verify");
472
473        assert_eq!(user.username, "bob");
474        assert!(user.has_role("user"));
475    }
476
477    #[test]
478    fn test_jwt_verify_rejects_garbage() {
479        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
480        let result = auth.verify_token("not.a.jwt");
481        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
482    }
483
484    #[test]
485    fn test_jwt_verify_rejects_empty() {
486        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
487        let result = auth.verify_token("");
488        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
489    }
490
491    #[test]
492    fn test_jwt_verify_rejects_wrong_secret() {
493        let auth_a = auth_with_verifier("secret-a", "issuer", 3600);
494        let auth_b = JwtAuthenticator::new("secret-b", "issuer", 3600);
495
496        let token = auth_a
497            .authenticate(&Credentials::new("user", "pw"))
498            .unwrap();
499        let result = auth_b.verify_token(&token.access_token);
500        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
501    }
502
503    #[test]
504    fn test_jwt_refresh_roundtrip() {
505        let auth = auth_with_verifier("super-secret", "issuer", 3600);
506        let token = auth.authenticate(&Credentials::new("carol", "pw")).unwrap();
507
508        let refreshed = auth
509            .refresh_token(token.refresh_token.as_ref().unwrap())
510            .expect("refresh");
511        // New access token must be a valid JWT and decode to the same subject
512        let user = auth
513            .verify_token(&refreshed.access_token)
514            .expect("verify refreshed");
515        assert_eq!(user.username, "carol");
516    }
517
518    #[test]
519    fn test_jwt_refresh_rejects_empty() {
520        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
521        let result = auth.refresh_token("");
522        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
523    }
524
525    #[test]
526    fn test_jwt_refresh_rejects_garbage() {
527        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
528        let result = auth.refresh_token("garbage");
529        assert!(matches!(result, Err(AuthError::TokenInvalid(_))));
530    }
531
532    #[test]
533    fn test_jwt_claims_carried_to_user() {
534        let auth = auth_with_verifier("secret", "issuer", 3600);
535        let token = auth.authenticate(&Credentials::new("dave", "pw")).unwrap();
536        let user = auth.verify_token(&token.access_token).unwrap();
537        // authenticate() grants "user" role by default
538        assert_eq!(user.roles, vec!["user".to_string()]);
539    }
540
541    /// v1.2.1 新增:验证 H-1 修复 — 未配置 password_verifier 时 authenticate() 必须返回 Err
542    #[test]
543    fn test_jwt_authenticate_rejects_when_verifier_not_configured() {
544        let auth = JwtAuthenticator::new("secret", "issuer", 3600);
545        let creds = Credentials::new("alice", "password123");
546        let result = auth.authenticate(&creds);
547        assert!(
548            matches!(result, Err(AuthError::Config(_))),
549            "expected Err(AuthError::Config) when password_verifier is None, got {:?}",
550            result
551        );
552    }
553}