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