1use chrono::{Duration, Utc};
2use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Claims {
8 pub sub: String,
10 pub role: String,
12 pub iat: i64,
14 pub exp: i64,
16 #[serde(default = "default_token_type")]
18 pub token_type: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub tenant_id: Option<String>,
22}
23
24fn default_token_type() -> String {
25 "access".to_string()
26}
27
28#[derive(Debug, Clone)]
30pub struct JwtConfig {
31 secret: Vec<u8>,
33 pub access_ttl: Duration,
35 pub refresh_ttl: Duration,
37}
38
39impl JwtConfig {
40 pub fn new(secret: &str, access_ttl_secs: i64, refresh_ttl_secs: i64) -> Self {
45 Self {
46 secret: secret.as_bytes().to_vec(),
47 access_ttl: Duration::seconds(access_ttl_secs),
48 refresh_ttl: Duration::seconds(refresh_ttl_secs),
49 }
50 }
51
52 pub fn from_env() -> Option<Self> {
56 let secret = std::env::var("JWT_SECRET").ok()?;
57 if secret.is_empty() {
58 return None;
59 }
60 Some(Self::new(&secret, 86400, 2_592_000))
62 }
63
64 pub fn encode_access(
66 &self,
67 user_id: &str,
68 role: &str,
69 ) -> Result<String, jsonwebtoken::errors::Error> {
70 self.encode_access_with_tenant(user_id, role, None)
71 }
72
73 pub fn encode_access_with_tenant(
75 &self,
76 user_id: &str,
77 role: &str,
78 tenant_id: Option<&str>,
79 ) -> Result<String, jsonwebtoken::errors::Error> {
80 let now = Utc::now();
81 let claims = Claims {
82 sub: user_id.to_string(),
83 role: role.to_string(),
84 iat: now.timestamp(),
85 exp: (now + self.access_ttl).timestamp(),
86 token_type: "access".to_string(),
87 tenant_id: tenant_id.map(ToString::to_string),
88 };
89 encode(
90 &Header::default(),
91 &claims,
92 &EncodingKey::from_secret(&self.secret),
93 )
94 }
95
96 pub fn encode_refresh(
98 &self,
99 user_id: &str,
100 role: &str,
101 ) -> Result<String, jsonwebtoken::errors::Error> {
102 let now = Utc::now();
103 let claims = Claims {
104 sub: user_id.to_string(),
105 role: role.to_string(),
106 iat: now.timestamp(),
107 exp: (now + self.refresh_ttl).timestamp(),
108 token_type: "refresh".to_string(),
109 tenant_id: None,
110 };
111 encode(
112 &Header::default(),
113 &claims,
114 &EncodingKey::from_secret(&self.secret),
115 )
116 }
117
118 pub fn decode(&self, token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
120 let data = decode::<Claims>(
121 token,
122 &DecodingKey::from_secret(&self.secret),
123 &Validation::default(),
124 )?;
125 Ok(data.claims)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn test_config() -> JwtConfig {
134 JwtConfig::new("test-secret-key-at-least-32-bytes-long!", 3600, 86400)
135 }
136
137 #[test]
138 fn encode_decode_access_token() {
139 let cfg = test_config();
140 let token = cfg.encode_access("user-123", "admin").unwrap();
141 let claims = cfg.decode(&token).unwrap();
142 assert_eq!(claims.sub, "user-123");
143 assert_eq!(claims.role, "admin");
144 assert_eq!(claims.token_type, "access");
145 }
146
147 #[test]
148 fn encode_decode_refresh_token() {
149 let cfg = test_config();
150 let token = cfg.encode_refresh("user-456", "member").unwrap();
151 let claims = cfg.decode(&token).unwrap();
152 assert_eq!(claims.sub, "user-456");
153 assert_eq!(claims.role, "member");
154 assert_eq!(claims.token_type, "refresh");
155 }
156
157 #[test]
158 fn invalid_token_fails() {
159 let cfg = test_config();
160 let result = cfg.decode("garbage.token.here");
161 assert!(result.is_err());
162 }
163
164 #[test]
165 fn wrong_secret_fails() {
166 let cfg1 = test_config();
167 let cfg2 = JwtConfig::new("different-secret-key-also-long-enough!", 3600, 86400);
168 let token = cfg1.encode_access("user-123", "admin").unwrap();
169 let result = cfg2.decode(&token);
170 assert!(result.is_err());
171 }
172
173 #[test]
174 fn expired_token_fails() {
175 let cfg = JwtConfig::new("test-secret-key-at-least-32-bytes-long!", -120, -120);
176 let token = cfg.encode_access("user-123", "admin").unwrap();
177 let result = cfg.decode(&token);
178 assert!(result.is_err());
179 }
180
181 #[test]
182 fn encode_access_with_tenant_id() {
183 let cfg = test_config();
184 let token = cfg
185 .encode_access_with_tenant("user-123", "admin", Some("org-abc"))
186 .unwrap();
187 let claims = cfg.decode(&token).unwrap();
188 assert_eq!(claims.sub, "user-123");
189 assert_eq!(claims.role, "admin");
190 assert_eq!(claims.tenant_id.as_deref(), Some("org-abc"));
191 }
192
193 #[test]
194 fn encode_access_without_tenant_id() {
195 let cfg = test_config();
196 let token = cfg.encode_access("user-123", "admin").unwrap();
197 let claims = cfg.decode(&token).unwrap();
198 assert!(claims.tenant_id.is_none());
199 }
200}