1use alloc::string::String;
4use alloy_primitives::hex;
5use core::{str::FromStr, time::Duration};
6use jsonwebtoken::get_current_timestamp;
7use rand::Rng;
8
9#[cfg(feature = "std")]
10use std::{
11 fs, io,
12 path::{Path, PathBuf},
13};
14
15#[cfg(feature = "serde")]
16use jsonwebtoken::{errors::ErrorKind, Algorithm, DecodingKey, Validation};
17
18#[derive(Debug, derive_more::Display)]
20pub enum JwtError {
21 #[display("{_0}")]
23 JwtSecretHexDecodeError(hex::FromHexError),
24
25 #[display("JWT key is expected to have a length of {_0} digits. {_1} digits key provided")]
27 InvalidLength(usize, usize),
28
29 #[display("unsupported signature algorithm. Only HS256 is supported")]
31 UnsupportedSignatureAlgorithm,
32
33 #[display("provided signature is invalid")]
35 InvalidSignature,
36
37 #[display("IAT (issued-at) claim is not within ±60 seconds from the current time")]
40 InvalidIssuanceTimestamp,
41
42 #[display("Authorization header is missing or invalid")]
44 MissingOrInvalidAuthorizationHeader,
45
46 #[display("JWT decoding error: {_0}")]
48 JwtDecodingError(String),
49
50 #[display("failed to create dir {path:?}: {source}")]
52 #[cfg(feature = "std")]
53 CreateDir {
54 source: io::Error,
56 path: PathBuf,
58 },
59
60 #[display("failed to read from {path:?}: {source}")]
62 #[cfg(feature = "std")]
63 Read {
64 source: io::Error,
66 path: PathBuf,
68 },
69
70 #[display("failed to write to {path:?}: {source}")]
72 #[cfg(feature = "std")]
73 Write {
74 source: io::Error,
76 path: PathBuf,
78 },
79}
80
81impl From<hex::FromHexError> for JwtError {
82 fn from(err: hex::FromHexError) -> Self {
83 Self::JwtSecretHexDecodeError(err)
84 }
85}
86
87#[cfg(feature = "std")]
88impl std::error::Error for JwtError {
89 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90 match self {
91 Self::JwtSecretHexDecodeError(err) => Some(err),
92 Self::CreateDir { source, .. }
93 | Self::Read { source, .. }
94 | Self::Write { source, .. } => Some(source),
95 _ => None,
96 }
97 }
98}
99
100const JWT_SECRET_LEN: usize = 64;
106
107const JWT_MAX_IAT_DIFF: Duration = Duration::from_secs(60);
109
110#[cfg(feature = "serde")]
112const JWT_SIGNATURE_ALGO: Algorithm = Algorithm::HS256;
113
114#[derive(Copy, Clone, Debug)]
123#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
124pub struct Claims {
125 pub iat: u64,
131 pub exp: Option<u64>,
134}
135
136impl Claims {
137 pub fn with_current_timestamp() -> Self {
139 Self { iat: get_current_timestamp(), exp: None }
140 }
141
142 pub fn is_within_time_window(&self) -> bool {
144 let now_secs = get_current_timestamp();
145 now_secs.abs_diff(self.iat) <= JWT_MAX_IAT_DIFF.as_secs()
146 }
147}
148
149impl Default for Claims {
150 fn default() -> Self {
152 Self::with_current_timestamp()
153 }
154}
155
156#[derive(Copy, Clone, PartialEq, Eq)]
164pub struct JwtSecret([u8; 32]);
165
166impl JwtSecret {
167 pub fn from_hex<S: AsRef<str>>(hex: S) -> Result<Self, JwtError> {
175 let hex = hex.as_ref().trim();
176 match hex::decode_to_array(hex) {
177 Ok(b) => Ok(Self(b)),
178 Err(hex::FromHexError::InvalidStringLength | hex::FromHexError::OddLength) => {
179 Err(JwtError::InvalidLength(JWT_SECRET_LEN, hex.len()))
180 }
181 Err(e) => Err(JwtError::JwtSecretHexDecodeError(e)),
182 }
183 }
184
185 #[cfg(feature = "std")]
189 pub fn from_file(fpath: &Path) -> Result<Self, JwtError> {
190 fs::read_to_string(fpath)
191 .map_err(|err| JwtError::Read { source: err, path: fpath.into() })
192 .and_then(Self::from_hex)
193 }
194
195 #[cfg(feature = "std")]
198 pub fn try_create_random(fpath: &Path) -> Result<Self, JwtError> {
199 if let Some(dir) = fpath.parent() {
200 fs::create_dir_all(dir)
202 .map_err(|err| JwtError::CreateDir { source: err, path: dir.into() })?
203 }
204
205 let secret = Self::random();
206 let bytes = &secret.0;
207 let hex = hex::encode(bytes);
208 fs::write(fpath, hex).map_err(|err| JwtError::Write { source: err, path: fpath.into() })?;
209 Ok(secret)
210 }
211
212 #[cfg(feature = "serde")]
220 pub fn validate(&self, jwt: &str) -> Result<(), JwtError> {
221 let mut validation = Validation::new(JWT_SIGNATURE_ALGO);
224 validation.set_required_spec_claims(&["iat"]);
225 let bytes = &self.0;
226
227 match jsonwebtoken::decode::<Claims>(jwt, &DecodingKey::from_secret(bytes), &validation) {
228 Ok(token) => {
229 if !token.claims.is_within_time_window() {
230 Err(JwtError::InvalidIssuanceTimestamp)?
231 }
232 }
233 Err(err) => match *err.kind() {
234 ErrorKind::InvalidSignature => Err(JwtError::InvalidSignature)?,
235 ErrorKind::InvalidAlgorithm => Err(JwtError::UnsupportedSignatureAlgorithm)?,
236 _ => {
237 let detail = format!("{err}");
238 Err(JwtError::JwtDecodingError(detail))?
239 }
240 },
241 };
242
243 Ok(())
244 }
245
246 pub fn random() -> Self {
248 Self(rand::thread_rng().gen())
249 }
250
251 #[cfg(feature = "serde")]
254 pub fn encode(&self, claims: &Claims) -> Result<String, jsonwebtoken::errors::Error> {
255 let bytes = &self.0;
256 let key = jsonwebtoken::EncodingKey::from_secret(bytes);
257 let algo = jsonwebtoken::Header::new(Algorithm::HS256);
258 jsonwebtoken::encode(&algo, claims, &key)
259 }
260
261 pub const fn as_bytes(&self) -> &[u8] {
263 &self.0
264 }
265}
266
267impl core::fmt::Debug for JwtSecret {
268 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
269 f.debug_tuple("JwtSecretHash").field(&"{{}}").finish()
270 }
271}
272
273impl FromStr for JwtSecret {
274 type Err = JwtError;
275
276 fn from_str(s: &str) -> Result<Self, Self::Err> {
277 Self::from_hex(s)
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use assert_matches::assert_matches;
285 use jsonwebtoken::{encode, EncodingKey, Header};
286 use similar_asserts::assert_eq;
287 #[cfg(feature = "std")]
288 use std::time::{Duration, SystemTime, UNIX_EPOCH};
289 use tempfile::tempdir;
290
291 #[test]
292 fn from_hex() {
293 let key = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430";
294 let secret_0: Result<JwtSecret, _> = JwtSecret::from_hex(key);
295 assert!(secret_0.is_ok());
296
297 let key = "0xf79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430";
298 let secret_1: Result<JwtSecret, _> = JwtSecret::from_hex(key);
299 assert!(secret_1.is_ok());
300
301 let key = "0xf79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430 ";
302 let secret_2: Result<JwtSecret, _> = JwtSecret::from_hex(key);
303 assert!(secret_2.is_ok());
304
305 assert_eq!(secret_0.as_ref().unwrap().clone(), secret_1.unwrap());
306 assert_eq!(secret_0.unwrap(), secret_2.unwrap());
307 }
308
309 #[test]
310 fn original_key_integrity_across_transformations() {
311 let original = "f79ae8046bc11c9927afe911db7143c51a806c4a537cc08e0d37140b0192f430";
312 let secret = JwtSecret::from_hex(original).unwrap();
313 let bytes = &secret.0;
314 let computed = hex::encode(bytes);
315 assert_eq!(original, computed);
316 }
317
318 #[test]
319 fn secret_has_64_hex_digits() {
320 let expected_len = 64;
321 let secret = JwtSecret::random();
322 let hex = hex::encode(secret.0);
323 assert_eq!(hex.len(), expected_len);
324 }
325
326 #[test]
327 fn creation_ok_hex_string_with_0x() {
328 let hex: String =
329 "0x7365637265747365637265747365637265747365637265747365637265747365".into();
330 let result = JwtSecret::from_hex(hex);
331 assert!(result.is_ok());
332 }
333
334 #[test]
335 fn creation_error_wrong_len() {
336 let hex = "f79ae8046";
337 let result = JwtSecret::from_hex(hex);
338 assert!(matches!(result, Err(JwtError::InvalidLength(_, _))));
339 }
340
341 #[test]
342 fn creation_error_wrong_hex_string() {
343 let hex: String = "This__________Is__________Not_______An____Hex_____________String".into();
344 let result = JwtSecret::from_hex(hex);
345 assert!(matches!(result, Err(JwtError::JwtSecretHexDecodeError(_))));
346 }
347
348 #[test]
349 #[cfg(feature = "serde")]
350 fn validation_ok() {
351 let secret = JwtSecret::random();
352 let claims = Claims { iat: get_current_timestamp(), exp: Some(10000000000) };
353 let jwt = secret.encode(&claims).unwrap();
354
355 let result = secret.validate(&jwt);
356
357 assert!(matches!(result, Ok(())));
358 }
359
360 #[test]
361 #[cfg(feature = "serde")]
362 fn validation_with_current_time_ok() {
363 let secret = JwtSecret::random();
364 let claims = Claims::default();
365 let jwt = secret.encode(&claims).unwrap();
366
367 let result = secret.validate(&jwt);
368
369 assert!(matches!(result, Ok(())));
370 }
371
372 #[test]
373 #[cfg(all(feature = "std", feature = "serde"))]
374 fn validation_error_iat_out_of_window() {
375 let secret = JwtSecret::random();
376
377 let offset = Duration::from_secs(JWT_MAX_IAT_DIFF.as_secs() + 10);
380 let out_of_window_time = SystemTime::now().checked_sub(offset).unwrap();
381 let claims = Claims { iat: to_u64(out_of_window_time), exp: Some(10000000000) };
382 let jwt = secret.encode(&claims).unwrap();
383
384 let result = secret.validate(&jwt);
385
386 assert!(matches!(result, Err(JwtError::InvalidIssuanceTimestamp)));
387
388 let offset = Duration::from_secs(JWT_MAX_IAT_DIFF.as_secs() + 10);
391 let out_of_window_time = SystemTime::now().checked_add(offset).unwrap();
392 let claims = Claims { iat: to_u64(out_of_window_time), exp: Some(10000000000) };
393 let jwt = secret.encode(&claims).unwrap();
394
395 let result = secret.validate(&jwt);
396
397 assert!(matches!(result, Err(JwtError::InvalidIssuanceTimestamp)));
398 }
399
400 #[test]
401 #[cfg(feature = "serde")]
402 fn validation_error_exp_expired() {
403 let secret = JwtSecret::random();
404 let claims = Claims { iat: get_current_timestamp(), exp: Some(1) };
405 let jwt = secret.encode(&claims).unwrap();
406
407 let result = secret.validate(&jwt);
408
409 assert!(matches!(result, Err(JwtError::JwtDecodingError(_))));
410 }
411
412 #[test]
413 #[cfg(feature = "serde")]
414 fn validation_error_wrong_signature() {
415 let secret_1 = JwtSecret::random();
416 let claims = Claims { iat: get_current_timestamp(), exp: Some(10000000000) };
417 let jwt = secret_1.encode(&claims).unwrap();
418
419 let secret_2 = JwtSecret::random();
421 let result = secret_2.validate(&jwt);
422 assert!(matches!(result, Err(JwtError::InvalidSignature)));
423 }
424
425 #[test]
426 #[cfg(feature = "serde")]
427 fn validation_error_unsupported_algorithm() {
428 let secret = JwtSecret::random();
429 let bytes = &secret.0;
430
431 let key = EncodingKey::from_secret(bytes);
432 let unsupported_algo = Header::new(Algorithm::HS384);
433
434 let claims = Claims { iat: get_current_timestamp(), exp: Some(10000000000) };
435 let jwt = encode(&unsupported_algo, &claims, &key).unwrap();
436 let result = secret.validate(&jwt);
437
438 assert!(matches!(result, Err(JwtError::UnsupportedSignatureAlgorithm)));
439 }
440
441 #[test]
442 #[cfg(feature = "serde")]
443 fn valid_without_exp_claim() {
444 let secret = JwtSecret::random();
445
446 let claims = Claims { iat: get_current_timestamp(), exp: None };
447 let jwt = secret.encode(&claims).unwrap();
448
449 let result = secret.validate(&jwt);
450
451 assert!(matches!(result, Ok(())));
452 }
453
454 #[test]
455 #[cfg(feature = "std")]
456 fn ephemeral_secret_created() {
457 let fpath: &Path = Path::new("secret0.hex");
458 assert!(fs::metadata(fpath).is_err());
459 JwtSecret::try_create_random(fpath).expect("A secret file should be created");
460 assert!(fs::metadata(fpath).is_ok());
461 fs::remove_file(fpath).unwrap();
462 }
463
464 #[test]
465 #[cfg(feature = "std")]
466 fn valid_secret_provided() {
467 let fpath = Path::new("secret1.hex");
468 assert!(fs::metadata(fpath).is_err());
469
470 let secret = JwtSecret::random();
471 fs::write(fpath, hex(&secret)).unwrap();
472
473 match JwtSecret::from_file(fpath) {
474 Ok(gen_secret) => {
475 fs::remove_file(fpath).unwrap();
476 assert_eq!(hex(&gen_secret), hex(&secret));
477 }
478 Err(_) => {
479 fs::remove_file(fpath).unwrap();
480 }
481 }
482 }
483
484 #[test]
485 #[cfg(feature = "std")]
486 fn invalid_hex_provided() {
487 let fpath = Path::new("secret2.hex");
488 fs::write(fpath, "invalid hex").unwrap();
489 let result = JwtSecret::from_file(fpath);
490 assert!(result.is_err());
491 fs::remove_file(fpath).unwrap();
492 }
493
494 #[test]
495 #[cfg(feature = "std")]
496 fn provided_file_not_exists() {
497 let fpath = Path::new("secret3.hex");
498 let result = JwtSecret::from_file(fpath);
499 assert_matches!(result, Err(JwtError::Read {source: _,path }) if path == fpath.to_path_buf());
500 assert!(fs::metadata(fpath).is_err());
501 }
502
503 #[test]
504 #[cfg(feature = "std")]
505 fn provided_file_is_a_directory() {
506 let dir = tempdir().unwrap();
507 let result = JwtSecret::from_file(dir.path());
508 assert_matches!(result, Err(JwtError::Read {source: _,path}) if path == dir.path());
509 }
510
511 #[cfg(feature = "std")]
512 fn to_u64(time: SystemTime) -> u64 {
513 time.duration_since(UNIX_EPOCH).unwrap().as_secs()
514 }
515
516 fn hex(secret: &JwtSecret) -> String {
517 hex::encode(secret.0)
518 }
519}