Skip to main content

backbone_auth/
jwt.rs

1//! JWT (JSON Web Token) handling with key rotation and multi-algorithm support
2//!
3//! Supports HS256 (symmetric HMAC) and RS256 (asymmetric RSA) algorithms.
4//! Supports multiple signing keys for zero-downtime key rotation.
5//! Old keys remain valid during a configurable grace period after rotation.
6
7use anyhow::Result;
8use chrono::{DateTime, Utc};
9use serde::{Serialize, Deserialize};
10use std::sync::RwLock;
11use zeroize::Zeroize;
12use crate::traits::RefreshTokenClaims;
13
14/// JWT token claims
15#[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/// JWT signing algorithm
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum JwtAlgorithm {
26    /// HMAC-SHA256 (symmetric) — requires shared secret
27    HS256,
28    /// RSA-SHA256 (asymmetric) — sign with private key, verify with public key
29    RS256,
30}
31
32/// A single HMAC signing key with lifecycle metadata
33#[derive(Clone)]
34pub struct JwtKey {
35    /// Unique key identifier (included in JWT header as `kid`)
36    pub kid: String,
37    /// The HMAC secret
38    pub secret: String,
39    /// When this key was created
40    pub created_at: DateTime<Utc>,
41    /// When this key was retired (rotated out)
42    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/// An RSA key pair for asymmetric JWT signing
52#[derive(Clone)]
53pub struct RsaKeyPair {
54    /// Unique key identifier (included in JWT header as `kid`)
55    pub kid: String,
56    /// PEM-encoded RSA private key (for signing)
57    pub private_key_pem: String,
58    /// PEM-encoded RSA public key (for verification)
59    pub public_key_pem: String,
60    /// When this key was created
61    pub created_at: DateTime<Utc>,
62    /// When this key was retired (rotated out)
63    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/// Internal key material abstraction for algorithm-agnostic operation
73#[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/// Configuration for key rotation behavior
111#[derive(Debug, Clone)]
112pub struct KeyRotationConfig {
113    /// How long retired keys remain valid for token validation
114    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
125/// JWT service with key rotation and multi-algorithm support
126///
127/// Maintains an active signing key and a list of retired keys.
128/// Tokens are always signed with the active key.
129/// Validation tries the active key first, then retired keys within the grace period.
130///
131/// Supports both HS256 (shared secret) and RS256 (RSA key pair).
132pub struct JwtService {
133    active_key: RwLock<KeyMaterial>,
134    retired_keys: RwLock<Vec<KeyMaterial>>,
135    rotation_config: KeyRotationConfig,
136}
137
138impl JwtService {
139    // =========================================================================
140    // Constructors — HS256
141    // =========================================================================
142
143    /// Create a new JWT service with HS256 (backward compatible)
144    pub fn new(secret: &str) -> Self {
145        Self::with_rotation(secret, KeyRotationConfig::default())
146    }
147
148    /// Create a new JWT service with HS256 and explicit rotation configuration
149    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    // =========================================================================
165    // Constructors — RS256
166    // =========================================================================
167
168    /// Create a new JWT service with RS256 (asymmetric)
169    ///
170    /// Validates the RSA key pair on construction — returns an error if the keys
171    /// are malformed or cannot be used for signing/verification.
172    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    /// Create a new JWT service with RS256 and explicit rotation configuration
177    ///
178    /// Validates the RSA key pair on construction — returns an error if the keys
179    /// are malformed or cannot be used for signing/verification.
180    pub fn with_rs256_rotation(
181        private_key_pem: &str,
182        public_key_pem: &str,
183        config: KeyRotationConfig,
184    ) -> Result<Self> {
185        // Validate keys upfront — fail fast if they are invalid
186        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    // =========================================================================
207    // Key info & export
208    // =========================================================================
209
210    /// Get the current active key ID
211    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    /// Get the algorithm used by this service
219    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    /// Export the public key PEM (RS256 only, returns None for HS256)
227    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    // =========================================================================
235    // Key rotation
236    // =========================================================================
237
238    /// Rotate the HS256 signing key.
239    /// Returns the `kid` of the new active key.
240    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    /// Rotate the RS256 key pair.
251    /// Validates the new key pair before rotating. Returns the `kid` of the new active key.
252    pub fn rotate_rsa_key(&self, private_key_pem: &str, public_key_pem: &str) -> Result<String> {
253        // Validate keys before rotating
254        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    /// Internal: swap active key, retire old one, prune expired
270    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    // =========================================================================
296    // Token creation
297    // =========================================================================
298
299    /// Create JWT token (signs with the active key, includes `kid` in header)
300    #[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    /// Create refresh token (signs with the active key)
317    #[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    // =========================================================================
334    // Token validation
335    // =========================================================================
336
337    /// Validate JWT token (tries active key first, then retired keys)
338    #[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    /// Decode JWT token without expiration validation
344    #[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    /// Validate refresh token (tries all valid keys)
356    #[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    // =========================================================================
374    // Private helpers
375    // =========================================================================
376
377    /// Build encoding header + key from KeyMaterial
378    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    /// Generic token validation that tries active key, then retired keys
397    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        // Extract kid from token header (if present)
408        let token_kid = Self::extract_kid(token);
409
410        // Try active key first
411        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        // Try retired keys (within grace period)
421        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    /// Try to validate a token with a specific key
443    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    /// Extract the `kid` from a token's header without full validation
481    fn extract_kid(token: &str) -> Option<String> {
482        jsonwebtoken::decode_header(token)
483            .ok()
484            .and_then(|header| header.kid)
485    }
486
487    /// Remove retired keys that have exceeded the grace period
488    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    // Test RSA key pair (2048-bit, generated for testing only — PKCS#8 format)
505    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    // Second RSA key pair for rotation tests (PKCS#8 format)
545    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    // =========================================================================
610    // HS256 tests (backward compatibility)
611    // =========================================================================
612
613    #[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        // Create token with original key
643        let old_token = service.create_token(&claims).unwrap();
644
645        // Rotate to new key
646        service.rotate_key("new_secret_after_rotation").unwrap();
647
648        // Old token should still validate (within grace period)
649        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        // Rotate key
658        service.rotate_key("new_secret_after_rotation").unwrap();
659
660        // New tokens should validate
661        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        // Use zero grace period
670        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        // Rotate — with zero grace, old key is immediately expired
679        service.rotate_key("new_secret").unwrap();
680
681        // Old token should fail validation
682        let result = service.validate_token(&old_token);
683        assert!(result.is_err());
684    }
685
686    #[test]
687    fn test_backward_compat_no_kid() {
688        // Simulate a token created without kid (e.g., before rotation was enabled)
689        use jsonwebtoken::{encode, EncodingKey, Header};
690
691        let secret = "test_secret_key";
692        let service = JwtService::new(secret);
693
694        // Create token manually without kid
695        let claims = make_test_claims();
696        let header = Header::default(); // No kid
697        let token = encode(&header, &claims, &EncodingKey::from_secret(secret.as_ref())).unwrap();
698
699        // Should still validate (tries active key since no kid to match)
700        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        // All tokens should still validate (within 24h grace)
721        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        // Rotate key
734        service.rotate_key("new_secret").unwrap();
735
736        // Old refresh token should still validate
737        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        // New refresh token should also validate
742        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    // =========================================================================
748    // RS256 tests
749    // =========================================================================
750
751    #[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        // Verify the token header uses RS256
763        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        // Create service with full key pair for signing
770        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        // Validate using only the public key (simulates a different service)
775        // We use the public key as "private" too — validation only needs the public key
776        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        // Create token with original key pair
789        let old_token = service.create_token(&claims).unwrap();
790
791        // Rotate to new key pair
792        service.rotate_rsa_key(TEST_RSA_PRIVATE_KEY_2, TEST_RSA_PUBLIC_KEY_2).unwrap();
793
794        // Old token should still validate (within grace period)
795        let validated = service.validate_token(&old_token).unwrap();
796        assert_eq!(validated.sub, "user-123");
797
798        // New tokens should also validate
799        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        // Rotate with zero grace period
815        service.rotate_rsa_key(TEST_RSA_PRIVATE_KEY_2, TEST_RSA_PUBLIC_KEY_2).unwrap();
816
817        // Old token should fail
818        assert!(service.validate_token(&old_token).is_err());
819
820        // New token should work
821        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        // Token signed with HS256 should not validate on RS256 service
828        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        // Cross-validation should fail
836        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        // RS256 service should export public key
843        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        // HS256 service should return None
849        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        // HS256 API should be completely unchanged
856        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        // Rotation still works
866        service.rotate_key("new_secret").unwrap();
867        assert!(service.validate_token(&token).is_ok());
868    }
869}