1use chrono::{DateTime, Duration, Utc};
2use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Claims {
8 pub sub: String,
10 pub iss: String,
12 pub aud: String,
14 pub iat: u64,
16 pub exp: u64,
18}
19
20#[derive(Debug, Clone)]
22pub struct JwtConfig {
23 pub secret: String,
24 pub issuer: String,
25 pub audience: String,
26 pub ttl_hours: u32,
27}
28
29pub fn create_token(user_id: &str, config: &JwtConfig, now: DateTime<Utc>) -> String {
34 let iat = now.timestamp() as u64;
35 let exp = (now + Duration::hours(i64::from(config.ttl_hours))).timestamp() as u64;
36 let claims = Claims {
37 sub: user_id.to_owned(),
38 iss: config.issuer.clone(),
39 aud: config.audience.clone(),
40 iat,
41 exp,
42 };
43
44 encode(
45 &Header::default(),
46 &claims,
47 &EncodingKey::from_secret(config.secret.as_bytes()),
48 )
49 .expect("JWT encoding is infallible with HMAC-SHA256")
50}
51
52pub fn verify_token(token: &str, config: &JwtConfig) -> Option<Claims> {
56 let mut validation = Validation::default();
57 validation.set_issuer(&[&config.issuer]);
58 validation.set_audience(&[&config.audience]);
59
60 decode::<Claims>(
61 token,
62 &DecodingKey::from_secret(config.secret.as_bytes()),
63 &validation,
64 )
65 .map(|data| data.claims)
66 .ok()
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 fn test_config() -> JwtConfig {
74 JwtConfig {
75 secret: "test-secret-key-for-jwt-validation".to_owned(),
76 issuer: "lenso".to_owned(),
77 audience: "lenso".to_owned(),
78 ttl_hours: 1,
79 }
80 }
81
82 #[test]
83 fn roundtrip_jwt_token() {
84 let config = test_config();
85 let now = Utc::now();
86 let token = create_token("usr_abc123", &config, now);
87
88 let claims = verify_token(&token, &config).expect("token should verify");
89 assert_eq!(claims.sub, "usr_abc123");
90 assert_eq!(claims.iss, "lenso");
91 assert_eq!(claims.aud, "lenso");
92 }
93
94 #[test]
95 fn rejects_wrong_secret() {
96 let config = test_config();
97 let now = Utc::now();
98 let token = create_token("usr_abc123", &config, now);
99
100 let mut wrong_config = config.clone();
101 wrong_config.secret = "wrong-secret".to_owned();
102
103 assert!(verify_token(&token, &wrong_config).is_none());
104 }
105
106 #[test]
107 fn rejects_expired_token() {
108 let config = test_config();
109 let past = Utc::now() - Duration::hours(2);
110 let token = create_token("usr_abc123", &config, past);
111
112 assert!(verify_token(&token, &config).is_none());
113 }
114
115 #[test]
116 fn rejects_wrong_audience() {
117 let config = test_config();
118 let now = Utc::now();
119 let token = create_token("usr_abc123", &config, now);
120
121 let mut wrong_config = config.clone();
122 wrong_config.audience = "wrong-audience".to_owned();
123
124 assert!(verify_token(&token, &wrong_config).is_none());
125 }
126}