1use chrono::{Duration, Utc};
2use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::error::AuthError;
7use crate::types::{PermissionName, RoleName, User};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct Claims {
12 pub sub: String,
14 pub email: String,
16 pub roles: Vec<String>,
18 pub permissions: Vec<String>,
20 pub exp: i64,
22 pub iat: i64,
24 pub jti: String,
26}
27
28pub struct JwtConfig {
30 pub secret: String,
32 pub expiry: Duration,
34 pub issuer: Option<String>,
36}
37
38impl JwtConfig {
39 pub fn new(secret: impl Into<String>, expiry: Duration) -> Self {
40 Self {
41 secret: secret.into(),
42 expiry,
43 issuer: None,
44 }
45 }
46}
47
48pub fn generate_token(
53 user: &User,
54 roles: &[RoleName],
55 permissions: &[PermissionName],
56 config: &JwtConfig,
57) -> Result<String, AuthError> {
58 let now = Utc::now();
59 let iat = now.timestamp();
60 let exp = (now + config.expiry).timestamp();
61
62 let claims = Claims {
63 sub: user.id.to_string(),
64 email: user.email.as_str().to_string(),
65 roles: roles.iter().map(|r| r.as_str().to_string()).collect(),
66 permissions: permissions.iter().map(|p| p.as_str().to_string()).collect(),
67 exp,
68 iat,
69 jti: Uuid::now_v7().to_string(),
70 };
71
72 let key = EncodingKey::from_secret(config.secret.as_bytes());
73 encode(&Header::new(Algorithm::HS256), &claims, &key).map_err(|e| AuthError::Jwt(e.to_string()))
74}
75
76pub fn validate_token(token: &str, config: &JwtConfig) -> Result<Claims, AuthError> {
82 let key = DecodingKey::from_secret(config.secret.as_bytes());
83 let mut validation = Validation::new(Algorithm::HS256);
84 validation.leeway = 0;
85 decode::<Claims>(token, &key, &validation)
86 .map(|data| data.claims)
87 .map_err(|e| AuthError::Jwt(e.to_string()))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::types::{Email, PermissionName, RoleName, User, UserId};
94
95 fn test_user() -> User {
96 User {
97 id: UserId::new(),
98 email: Email::new_unchecked("alice@example.com".to_string()),
99 username: None,
100 password_hash: None,
101 email_verified: true,
102 is_active: true,
103 created_at: Utc::now(),
104 updated_at: Utc::now(),
105 custom_data: None,
106 }
107 }
108
109 fn test_config() -> JwtConfig {
110 JwtConfig::new("test-secret-key-for-hs256", Duration::hours(1))
111 }
112
113 #[test]
114 fn test_generate_and_validate_round_trip() {
115 let user = test_user();
116 let roles = vec![RoleName::new("admin")];
117 let permissions = vec![PermissionName::new("read:posts")];
118 let config = test_config();
119
120 let token = generate_token(&user, &roles, &permissions, &config).expect("generate token");
121
122 let claims = validate_token(&token, &config).expect("validate token");
123
124 assert_eq!(claims.sub, user.id.to_string());
125 assert_eq!(claims.email, "alice@example.com");
126 }
127
128 #[test]
129 fn test_expired_token_returns_error() {
130 let user = test_user();
131 let config = test_config();
132
133 let now = Utc::now();
135 let claims = Claims {
136 sub: user.id.to_string(),
137 email: user.email.as_str().to_string(),
138 roles: vec![],
139 permissions: vec![],
140 exp: (now - Duration::hours(2)).timestamp(),
141 iat: (now - Duration::hours(3)).timestamp(),
142 jti: Uuid::now_v7().to_string(),
143 };
144 let key = EncodingKey::from_secret(config.secret.as_bytes());
145 let token =
146 encode(&Header::new(Algorithm::HS256), &claims, &key).expect("encode expired token");
147
148 let result = validate_token(&token, &config);
149 assert!(result.is_err(), "expired token must be rejected");
150 let err = result.unwrap_err().to_string();
151 assert!(err.contains("jwt"), "error should mention jwt");
152 }
153
154 #[test]
155 fn test_wrong_secret_returns_error() {
156 let user = test_user();
157 let config = test_config();
158 let token = generate_token(&user, &[], &[], &config).expect("generate token");
159
160 let wrong_config = JwtConfig::new("wrong-secret", Duration::hours(1));
161 let result = validate_token(&token, &wrong_config);
162 assert!(result.is_err(), "wrong secret must be rejected");
163 }
164
165 #[test]
166 fn test_claims_contain_roles_and_permissions() {
167 let user = test_user();
168 let roles = vec![RoleName::new("admin"), RoleName::new("editor")];
169 let permissions = vec![
170 PermissionName::new("read:posts"),
171 PermissionName::new("write:posts"),
172 ];
173 let config = test_config();
174
175 let token = generate_token(&user, &roles, &permissions, &config).expect("generate token");
176 let claims = validate_token(&token, &config).expect("validate token");
177
178 assert_eq!(claims.roles, vec!["admin", "editor"]);
179 assert_eq!(claims.permissions, vec!["read:posts", "write:posts"]);
180 }
181
182 #[test]
183 fn test_jti_is_unique_per_token() {
184 let user = test_user();
185 let config = test_config();
186
187 let token1 = generate_token(&user, &[], &[], &config).expect("generate token 1");
188 let token2 = generate_token(&user, &[], &[], &config).expect("generate token 2");
189
190 let claims1 = validate_token(&token1, &config).expect("validate token 1");
191 let claims2 = validate_token(&token2, &config).expect("validate token 2");
192
193 assert_ne!(
194 claims1.jti, claims2.jti,
195 "each token must have a unique jti"
196 );
197 }
198}