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 }
106 }
107
108 fn test_config() -> JwtConfig {
109 JwtConfig::new("test-secret-key-for-hs256", Duration::hours(1))
110 }
111
112 #[test]
113 fn test_generate_and_validate_round_trip() {
114 let user = test_user();
115 let roles = vec![RoleName::new("admin")];
116 let permissions = vec![PermissionName::new("read:posts")];
117 let config = test_config();
118
119 let token = generate_token(&user, &roles, &permissions, &config).expect("generate token");
120
121 let claims = validate_token(&token, &config).expect("validate token");
122
123 assert_eq!(claims.sub, user.id.to_string());
124 assert_eq!(claims.email, "alice@example.com");
125 }
126
127 #[test]
128 fn test_expired_token_returns_error() {
129 let user = test_user();
130 let config = test_config();
131
132 let now = Utc::now();
134 let claims = Claims {
135 sub: user.id.to_string(),
136 email: user.email.as_str().to_string(),
137 roles: vec![],
138 permissions: vec![],
139 exp: (now - Duration::hours(2)).timestamp(),
140 iat: (now - Duration::hours(3)).timestamp(),
141 jti: Uuid::now_v7().to_string(),
142 };
143 let key = EncodingKey::from_secret(config.secret.as_bytes());
144 let token =
145 encode(&Header::new(Algorithm::HS256), &claims, &key).expect("encode expired token");
146
147 let result = validate_token(&token, &config);
148 assert!(result.is_err(), "expired token must be rejected");
149 let err = result.unwrap_err().to_string();
150 assert!(err.contains("jwt"), "error should mention jwt");
151 }
152
153 #[test]
154 fn test_wrong_secret_returns_error() {
155 let user = test_user();
156 let config = test_config();
157 let token = generate_token(&user, &[], &[], &config).expect("generate token");
158
159 let wrong_config = JwtConfig::new("wrong-secret", Duration::hours(1));
160 let result = validate_token(&token, &wrong_config);
161 assert!(result.is_err(), "wrong secret must be rejected");
162 }
163
164 #[test]
165 fn test_claims_contain_roles_and_permissions() {
166 let user = test_user();
167 let roles = vec![RoleName::new("admin"), RoleName::new("editor")];
168 let permissions = vec![
169 PermissionName::new("read:posts"),
170 PermissionName::new("write:posts"),
171 ];
172 let config = test_config();
173
174 let token = generate_token(&user, &roles, &permissions, &config).expect("generate token");
175 let claims = validate_token(&token, &config).expect("validate token");
176
177 assert_eq!(claims.roles, vec!["admin", "editor"]);
178 assert_eq!(claims.permissions, vec!["read:posts", "write:posts"]);
179 }
180
181 #[test]
182 fn test_jti_is_unique_per_token() {
183 let user = test_user();
184 let config = test_config();
185
186 let token1 = generate_token(&user, &[], &[], &config).expect("generate token 1");
187 let token2 = generate_token(&user, &[], &[], &config).expect("generate token 2");
188
189 let claims1 = validate_token(&token1, &config).expect("validate token 1");
190 let claims2 = validate_token(&token2, &config).expect("validate token 2");
191
192 assert_ne!(
193 claims1.jti, claims2.jti,
194 "each token must have a unique jti"
195 );
196 }
197}