Skip to main content

api_tools/server/axum/security/jwt/
mod.rs

1//! JWT module
2
3pub mod access_token;
4pub mod payload;
5
6use crate::server::axum::response::ApiError;
7use crate::server::axum::security::jwt::access_token::AccessToken;
8use crate::value_objects::datetime::UtcDateTime;
9use jsonwebtoken::errors::ErrorKind::ExpiredSignature;
10use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation, decode, encode};
11use serde::{Deserialize, Serialize};
12use std::fmt::{Debug, Formatter};
13use thiserror::Error;
14
15const JWT_ACCESS_LIFETIME_IN_MINUTES: i64 = 15; // 15 minutes
16const JWT_REFRESH_LIFETIME_IN_HOURS: i64 = 7 * 24; // 7 days
17
18/// JWT errors
19#[derive(Debug, Clone, PartialEq, Error)]
20pub enum JwtError {
21    #[error("Parse token error: {0}")]
22    ParseError(String),
23
24    #[error("Generate token error: {0}")]
25    GenerateError(String),
26
27    #[error("Invalid or unsupported algorithm: {0}")]
28    InvalidAlgorithm(String),
29
30    #[error("Encoding key error: {0}")]
31    EncodingKeyError(String),
32
33    #[error("Decoding key error: {0}")]
34    DecodingKeyError(String),
35
36    #[error("Expired token")]
37    ExpiredToken,
38}
39
40/// JWT error
41impl From<JwtError> for ApiError {
42    fn from(value: JwtError) -> Self {
43        Self::InternalServerError(value.to_string())
44    }
45}
46
47/// JWT representation
48#[derive(Clone)]
49pub struct Jwt {
50    /// The algorithm supported for signing/verifying JWT
51    algorithm: Algorithm,
52
53    /// Access Token lifetime (in minute)
54    /// The default value is 15 minutes.
55    access_lifetime: i64,
56
57    /// Refresh Token lifetime (in hour)
58    /// The default value is 7 days.
59    refresh_lifetime: i64,
60
61    /// Encoding key
62    encoding_key: Option<EncodingKey>,
63
64    /// Decoding key
65    decoding_key: Option<DecodingKey>,
66}
67
68impl Default for Jwt {
69    fn default() -> Self {
70        Self {
71            algorithm: Algorithm::HS512,
72            access_lifetime: JWT_ACCESS_LIFETIME_IN_MINUTES,
73            refresh_lifetime: JWT_REFRESH_LIFETIME_IN_HOURS,
74            encoding_key: None,
75            decoding_key: None,
76        }
77    }
78}
79
80impl Debug for Jwt {
81    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82        write!(
83            f,
84            "JWT => algo: {:?}, access_lifetime: {}, refresh_lifetime: {}",
85            self.algorithm, self.access_lifetime, self.refresh_lifetime
86        )
87    }
88}
89
90impl Jwt {
91    /// Initialize a new `Jwt`
92    pub fn init(
93        algorithm: &str,
94        access_lifetime: i64,
95        refresh_lifetime: i64,
96        secret: Option<&str>,
97        private_key: Option<&str>,
98        public_key: Option<&str>,
99    ) -> Result<Self, JwtError> {
100        let mut jwt = Jwt {
101            algorithm: Self::algorithm_from_str(algorithm)?,
102            access_lifetime,
103            refresh_lifetime,
104            ..Default::default()
105        };
106
107        // Encoding key
108        match (secret, private_key, jwt.use_secret()) {
109            (Some(secret), _, true) => jwt.set_encoding_key(secret.trim())?,
110            (_, Some(private_key), false) => jwt.set_encoding_key(private_key.trim())?,
111            _ => return Err(JwtError::EncodingKeyError("invalid JWT encoding key".to_owned())),
112        }
113
114        // Decoding key
115        match (secret, public_key, jwt.use_secret()) {
116            (Some(secret), _, true) => jwt.set_decoding_key(secret.trim())?,
117            (_, Some(public_key), false) => jwt.set_decoding_key(public_key.trim())?,
118            _ => return Err(JwtError::DecodingKeyError("invalid JWT decoding key".to_owned())),
119        }
120
121        Ok(jwt)
122    }
123
124    /// Get access token lifetime
125    pub fn access_lifetime(&self) -> i64 {
126        self.access_lifetime
127    }
128
129    /// Get refresh token lifetime
130    pub fn refresh_lifetime(&self) -> i64 {
131        self.refresh_lifetime
132    }
133
134    /// Update access token lifetime (in minute)
135    pub fn set_access_lifetime(&mut self, duration: i64) {
136        self.access_lifetime = duration;
137    }
138
139    /// Update refresh token lifetime (in day)
140    pub fn set_refresh_lifetime(&mut self, duration: i64) {
141        self.refresh_lifetime = duration;
142    }
143
144    /// Update encoding key
145    pub fn set_encoding_key(&mut self, secret: &str) -> Result<(), JwtError> {
146        let key = match self.algorithm {
147            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => EncodingKey::from_secret(secret.as_bytes()),
148            Algorithm::ES256 | Algorithm::ES384 => EncodingKey::from_ec_pem(secret.as_bytes())
149                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,
150            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => EncodingKey::from_rsa_pem(secret.as_bytes())
151                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,
152            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => EncodingKey::from_rsa_pem(secret.as_bytes())
153                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,
154            Algorithm::EdDSA => EncodingKey::from_ed_pem(secret.as_bytes())
155                .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?,
156        };
157
158        self.encoding_key = Some(key);
159
160        Ok(())
161    }
162
163    /// Update decoding key
164    pub fn set_decoding_key(&mut self, secret: &str) -> Result<(), JwtError> {
165        let key = match self.algorithm {
166            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => DecodingKey::from_secret(secret.as_bytes()),
167            Algorithm::ES256 | Algorithm::ES384 => DecodingKey::from_ec_pem(secret.as_bytes())
168                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,
169            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => DecodingKey::from_rsa_pem(secret.as_bytes())
170                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,
171            Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512 => DecodingKey::from_rsa_pem(secret.as_bytes())
172                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,
173            Algorithm::EdDSA => DecodingKey::from_ed_pem(secret.as_bytes())
174                .map_err(|err| JwtError::DecodingKeyError(err.to_string()))?,
175        };
176
177        self.decoding_key = Some(key);
178
179        Ok(())
180    }
181
182    /// Generate JWT
183    pub fn generate<P: Debug + Serialize>(&self, payload: P, expired_at: UtcDateTime) -> Result<AccessToken, JwtError> {
184        let header = jsonwebtoken::Header::new(self.algorithm);
185
186        match self.encoding_key.clone() {
187            Some(encoding_key) => {
188                let token = encode(&header, &payload, &encoding_key)
189                    .map_err(|err| JwtError::EncodingKeyError(err.to_string()))?;
190
191                Ok(AccessToken { token, expired_at })
192            }
193            _ => Err(JwtError::EncodingKeyError("empty key".to_owned())),
194        }
195    }
196
197    /// Parse JWT
198    pub fn parse<P: Clone + Debug + for<'de> Deserialize<'de>>(&self, token: &AccessToken) -> Result<P, JwtError> {
199        let validation = Validation::new(self.algorithm);
200
201        match self.decoding_key.clone() {
202            Some(decoding_key) => {
203                let token = decode::<P>(&token.token, &decoding_key, &validation).map_err(|err| match err.kind() {
204                    ExpiredSignature => JwtError::ExpiredToken,
205                    _ => JwtError::DecodingKeyError(err.to_string()),
206                })?;
207
208                Ok(token.claims)
209            }
210            _ => Err(JwtError::DecodingKeyError("empty key".to_owned())),
211        }
212    }
213
214    /// Return true if a secret key is used instead of a pair of keys
215    fn use_secret(&self) -> bool {
216        self.algorithm == Algorithm::HS256 || self.algorithm == Algorithm::HS384 || self.algorithm == Algorithm::HS512
217    }
218
219    /// Convert `&str` to `Algorithm`
220    fn algorithm_from_str(algo: &str) -> Result<Algorithm, JwtError> {
221        Ok(match algo {
222            "HS256" => Algorithm::HS256,
223            "HS384" => Algorithm::HS384,
224            "HS512" => Algorithm::HS512,
225            "ES256" => Algorithm::ES256,
226            "ES384" => Algorithm::ES384,
227            _ => {
228                return Err(JwtError::InvalidAlgorithm(algo.to_string()));
229            }
230        })
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_jwt_use_secret() {
240        let jwt = Jwt::default();
241        assert!(jwt.use_secret());
242
243        let mut jwt = Jwt {
244            algorithm: Algorithm::ES256,
245            ..Default::default()
246        };
247        assert!(!jwt.use_secret());
248
249        jwt.algorithm = Algorithm::HS256;
250        assert!(jwt.use_secret());
251    }
252
253    #[test]
254    fn test_jwt_algorithm_from_str() {
255        assert_eq!(Jwt::algorithm_from_str("HS256").unwrap(), Algorithm::HS256);
256        assert_eq!(Jwt::algorithm_from_str("HS384").unwrap(), Algorithm::HS384);
257        assert_eq!(Jwt::algorithm_from_str("HS512").unwrap(), Algorithm::HS512);
258        assert_eq!(Jwt::algorithm_from_str("ES256").unwrap(), Algorithm::ES256);
259        assert_eq!(Jwt::algorithm_from_str("ES384").unwrap(), Algorithm::ES384);
260
261        let invalid_algo = Jwt::algorithm_from_str("ES512");
262        assert!(invalid_algo.is_err());
263        if let Err(e) = invalid_algo {
264            assert_eq!(e, JwtError::InvalidAlgorithm("ES512".to_string()));
265        }
266    }
267
268    #[test]
269    fn test_jwt_default() {
270        let jwt = Jwt::default();
271        assert_eq!(jwt.algorithm, Algorithm::HS512);
272        assert_eq!(jwt.access_lifetime, JWT_ACCESS_LIFETIME_IN_MINUTES);
273        assert_eq!(jwt.refresh_lifetime, JWT_REFRESH_LIFETIME_IN_HOURS);
274        assert!(jwt.encoding_key.is_none());
275        assert!(jwt.decoding_key.is_none());
276    }
277
278    #[test]
279    fn test_jwt_debug() {
280        let jwt = Jwt::default();
281        let debug_str = format!("{:?}", jwt);
282
283        assert_eq!(
284            debug_str,
285            format!("JWT => algo: HS512, access_lifetime: 15, refresh_lifetime: {}", 7 * 24)
286        );
287    }
288
289    #[test]
290    fn test_jwt_use_secret_all_algorithms() {
291        for algo in [Algorithm::HS256, Algorithm::HS384, Algorithm::HS512] {
292            let jwt = Jwt {
293                algorithm: algo,
294                ..Default::default()
295            };
296            assert!(jwt.use_secret(), "{algo:?} must use secret");
297        }
298        for algo in [
299            Algorithm::ES256,
300            Algorithm::ES384,
301            Algorithm::RS256,
302            Algorithm::RS384,
303            Algorithm::RS512,
304            Algorithm::PS256,
305            Algorithm::PS384,
306            Algorithm::PS512,
307            Algorithm::EdDSA,
308        ] {
309            let jwt = Jwt {
310                algorithm: algo,
311                ..Default::default()
312            };
313            assert!(!jwt.use_secret(), "{algo:?} must use a key pair");
314        }
315    }
316
317    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318    struct TestClaims {
319        sub: String,
320        exp: i64,
321    }
322
323    fn future_exp(seconds: i64) -> i64 {
324        chrono::Utc::now().timestamp() + seconds
325    }
326
327    #[test]
328    fn test_jwt_init_and_round_trip_hs512() {
329        let jwt = Jwt::init("HS512", 15, 7 * 24, Some("super-secret"), None, None).expect("init");
330        let claims = TestClaims {
331            sub: "user-42".to_string(),
332            exp: future_exp(60),
333        };
334        let token = jwt.generate(claims.clone(), UtcDateTime::now()).expect("generate");
335        assert!(!token.token.is_empty());
336
337        let parsed: TestClaims = jwt.parse(&token).expect("parse");
338        assert_eq!(parsed, claims);
339    }
340
341    #[test]
342    fn test_jwt_parse_expired_token() {
343        let jwt = Jwt::init("HS256", 15, 7 * 24, Some("secret"), None, None).expect("init");
344        let claims = TestClaims {
345            sub: "user".to_string(),
346            // The default `Validation` leeway is 60 s, so push well past it.
347            exp: future_exp(-300),
348        };
349        let token = jwt.generate(claims, UtcDateTime::now()).expect("generate");
350
351        let err = jwt.parse::<TestClaims>(&token).unwrap_err();
352        assert_eq!(err, JwtError::ExpiredToken);
353    }
354
355    #[test]
356    fn test_jwt_init_invalid_algorithm() {
357        let err = Jwt::init("FOO", 15, 7 * 24, Some("secret"), None, None).unwrap_err();
358        assert_eq!(err, JwtError::InvalidAlgorithm("FOO".to_string()));
359    }
360
361    #[test]
362    fn test_jwt_init_hs256_missing_secret() {
363        let err = Jwt::init("HS256", 15, 7 * 24, None, None, None).unwrap_err();
364        assert!(matches!(err, JwtError::EncodingKeyError(_)));
365    }
366
367    #[test]
368    fn test_jwt_init_es256_with_secret_only_fails() {
369        // ES256 requires a key pair; passing only a secret must fail at the
370        // encoding-key stage.
371        let err = Jwt::init("ES256", 15, 7 * 24, Some("secret"), None, None).unwrap_err();
372        assert!(matches!(err, JwtError::EncodingKeyError(_)));
373    }
374
375    #[test]
376    fn test_jwt_generate_without_encoding_key() {
377        // Bypass `init` to construct a Jwt with no encoding key and check
378        // `generate` reports it cleanly rather than panicking.
379        let jwt = Jwt::default();
380        let claims = TestClaims {
381            sub: "user".to_string(),
382            exp: future_exp(60),
383        };
384        let err = jwt.generate(claims, UtcDateTime::now()).unwrap_err();
385        assert!(matches!(err, JwtError::EncodingKeyError(_)));
386    }
387
388    #[test]
389    fn jwt_error_into_api_error_maps_to_internal_server_error() {
390        let api_err: ApiError = JwtError::ExpiredToken.into();
391        assert!(matches!(api_err, ApiError::InternalServerError(_)));
392        // The Display payload of the JwtError is preserved.
393        let ApiError::InternalServerError(msg) = api_err else {
394            unreachable!()
395        };
396        assert_eq!(msg, "Expired token");
397    }
398
399    #[test]
400    fn lifetime_getters_and_setters_round_trip() {
401        let mut jwt = Jwt::default();
402        assert_eq!(jwt.access_lifetime(), JWT_ACCESS_LIFETIME_IN_MINUTES);
403        assert_eq!(jwt.refresh_lifetime(), JWT_REFRESH_LIFETIME_IN_HOURS);
404
405        jwt.set_access_lifetime(30);
406        jwt.set_refresh_lifetime(48);
407        assert_eq!(jwt.access_lifetime(), 30);
408        assert_eq!(jwt.refresh_lifetime(), 48);
409    }
410
411    /// Walks every non-HMAC algorithm to exercise each PEM-loading match arm
412    /// in `set_encoding_key` / `set_decoding_key`. Each call must surface a
413    /// typed error rather than panicking on garbage input.
414    #[test]
415    fn set_encoding_and_decoding_keys_reject_invalid_pem_for_each_algorithm() {
416        for algo in [
417            Algorithm::ES256,
418            Algorithm::ES384,
419            Algorithm::RS256,
420            Algorithm::RS384,
421            Algorithm::RS512,
422            Algorithm::PS256,
423            Algorithm::PS384,
424            Algorithm::PS512,
425            Algorithm::EdDSA,
426        ] {
427            let mut jwt = Jwt {
428                algorithm: algo,
429                ..Default::default()
430            };
431            assert!(
432                matches!(
433                    jwt.set_encoding_key("not-a-real-pem").unwrap_err(),
434                    JwtError::EncodingKeyError(_),
435                ),
436                "set_encoding_key({algo:?}) should report EncodingKeyError",
437            );
438            assert!(
439                matches!(
440                    jwt.set_decoding_key("not-a-real-pem").unwrap_err(),
441                    JwtError::DecodingKeyError(_),
442                ),
443                "set_decoding_key({algo:?}) should report DecodingKeyError",
444            );
445        }
446    }
447
448    #[test]
449    fn init_es256_with_invalid_private_key_fails_at_encoding_stage() {
450        // Drives the `(_, Some(private_key), false)` arm of the encoding-key
451        // match; the inner `set_encoding_key` then surfaces the PEM error.
452        let err = Jwt::init("ES256", 15, 7 * 24, None, Some("not-a-pem"), None).unwrap_err();
453        assert!(matches!(err, JwtError::EncodingKeyError(_)));
454    }
455
456    #[test]
457    fn parse_without_decoding_key_returns_decoding_error() {
458        // `Jwt::default()` has no decoding key; parse must fail cleanly.
459        let jwt = Jwt::default();
460        let dummy = AccessToken {
461            token: "irrelevant".to_string(),
462            expired_at: UtcDateTime::now(),
463        };
464        let err = jwt.parse::<TestClaims>(&dummy).unwrap_err();
465        assert!(matches!(err, JwtError::DecodingKeyError(_)));
466    }
467
468    #[test]
469    fn test_jwt_parse_with_wrong_secret_fails() {
470        let issuer = Jwt::init("HS256", 15, 7 * 24, Some("secret-A"), None, None).expect("init");
471        let verifier = Jwt::init("HS256", 15, 7 * 24, Some("secret-B"), None, None).expect("init");
472        let claims = TestClaims {
473            sub: "user".to_string(),
474            exp: future_exp(60),
475        };
476        let token = issuer.generate(claims, UtcDateTime::now()).expect("generate");
477
478        let err = verifier.parse::<TestClaims>(&token).unwrap_err();
479        assert!(matches!(err, JwtError::DecodingKeyError(_)));
480    }
481}