lmrc_http_common/auth/
jwt.rs1use chrono::{Duration, Utc};
4use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum JwtError {
11 #[error("Failed to create token: {0}")]
12 CreateError(String),
13
14 #[error("Invalid token: {0}")]
15 InvalidToken(String),
16
17 #[error("Token expired")]
18 Expired,
19
20 #[error("Missing claim: {0}")]
21 MissingClaim(String),
22}
23
24#[derive(Debug, Clone)]
26pub struct JwtConfig {
27 pub secret: String,
29 pub expiration_seconds: i64,
31 pub issuer: Option<String>,
33 pub audience: Option<String>,
35}
36
37impl JwtConfig {
38 pub fn new(secret: impl Into<String>, expiration_seconds: i64) -> Self {
39 Self {
40 secret: secret.into(),
41 expiration_seconds,
42 issuer: None,
43 audience: None,
44 }
45 }
46
47 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
48 self.issuer = Some(issuer.into());
49 self
50 }
51
52 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
53 self.audience = Some(audience.into());
54 self
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct JwtClaims {
61 pub sub: String,
63 pub iat: i64,
65 pub exp: i64,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub iss: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub aud: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub email: Option<String>,
76 #[serde(flatten)]
78 pub custom: serde_json::Map<String, serde_json::Value>,
79}
80
81impl JwtClaims {
82 pub fn new(subject: impl Into<String>, expires_in: Duration) -> Self {
84 let now = Utc::now();
85 Self {
86 sub: subject.into(),
87 iat: now.timestamp(),
88 exp: (now + expires_in).timestamp(),
89 iss: None,
90 aud: None,
91 email: None,
92 custom: serde_json::Map::new(),
93 }
94 }
95
96 pub fn with_email(mut self, email: impl Into<String>) -> Self {
97 self.email = Some(email.into());
98 self
99 }
100
101 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
102 self.iss = Some(issuer.into());
103 self
104 }
105
106 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
107 self.aud = Some(audience.into());
108 self
109 }
110
111 pub fn add_custom_claim(
112 mut self,
113 key: impl Into<String>,
114 value: impl Into<serde_json::Value>,
115 ) -> Self {
116 self.custom.insert(key.into(), value.into());
117 self
118 }
119
120 pub fn is_expired(&self) -> bool {
122 Utc::now().timestamp() > self.exp
123 }
124}
125
126pub fn create_token(claims: &JwtClaims, config: &JwtConfig) -> Result<String, JwtError> {
144 encode(
145 &Header::default(),
146 claims,
147 &EncodingKey::from_secret(config.secret.as_bytes()),
148 )
149 .map_err(|e| JwtError::CreateError(e.to_string()))
150}
151
152pub fn verify_token(token: &str, config: &JwtConfig) -> Result<JwtClaims, JwtError> {
172 let mut validation = Validation::default();
173
174 if let Some(ref iss) = config.issuer {
175 validation.set_issuer(&[iss]);
176 }
177
178 if let Some(ref aud) = config.audience {
179 validation.set_audience(&[aud]);
180 }
181
182 let token_data = decode::<JwtClaims>(
183 token,
184 &DecodingKey::from_secret(config.secret.as_bytes()),
185 &validation,
186 )
187 .map_err(|e| {
188 if e.to_string().contains("ExpiredSignature") {
189 JwtError::Expired
190 } else {
191 JwtError::InvalidToken(e.to_string())
192 }
193 })?;
194
195 Ok(token_data.claims)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_create_and_verify_token() {
204 let config = JwtConfig::new("test_secret", 3600);
205 let claims = JwtClaims::new("user_123", Duration::hours(1))
206 .with_email("test@example.com");
207
208 let token = create_token(&claims, &config).unwrap();
209 let decoded = verify_token(&token, &config).unwrap();
210
211 assert_eq!(decoded.sub, "user_123");
212 assert_eq!(decoded.email, Some("test@example.com".to_string()));
213 }
214
215 #[test]
216 fn test_expired_token() {
217 let config = JwtConfig::new("test_secret", 3600);
218 let mut claims = JwtClaims::new("user_123", Duration::hours(1));
219 claims.exp = Utc::now().timestamp() - 100;
221
222 let token = create_token(&claims, &config).unwrap();
223 let result = verify_token(&token, &config);
224
225 assert!(matches!(result, Err(JwtError::Expired)));
226 }
227
228 #[test]
229 fn test_invalid_secret() {
230 let config = JwtConfig::new("test_secret", 3600);
231 let claims = JwtClaims::new("user_123", Duration::hours(1));
232
233 let token = create_token(&claims, &config).unwrap();
234
235 let wrong_config = JwtConfig::new("wrong_secret", 3600);
236 let result = verify_token(&token, &wrong_config);
237
238 assert!(matches!(result, Err(JwtError::InvalidToken(_))));
239 }
240}