1use anyhow::Result;
8use chrono::{DateTime, Utc};
9use serde::{Serialize, Deserialize};
10use std::sync::RwLock;
11use zeroize::Zeroize;
12use crate::traits::RefreshTokenClaims;
13
14#[derive(Debug, Serialize, Deserialize)]
16pub struct Claims {
17 pub sub: String,
18 pub exp: usize,
19 pub iat: usize,
20 pub iss: String,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JwtAlgorithm {
26 HS256,
28 RS256,
30}
31
32#[derive(Clone)]
34pub struct JwtKey {
35 pub kid: String,
37 pub secret: String,
39 pub created_at: DateTime<Utc>,
41 pub retired_at: Option<DateTime<Utc>>,
43}
44
45impl Drop for JwtKey {
46 fn drop(&mut self) {
47 self.secret.zeroize();
48 }
49}
50
51#[derive(Clone)]
53pub struct RsaKeyPair {
54 pub kid: String,
56 pub private_key_pem: String,
58 pub public_key_pem: String,
60 pub created_at: DateTime<Utc>,
62 pub retired_at: Option<DateTime<Utc>>,
64}
65
66impl Drop for RsaKeyPair {
67 fn drop(&mut self) {
68 self.private_key_pem.zeroize();
69 }
70}
71
72#[derive(Clone)]
74enum KeyMaterial {
75 Hmac(JwtKey),
76 Rsa(RsaKeyPair),
77}
78
79impl KeyMaterial {
80 fn kid(&self) -> &str {
81 match self {
82 KeyMaterial::Hmac(k) => &k.kid,
83 KeyMaterial::Rsa(k) => &k.kid,
84 }
85 }
86
87 fn retired_at(&self) -> Option<DateTime<Utc>> {
88 match self {
89 KeyMaterial::Hmac(k) => k.retired_at,
90 KeyMaterial::Rsa(k) => k.retired_at,
91 }
92 }
93
94 fn set_retired(&mut self) {
95 let now = Utc::now();
96 match self {
97 KeyMaterial::Hmac(k) => k.retired_at = Some(now),
98 KeyMaterial::Rsa(k) => k.retired_at = Some(now),
99 }
100 }
101
102 fn algorithm(&self) -> jsonwebtoken::Algorithm {
103 match self {
104 KeyMaterial::Hmac(_) => jsonwebtoken::Algorithm::HS256,
105 KeyMaterial::Rsa(_) => jsonwebtoken::Algorithm::RS256,
106 }
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct KeyRotationConfig {
113 pub grace_period: chrono::Duration,
115}
116
117impl Default for KeyRotationConfig {
118 fn default() -> Self {
119 Self {
120 grace_period: chrono::Duration::hours(24),
121 }
122 }
123}
124
125pub struct JwtService {
133 active_key: RwLock<KeyMaterial>,
134 retired_keys: RwLock<Vec<KeyMaterial>>,
135 rotation_config: KeyRotationConfig,
136}
137
138impl JwtService {
139 pub fn new(secret: &str) -> Self {
145 Self::with_rotation(secret, KeyRotationConfig::default())
146 }
147
148 pub fn with_rotation(secret: &str, config: KeyRotationConfig) -> Self {
150 let key = JwtKey {
151 kid: uuid::Uuid::new_v4().to_string(),
152 secret: secret.to_string(),
153 created_at: Utc::now(),
154 retired_at: None,
155 };
156
157 Self {
158 active_key: RwLock::new(KeyMaterial::Hmac(key)),
159 retired_keys: RwLock::new(Vec::new()),
160 rotation_config: config,
161 }
162 }
163
164 pub fn new_rs256(private_key_pem: &str, public_key_pem: &str) -> Result<Self> {
173 Self::with_rs256_rotation(private_key_pem, public_key_pem, KeyRotationConfig::default())
174 }
175
176 pub fn with_rs256_rotation(
181 private_key_pem: &str,
182 public_key_pem: &str,
183 config: KeyRotationConfig,
184 ) -> Result<Self> {
185 jsonwebtoken::EncodingKey::from_rsa_pem(private_key_pem.as_bytes())
187 .map_err(|e| anyhow::anyhow!("Invalid RSA private key PEM: {}", e))?;
188 jsonwebtoken::DecodingKey::from_rsa_pem(public_key_pem.as_bytes())
189 .map_err(|e| anyhow::anyhow!("Invalid RSA public key PEM: {}", e))?;
190
191 let key = RsaKeyPair {
192 kid: uuid::Uuid::new_v4().to_string(),
193 private_key_pem: private_key_pem.to_string(),
194 public_key_pem: public_key_pem.to_string(),
195 created_at: Utc::now(),
196 retired_at: None,
197 };
198
199 Ok(Self {
200 active_key: RwLock::new(KeyMaterial::Rsa(key)),
201 retired_keys: RwLock::new(Vec::new()),
202 rotation_config: config,
203 })
204 }
205
206 pub fn active_kid(&self) -> String {
212 self.active_key.read()
213 .unwrap_or_else(|e| e.into_inner())
214 .kid()
215 .to_string()
216 }
217
218 pub fn algorithm(&self) -> JwtAlgorithm {
220 match &*self.active_key.read().unwrap_or_else(|e| e.into_inner()) {
221 KeyMaterial::Hmac(_) => JwtAlgorithm::HS256,
222 KeyMaterial::Rsa(_) => JwtAlgorithm::RS256,
223 }
224 }
225
226 pub fn public_key_pem(&self) -> Option<String> {
228 match &*self.active_key.read().unwrap_or_else(|e| e.into_inner()) {
229 KeyMaterial::Rsa(k) => Some(k.public_key_pem.clone()),
230 KeyMaterial::Hmac(_) => None,
231 }
232 }
233
234 pub fn rotate_key(&self, new_secret: &str) -> Result<String> {
241 let new_key = KeyMaterial::Hmac(JwtKey {
242 kid: uuid::Uuid::new_v4().to_string(),
243 secret: new_secret.to_string(),
244 created_at: Utc::now(),
245 retired_at: None,
246 });
247 self.rotate_key_material(new_key)
248 }
249
250 pub fn rotate_rsa_key(&self, private_key_pem: &str, public_key_pem: &str) -> Result<String> {
253 jsonwebtoken::EncodingKey::from_rsa_pem(private_key_pem.as_bytes())
255 .map_err(|e| anyhow::anyhow!("Invalid RSA private key PEM for rotation: {}", e))?;
256 jsonwebtoken::DecodingKey::from_rsa_pem(public_key_pem.as_bytes())
257 .map_err(|e| anyhow::anyhow!("Invalid RSA public key PEM for rotation: {}", e))?;
258
259 let new_key = KeyMaterial::Rsa(RsaKeyPair {
260 kid: uuid::Uuid::new_v4().to_string(),
261 private_key_pem: private_key_pem.to_string(),
262 public_key_pem: public_key_pem.to_string(),
263 created_at: Utc::now(),
264 retired_at: None,
265 });
266 self.rotate_key_material(new_key)
267 }
268
269 fn rotate_key_material(&self, new_key: KeyMaterial) -> Result<String> {
271 let new_kid = new_key.kid().to_string();
272
273 let mut active = self.active_key.write()
274 .map_err(|_| anyhow::anyhow!("JWT active key lock poisoned"))?;
275 let mut old_key = new_key;
276 std::mem::swap(&mut *active, &mut old_key);
277
278 old_key.set_retired();
279
280 let mut retired = self.retired_keys.write()
281 .map_err(|_| anyhow::anyhow!("JWT retired keys lock poisoned"))?;
282 retired.push(old_key);
283 Self::prune_expired_keys(&mut retired, &self.rotation_config.grace_period);
284
285 tracing::info!(
286 event = "auth.key_rotated",
287 new_kid = %new_kid,
288 retired_keys_count = retired.len(),
289 "JWT signing key rotated"
290 );
291
292 Ok(new_kid)
293 }
294
295 #[tracing::instrument(skip_all, fields(sub = %claims.sub))]
301 pub fn create_token(&self, claims: &Claims) -> Result<String> {
302 let active = self.active_key.read()
303 .map_err(|_| anyhow::anyhow!("JWT key lock poisoned"))?;
304 let (header, encoding_key) = Self::make_encoding_parts(&active)?;
305
306 let token = jsonwebtoken::encode(&header, claims, &encoding_key)
307 .map_err(|e| {
308 tracing::error!(event = "auth.jwt_create_failed", "Failed to create JWT token");
309 anyhow::anyhow!("Failed to create JWT token: {}", e)
310 })?;
311
312 tracing::debug!(event = "auth.jwt_created", kid = %active.kid(), "JWT access token created");
313 Ok(token)
314 }
315
316 #[tracing::instrument(skip_all, fields(sub = %claims.sub))]
318 pub fn create_refresh_token(&self, claims: &RefreshTokenClaims) -> Result<String> {
319 let active = self.active_key.read()
320 .map_err(|_| anyhow::anyhow!("JWT key lock poisoned"))?;
321 let (header, encoding_key) = Self::make_encoding_parts(&active)?;
322
323 let token = jsonwebtoken::encode(&header, claims, &encoding_key)
324 .map_err(|e| {
325 tracing::error!(event = "auth.refresh_token_create_failed", "Failed to create refresh token");
326 anyhow::anyhow!("Failed to create refresh token: {}", e)
327 })?;
328
329 tracing::debug!(event = "auth.refresh_token_created", "Refresh token created");
330 Ok(token)
331 }
332
333 #[tracing::instrument(skip_all)]
339 pub fn validate_token(&self, token: &str) -> Result<Claims> {
340 self.validate_token_generic::<Claims>(token, None)
341 }
342
343 #[tracing::instrument(skip_all)]
345 pub fn decode_token(&self, token: &str) -> Result<Claims> {
346 let algorithm = self.active_key.read()
347 .map_err(|_| anyhow::anyhow!("JWT key lock poisoned"))?
348 .algorithm();
349 let mut validation = jsonwebtoken::Validation::new(algorithm);
350 validation.validate_exp = false;
351 validation.validate_nbf = false;
352 self.validate_token_generic::<Claims>(token, Some(validation))
353 }
354
355 #[tracing::instrument(skip_all)]
357 pub fn validate_refresh_token(&self, token: &str) -> Result<RefreshTokenClaims> {
358 let claims = self.validate_token_generic::<RefreshTokenClaims>(token, None)?;
359
360 if claims.token_type != "refresh" {
361 tracing::warn!(event = "auth.invalid_token_type", "Expected refresh token, got different type");
362 return Err(anyhow::anyhow!("Invalid token type: expected refresh token"));
363 }
364
365 tracing::debug!(
366 event = "auth.refresh_token_validated",
367 sub = %claims.sub,
368 "Refresh token validated"
369 );
370 Ok(claims)
371 }
372
373 fn make_encoding_parts(key: &KeyMaterial) -> Result<(jsonwebtoken::Header, jsonwebtoken::EncodingKey)> {
379 match key {
380 KeyMaterial::Hmac(k) => {
381 let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256);
382 header.kid = Some(k.kid.clone());
383 let encoding_key = jsonwebtoken::EncodingKey::from_secret(k.secret.as_ref());
384 Ok((header, encoding_key))
385 }
386 KeyMaterial::Rsa(k) => {
387 let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
388 header.kid = Some(k.kid.clone());
389 let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(k.private_key_pem.as_bytes())
390 .map_err(|e| anyhow::anyhow!("Invalid RSA private key: {}", e))?;
391 Ok((header, encoding_key))
392 }
393 }
394 }
395
396 fn validate_token_generic<T: serde::de::DeserializeOwned>(
398 &self,
399 token: &str,
400 custom_validation: Option<jsonwebtoken::Validation>,
401 ) -> Result<T> {
402 let algorithm = self.active_key.read()
403 .map_err(|_| anyhow::anyhow!("JWT key lock poisoned"))?
404 .algorithm();
405 let validation = custom_validation.unwrap_or_else(|| jsonwebtoken::Validation::new(algorithm));
406
407 let token_kid = Self::extract_kid(token);
409
410 let active = self.active_key.read()
412 .map_err(|_| anyhow::anyhow!("JWT key lock poisoned"))?;
413 if token_kid.as_ref().map_or(true, |kid| kid == active.kid()) {
414 if let Some(claims) = Self::try_validate_with_key::<T>(token, &active, &validation) {
415 return Ok(claims);
416 }
417 }
418 drop(active);
419
420 let mut retired = self.retired_keys.write()
422 .map_err(|_| anyhow::anyhow!("JWT retired keys lock poisoned"))?;
423 Self::prune_expired_keys(&mut retired, &self.rotation_config.grace_period);
424
425 for key in retired.iter() {
426 if token_kid.as_ref().map_or(true, |kid| kid == key.kid()) {
427 if let Some(claims) = Self::try_validate_with_key::<T>(token, key, &validation) {
428 tracing::debug!(
429 event = "auth.validated_with_retired_key",
430 kid = %key.kid(),
431 "Token validated with retired key"
432 );
433 return Ok(claims);
434 }
435 }
436 }
437
438 tracing::warn!(event = "auth.jwt_validation_failed", "JWT token validation failed");
439 Err(anyhow::anyhow!("Failed to validate JWT token"))
440 }
441
442 fn try_validate_with_key<T: serde::de::DeserializeOwned>(
444 token: &str,
445 key: &KeyMaterial,
446 validation: &jsonwebtoken::Validation,
447 ) -> Option<T> {
448 let decoding_key = match key {
449 KeyMaterial::Hmac(k) => jsonwebtoken::DecodingKey::from_secret(k.secret.as_ref()),
450 KeyMaterial::Rsa(k) => {
451 match jsonwebtoken::DecodingKey::from_rsa_pem(k.public_key_pem.as_bytes()) {
452 Ok(dk) => dk,
453 Err(e) => {
454 tracing::debug!(
455 event = "auth.rsa_key_parse_failed",
456 kid = %k.kid,
457 error = %e,
458 "Failed to parse RSA public key PEM during token validation"
459 );
460 return None;
461 }
462 }
463 }
464 };
465
466 match jsonwebtoken::decode::<T>(token, &decoding_key, validation) {
467 Ok(data) => Some(data.claims),
468 Err(e) => {
469 tracing::debug!(
470 event = "auth.token_decode_failed",
471 kid = %key.kid(),
472 error = %e,
473 "Token validation failed for key"
474 );
475 None
476 }
477 }
478 }
479
480 fn extract_kid(token: &str) -> Option<String> {
482 jsonwebtoken::decode_header(token)
483 .ok()
484 .and_then(|header| header.kid)
485 }
486
487 fn prune_expired_keys(retired: &mut Vec<KeyMaterial>, grace_period: &chrono::Duration) {
489 let now = Utc::now();
490 retired.retain(|key| {
491 if let Some(retired_at) = key.retired_at() {
492 now - retired_at < *grace_period
493 } else {
494 true
495 }
496 });
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 const TEST_RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
506MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDlctXcg7jOYn8J
507nklV8rCyEV2AMHqtFKJJz5CvJ5oL3KRegKxJygyBPfVhBdoG9kmNKdlSXEeWDwan
508BzQ/Vv4i3569uIY1Z35S+6nrlviTlm1rzQq7n8G0zMW7iYBj8QZp0p6XnBUh8UJI
509rzyo/m4NsQ3BPcu19ijdl5emH1MTGqUHrIdZJgU52SMp6XI6aqsUA92PUjBE6PtM
510P0JQXDOxEwX0I9gpwopwLOg+TyIWItZsos4HB7x1GEo7bytOCik7O7CqRKh+n705
511WkEZPll6apA7A3MXpAWq4TUsrrk+4lQzNaiCsUvFfcCmukm/J9BfoO2rKj67fJT8
512DBq3oroNAgMBAAECggEAG+XtE+1eLJX0TVaWIyGpk1UiMcJzQBU4sFHRDUL664NN
5135wGtMSGkiJhgfAYKnvsWMVhLyMRYnenAzNFG7IamytW2xumnQ9oMFYns/Ky0F7nc
514HxXkvrBrjJCzYByVZFF7jqVhzBxZw3FCtnS8Iu4gsoB7JCpf2QWPrXXPpg67+p/x
515Od7o2ylCQrS+Sbki7swSBkf3ID2CAwfOhiAl3KZz4Jy960Br/BBlp5xO67QAJ3D+
516LY0eAYNmeHi18m8KjWCBN8lDmk2qx470xc1tng5Wk12dkLNEkdwy7ePBy9SadENF
517sWRM9HEGddf8qHbkZtWUXI81pl1xSZKFxvjTKwumQQKBgQD/Nvg8NMGpS9WKBKR2
5181ko42HR8kn64zlIJqq5LOhkhISyDjXbVKQaX8Kb7G+5zPYxKDC3siGOcCBWKaVQF
519ArN/RcOIJhZD0u55FGlLC/j+OgCr+5tC9SEQIDw2FqAzAHUIrw9VeKsWPgzHolik
520x4qdIYgCxeMSPfMCYx1L4CGYLQKBgQDmJ5HpL6osKKUQ8kVlBnIeFQIzst3V3aAI
521rx496pNvoWfRSbPtahCPuZfNvw+mA1wbPe1adnvnoVrkFj8p8XqYJsSfdmgGiGTX
522uaHeR1toNUaSGyx0yPdkcHzQ9zwN92+oX2GSEEYD9kukRNuRmo9te8MJZbd/yz3O
523oD0D83n1YQKBgQCzsMFgqoh8KX+lGJWvcjt6ALUrjH2aovHSCpGDN9m/oLrVuQGl
524Haidy/vVq5ndG8Wt0Rt4gaMYlfyMopJcoMU+5CDCuIZOpLHxIDTuePSMEsysSo/L
525ugnYb8nVD2Ml5bmBLriuJjLXi0K2QJEHG9N1xMkdorS5AFMcKCrVZG5i1QKBgQDI
526JkdEs7fYmSwbVaU1mupo8LoufXFfiFGg27GABNxcqs/e+KppX+CxLKQwP+R66dcS
527tcMQ9ZMBN5gUXKhncGG9qZE6X71NWRXhaMS0yfda42HQs6LwmMhT52MUUr0+JB1N
528Hk16uX45+dmELIGJ2RC8FHHjXTq7/uJsK3uEURuRYQKBgHKvrd5M8pOoTnBx7nE5
529YXaBBAmeOYRKkczMF7ppj34u2UDcVEscEd0QNOPlPvoKmmEKi5sHeUdcj3sqMvj/
530xFr+oxoyZ0KF6+G2rOT7hjLpTuYSwqstJgApx52iqaNjkN7bPtQSk+QyXJJNEK0+
531bsxa1iX8xcbwZU2JO+Z2ZoT2
532-----END PRIVATE KEY-----";
533
534 const TEST_RSA_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----
535MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5XLV3IO4zmJ/CZ5JVfKw
536shFdgDB6rRSiSc+QryeaC9ykXoCsScoMgT31YQXaBvZJjSnZUlxHlg8Gpwc0P1b+
537It+evbiGNWd+Uvup65b4k5Zta80Ku5/BtMzFu4mAY/EGadKel5wVIfFCSK88qP5u
538DbENwT3LtfYo3ZeXph9TExqlB6yHWSYFOdkjKelyOmqrFAPdj1IwROj7TD9CUFwz
539sRMF9CPYKcKKcCzoPk8iFiLWbKLOBwe8dRhKO28rTgopOzuwqkSofp+9OVpBGT5Z
540emqQOwNzF6QFquE1LK65PuJUMzWogrFLxX3AprpJvyfQX6Dtqyo+u3yU/Awat6K6
541DQIDAQAB
542-----END PUBLIC KEY-----";
543
544 const TEST_RSA_PRIVATE_KEY_2: &str = "-----BEGIN PRIVATE KEY-----
546MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZ4IoevJ7RmSUo
547QmvDOsu/btA217fvnRWYzHQdhxhAf9B3wa8kgIEIRF40uZrsL/udxk8grbv/hhEb
548upWb1rouz9htyqLhMq+ti1PmFgeg4BvHsPlQhAggHEvUgmRWRdmwM8qiUbNOkVKT
549L/khqricMRyxra1+Tp43CXOsJtehIfvbOURa5HC0qWGAvHdIe820s+Z++AfD943C
5509IiZ5vRjtmaZ1JUMEh3Y2IspY5kWdlbgH5Ea8nkiUajwVyFgfUx/WyHHB65yUz6n
5515PxxR12Z3ucmtMRS2SxxSM229dxDbwDLKCsVlYNENSIfU/ZBFWThikvd3Hbz8uLl
552byarm2UrAgMBAAECggEAMlnfYZoSk/qx6Rtsfwoz8vIfgUUaF3B0gMLjJL3HP4Sq
553PzrOCIAAEdKG+OVZ5bJzEjO1rqYn17X6dy+ICqM1lMLoz/qv6J5HljIoOfimW6nf
554EaeW/mH85LrVVW+q03tCAyP89MUvzHzuGeDQ0NR85G+/I1qxSQrPKoXvKv4w/+YG
555/Iq170RHOBu3uo4YTrqQHi7k7k28NyGE0dOkD3nqATUDzhMk3VTGA7l9Mrvm0Pth
556coAsDmbrulODZG2qOmt0tK9wVBQTPy1IxJ+QgBlKMixGZFi/4BKX0XRQmn3sfdhk
5571qO1nn2UAPm1QNTA2wn+I5OtU5HZHZ+uFDZ+5fKmVQKBgQDvZ5usmAKR+OPaI2Pv
558Oboo0fB/Xh2nIYRk86UHPEWd8NvRG2e1lbRDYn5IxPbWWPtDv6ILhKPewz1b6mvw
559JZYjPWsS8mkT+Cejo11+bBs746ndhIb/gBWbnpfZBCY9wdvc+VWB1aQqwDb0TjsX
560xqWs5YtH7ajbpaLAPTxbG58vBQKBgQDo+uj2X+NS7LS9GikYqc6i68FdOFHUH+XW
561MSTlQBWwFOiC94rietxDTSkds4CjL0zfUnYO7cmTlK6ixYS3i8msGVw+VdenLOP/
562hdRSSkYy+n6fWwy+4o9fOsrzhMArnSdKAJVb7Mlaos+3z1J+5Z56HogQbwUJXCSV
563BcdtYamabwKBgBaK3/q5eYx7LiFNMczF18SeOBIWL56cJlZHJuPuhfOgSWKAPRy5
564EvdBX/jEKyX1zPsNIVoKTE/efHmaMj2znFaHIvzuvHw34qui51vPHCVgg48rOnb2
565fZJgtZWmsV8hUO2WwLlv/3xTCxmoACJ1/wWvu5SzSTIdf5ywZ22AxVVtAoGAfKLe
566Rg9+GTqwZgm8uoj9FoNw6mHaxNRbrH6V8l6aO5yz1nx/PDHl68s3l8ATrTj8suv2
567ZH4pPF5qHoH0QgzyUrMuedqKh9CoGGaL84nwjA0d+DpJU0T41kUplaUK+UoVXq15
568Obgu7+Hxpa+vvlswsLvspn39/8ffeimhSo7YoNkCgYEAkgLqzZ4bJTH7rfuodGkG
569M+oOYGxdLep3fg6DE1xwSiGkMqFkodGtR6LNK2Fc/6DIlho3M7VsQkdfDBeDhYjP
570krcc7GlWFVqoFLRP6DsNbRzW1bOCOAZLfOVq0AEPN+2VXvXmrqiRoBXN7cAwH/E6
571FO3LPObNpUirt6dAlT6Fy4o=
572-----END PRIVATE KEY-----";
573
574 const TEST_RSA_PUBLIC_KEY_2: &str = "-----BEGIN PUBLIC KEY-----
575MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2eCKHrye0ZklKEJrwzrL
576v27QNte3750VmMx0HYcYQH/Qd8GvJICBCEReNLma7C/7ncZPIK27/4YRG7qVm9a6
577Ls/Ybcqi4TKvrYtT5hYHoOAbx7D5UIQIIBxL1IJkVkXZsDPKolGzTpFSky/5Iaq4
578nDEcsa2tfk6eNwlzrCbXoSH72zlEWuRwtKlhgLx3SHvNtLPmfvgHw/eNwvSImeb0
579Y7ZmmdSVDBId2NiLKWOZFnZW4B+RGvJ5IlGo8FchYH1Mf1shxweuclM+p+T8cUdd
580md7nJrTEUtkscUjNtvXcQ28AyygrFZWDRDUiH1P2QRVk4YpL3dx28/Li5W8mq5tl
581KwIDAQAB
582-----END PUBLIC KEY-----";
583
584 fn make_test_claims() -> Claims {
585 let now = std::time::SystemTime::now()
586 .duration_since(std::time::UNIX_EPOCH)
587 .unwrap();
588 Claims {
589 sub: "user-123".to_string(),
590 exp: (now.as_secs() + 3600) as usize,
591 iat: now.as_secs() as usize,
592 iss: "backbone".to_string(),
593 }
594 }
595
596 fn make_test_refresh_claims() -> RefreshTokenClaims {
597 let now = std::time::SystemTime::now()
598 .duration_since(std::time::UNIX_EPOCH)
599 .unwrap();
600 RefreshTokenClaims {
601 sub: "user-123".to_string(),
602 exp: (now.as_secs() + 3600) as usize,
603 iat: now.as_secs() as usize,
604 iss: "backbone".to_string(),
605 token_type: "refresh".to_string(),
606 }
607 }
608
609 #[test]
614 fn test_single_key_backward_compat() {
615 let service = JwtService::new("test_secret_key");
616 let claims = make_test_claims();
617
618 let token = service.create_token(&claims).unwrap();
619 let validated = service.validate_token(&token).unwrap();
620
621 assert_eq!(validated.sub, "user-123");
622 assert_eq!(validated.iss, "backbone");
623 }
624
625 #[test]
626 fn test_token_has_kid_header() {
627 let service = JwtService::new("test_secret_key");
628 let claims = make_test_claims();
629
630 let token = service.create_token(&claims).unwrap();
631
632 let header = jsonwebtoken::decode_header(&token).unwrap();
633 assert!(header.kid.is_some());
634 assert_eq!(header.kid.unwrap(), service.active_kid());
635 }
636
637 #[test]
638 fn test_rotate_key_old_token_valid() {
639 let service = JwtService::new("original_secret");
640 let claims = make_test_claims();
641
642 let old_token = service.create_token(&claims).unwrap();
644
645 service.rotate_key("new_secret_after_rotation").unwrap();
647
648 let validated = service.validate_token(&old_token).unwrap();
650 assert_eq!(validated.sub, "user-123");
651 }
652
653 #[test]
654 fn test_rotate_key_new_token_valid() {
655 let service = JwtService::new("original_secret");
656
657 service.rotate_key("new_secret_after_rotation").unwrap();
659
660 let claims = make_test_claims();
662 let new_token = service.create_token(&claims).unwrap();
663 let validated = service.validate_token(&new_token).unwrap();
664 assert_eq!(validated.sub, "user-123");
665 }
666
667 #[test]
668 fn test_grace_period_expiry() {
669 let config = KeyRotationConfig {
671 grace_period: chrono::Duration::zero(),
672 };
673 let service = JwtService::with_rotation("original_secret", config);
674 let claims = make_test_claims();
675
676 let old_token = service.create_token(&claims).unwrap();
677
678 service.rotate_key("new_secret").unwrap();
680
681 let result = service.validate_token(&old_token);
683 assert!(result.is_err());
684 }
685
686 #[test]
687 fn test_backward_compat_no_kid() {
688 use jsonwebtoken::{encode, EncodingKey, Header};
690
691 let secret = "test_secret_key";
692 let service = JwtService::new(secret);
693
694 let claims = make_test_claims();
696 let header = Header::default(); let token = encode(&header, &claims, &EncodingKey::from_secret(secret.as_ref())).unwrap();
698
699 let validated = service.validate_token(&token).unwrap();
701 assert_eq!(validated.sub, "user-123");
702 }
703
704 #[test]
705 fn test_multiple_rotations() {
706 let config = KeyRotationConfig {
707 grace_period: chrono::Duration::hours(24),
708 };
709 let service = JwtService::with_rotation("secret_v1", config);
710
711 let claims = make_test_claims();
712 let token_v1 = service.create_token(&claims).unwrap();
713
714 service.rotate_key("secret_v2").unwrap();
715 let token_v2 = service.create_token(&claims).unwrap();
716
717 service.rotate_key("secret_v3").unwrap();
718 let token_v3 = service.create_token(&claims).unwrap();
719
720 assert!(service.validate_token(&token_v1).is_ok());
722 assert!(service.validate_token(&token_v2).is_ok());
723 assert!(service.validate_token(&token_v3).is_ok());
724 }
725
726 #[test]
727 fn test_refresh_token_rotation() {
728 let service = JwtService::new("original_secret");
729 let claims = make_test_refresh_claims();
730
731 let old_token = service.create_refresh_token(&claims).unwrap();
732
733 service.rotate_key("new_secret").unwrap();
735
736 let validated = service.validate_refresh_token(&old_token).unwrap();
738 assert_eq!(validated.sub, "user-123");
739 assert_eq!(validated.token_type, "refresh");
740
741 let new_token = service.create_refresh_token(&claims).unwrap();
743 let validated = service.validate_refresh_token(&new_token).unwrap();
744 assert_eq!(validated.sub, "user-123");
745 }
746
747 #[test]
752 fn test_rs256_create_validate() {
753 let service = JwtService::new_rs256(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY).unwrap();
754 let claims = make_test_claims();
755
756 let token = service.create_token(&claims).unwrap();
757 let validated = service.validate_token(&token).unwrap();
758
759 assert_eq!(validated.sub, "user-123");
760 assert_eq!(validated.iss, "backbone");
761
762 let header = jsonwebtoken::decode_header(&token).unwrap();
764 assert_eq!(header.alg, jsonwebtoken::Algorithm::RS256);
765 }
766
767 #[test]
768 fn test_rs256_validate_with_public_key_only() {
769 let signing_service = JwtService::new_rs256(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY).unwrap();
771 let claims = make_test_claims();
772 let token = signing_service.create_token(&claims).unwrap();
773
774 let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
777 let decoding_key = jsonwebtoken::DecodingKey::from_rsa_pem(TEST_RSA_PUBLIC_KEY.as_bytes()).unwrap();
778 let decoded = jsonwebtoken::decode::<Claims>(&token, &decoding_key, &validation).unwrap();
779
780 assert_eq!(decoded.claims.sub, "user-123");
781 }
782
783 #[test]
784 fn test_rs256_key_rotation() {
785 let service = JwtService::new_rs256(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY).unwrap();
786 let claims = make_test_claims();
787
788 let old_token = service.create_token(&claims).unwrap();
790
791 service.rotate_rsa_key(TEST_RSA_PRIVATE_KEY_2, TEST_RSA_PUBLIC_KEY_2).unwrap();
793
794 let validated = service.validate_token(&old_token).unwrap();
796 assert_eq!(validated.sub, "user-123");
797
798 let new_token = service.create_token(&claims).unwrap();
800 let validated = service.validate_token(&new_token).unwrap();
801 assert_eq!(validated.sub, "user-123");
802 }
803
804 #[test]
805 fn test_rs256_grace_period_expiry() {
806 let config = KeyRotationConfig {
807 grace_period: chrono::Duration::zero(),
808 };
809 let service = JwtService::with_rs256_rotation(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY, config).unwrap();
810 let claims = make_test_claims();
811
812 let old_token = service.create_token(&claims).unwrap();
813
814 service.rotate_rsa_key(TEST_RSA_PRIVATE_KEY_2, TEST_RSA_PUBLIC_KEY_2).unwrap();
816
817 assert!(service.validate_token(&old_token).is_err());
819
820 let new_token = service.create_token(&claims).unwrap();
822 assert!(service.validate_token(&new_token).is_ok());
823 }
824
825 #[test]
826 fn test_cross_algorithm_rejection() {
827 let hs_service = JwtService::new("test_secret");
829 let rs_service = JwtService::new_rs256(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY).unwrap();
830 let claims = make_test_claims();
831
832 let hs_token = hs_service.create_token(&claims).unwrap();
833 let rs_token = rs_service.create_token(&claims).unwrap();
834
835 assert!(rs_service.validate_token(&hs_token).is_err());
837 assert!(hs_service.validate_token(&rs_token).is_err());
838 }
839
840 #[test]
841 fn test_public_key_export() {
842 let rs_service = JwtService::new_rs256(TEST_RSA_PRIVATE_KEY, TEST_RSA_PUBLIC_KEY).unwrap();
844 let public_key = rs_service.public_key_pem();
845 assert!(public_key.is_some());
846 assert!(public_key.unwrap().contains("BEGIN PUBLIC KEY"));
847
848 let hs_service = JwtService::new("secret");
850 assert!(hs_service.public_key_pem().is_none());
851 }
852
853 #[test]
854 fn test_rs256_backward_compat() {
855 let service = JwtService::new("my_secret");
857 assert_eq!(service.algorithm(), JwtAlgorithm::HS256);
858 assert!(service.public_key_pem().is_none());
859
860 let claims = make_test_claims();
861 let token = service.create_token(&claims).unwrap();
862 let validated = service.validate_token(&token).unwrap();
863 assert_eq!(validated.sub, "user-123");
864
865 service.rotate_key("new_secret").unwrap();
867 assert!(service.validate_token(&token).is_ok());
868 }
869}