oxify_authn/
jwt.rs

1//! JWT token handling and validation
2//!
3//! Ported from `OxiRS` (<https://github.com/cool-japan/oxirs>)
4//! Original implementation: Copyright (c) `OxiRS` Contributors
5//! Adapted for `OxiFY`
6//! License: MIT OR Apache-2.0 (compatible with `OxiRS`)
7//!
8//! # Features
9//! - HS256/384/512 symmetric signing
10//! - RS256/384/512 RSA asymmetric signing
11//! - ES256/384 ECDSA asymmetric signing
12//! - JWT ID (jti) claim for token revocation
13//! - Refresh token support
14
15use crate::types::{
16    AuthError, Claims, JwtAlgorithm, JwtConfig, JwtKeyConfig, Result, TokenValidation, User,
17};
18use chrono::{DateTime, Duration, Utc};
19use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
20
21/// JWT token manager
22pub struct JwtManager {
23    encoding_key: Option<EncodingKey>,
24    decoding_key: DecodingKey,
25    algorithm: Algorithm,
26    issuer: String,
27    audience: String,
28    expiration_secs: u64,
29    refresh_expiration_secs: u64,
30    include_jti: bool,
31}
32
33impl JwtManager {
34    /// Create a new JWT manager
35    pub fn new(config: &JwtConfig) -> Result<Self> {
36        let algorithm = Self::convert_algorithm(config.algorithm);
37
38        let (encoding_key, decoding_key) = Self::create_keys(config)?;
39
40        Ok(Self {
41            encoding_key,
42            decoding_key,
43            algorithm,
44            issuer: config.issuer.clone(),
45            audience: config.audience.clone(),
46            expiration_secs: config.expiration_secs,
47            refresh_expiration_secs: config.refresh_expiration_secs,
48            include_jti: config.include_jti,
49        })
50    }
51
52    /// Convert `JwtAlgorithm` to jsonwebtoken Algorithm
53    fn convert_algorithm(alg: JwtAlgorithm) -> Algorithm {
54        match alg {
55            JwtAlgorithm::HS256 => Algorithm::HS256,
56            JwtAlgorithm::HS384 => Algorithm::HS384,
57            JwtAlgorithm::HS512 => Algorithm::HS512,
58            JwtAlgorithm::RS256 => Algorithm::RS256,
59            JwtAlgorithm::RS384 => Algorithm::RS384,
60            JwtAlgorithm::RS512 => Algorithm::RS512,
61            JwtAlgorithm::ES256 => Algorithm::ES256,
62            JwtAlgorithm::ES384 => Algorithm::ES384,
63        }
64    }
65
66    /// Create encoding and decoding keys from config
67    fn create_keys(config: &JwtConfig) -> Result<(Option<EncodingKey>, DecodingKey)> {
68        match &config.key_config {
69            Some(JwtKeyConfig::Rsa {
70                private_key,
71                public_key,
72            }) => {
73                let encoding_key = if let Some(pk) = private_key {
74                    Some(EncodingKey::from_rsa_pem(pk.as_bytes()).map_err(|e| {
75                        AuthError::ConfigurationError(format!("Invalid RSA private key: {e}"))
76                    })?)
77                } else {
78                    None
79                };
80
81                let decoding_key =
82                    DecodingKey::from_rsa_pem(public_key.as_bytes()).map_err(|e| {
83                        AuthError::ConfigurationError(format!("Invalid RSA public key: {e}"))
84                    })?;
85
86                Ok((encoding_key, decoding_key))
87            }
88            Some(JwtKeyConfig::Ec {
89                private_key,
90                public_key,
91            }) => {
92                let encoding_key = if let Some(pk) = private_key {
93                    Some(EncodingKey::from_ec_pem(pk.as_bytes()).map_err(|e| {
94                        AuthError::ConfigurationError(format!("Invalid EC private key: {e}"))
95                    })?)
96                } else {
97                    None
98                };
99
100                let decoding_key =
101                    DecodingKey::from_ec_pem(public_key.as_bytes()).map_err(|e| {
102                        AuthError::ConfigurationError(format!("Invalid EC public key: {e}"))
103                    })?;
104
105                Ok((encoding_key, decoding_key))
106            }
107            Some(JwtKeyConfig::Secret(secret)) => {
108                let encoding_key = EncodingKey::from_secret(secret.as_bytes());
109                let decoding_key = DecodingKey::from_secret(secret.as_bytes());
110                Ok((Some(encoding_key), decoding_key))
111            }
112            None => {
113                // Use secret from config
114                let encoding_key = EncodingKey::from_secret(config.secret.as_bytes());
115                let decoding_key = DecodingKey::from_secret(config.secret.as_bytes());
116                Ok((Some(encoding_key), decoding_key))
117            }
118        }
119    }
120
121    /// Generate a JWT token for a user
122    pub fn generate_token(&self, user: &User) -> Result<String> {
123        let encoding_key = self.encoding_key.as_ref().ok_or_else(|| {
124            AuthError::ConfigurationError(
125                "No encoding key available (verification only mode)".into(),
126            )
127        })?;
128
129        let now = Utc::now();
130        let expiration = now + Duration::seconds(self.expiration_secs as i64);
131
132        let jti = if self.include_jti {
133            Some(uuid::Uuid::new_v4().to_string())
134        } else {
135            None
136        };
137
138        let claims = Claims {
139            sub: user.username.clone(),
140            exp: expiration.timestamp(),
141            iat: now.timestamp(),
142            nbf: now.timestamp(),
143            iss: self.issuer.clone(),
144            aud: self.audience.clone(),
145            roles: user.roles.clone(),
146            permissions: user.permissions.clone(),
147            jti,
148            email: user.email.clone(),
149            token_type: Some("access".to_string()),
150        };
151
152        encode(&Header::new(self.algorithm), &claims, encoding_key)
153            .map_err(|e| AuthError::InternalError(format!("Failed to generate JWT token: {e}")))
154    }
155
156    /// Generate a token with custom claims
157    pub fn generate_token_with_claims(&self, claims: &Claims) -> Result<String> {
158        let encoding_key = self.encoding_key.as_ref().ok_or_else(|| {
159            AuthError::ConfigurationError(
160                "No encoding key available (verification only mode)".into(),
161            )
162        })?;
163
164        encode(&Header::new(self.algorithm), claims, encoding_key)
165            .map_err(|e| AuthError::InternalError(format!("Failed to generate JWT token: {e}")))
166    }
167
168    /// Validate a JWT token and return user information
169    pub fn validate_token(&self, token: &str) -> Result<TokenValidation> {
170        let mut validation = Validation::new(self.algorithm);
171        validation.set_issuer(std::slice::from_ref(&self.issuer));
172        validation.set_audience(std::slice::from_ref(&self.audience));
173
174        let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
175            .map_err(|_e| AuthError::InvalidToken("Failed to decode token".into()))?;
176
177        let claims = token_data.claims;
178
179        // Check if token is expired
180        let exp_time = DateTime::from_timestamp(claims.exp, 0).ok_or(AuthError::InvalidToken(
181            "Invalid expiration timestamp".into(),
182        ))?;
183
184        if Utc::now() > exp_time {
185            return Err(AuthError::TokenExpired);
186        }
187
188        let user = User {
189            username: claims.sub,
190            roles: claims.roles,
191            email: claims.email,
192            full_name: None,
193            last_login: None,
194            permissions: claims.permissions,
195        };
196
197        Ok(TokenValidation {
198            user,
199            expires_at: exp_time,
200        })
201    }
202
203    /// Validate token and return full claims
204    pub fn validate_token_claims(&self, token: &str) -> Result<Claims> {
205        let mut validation = Validation::new(self.algorithm);
206        validation.set_issuer(std::slice::from_ref(&self.issuer));
207        validation.set_audience(std::slice::from_ref(&self.audience));
208
209        let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
210            .map_err(|_e| AuthError::InvalidToken("Failed to decode token claims".into()))?;
211
212        let claims = token_data.claims;
213
214        // Check if token is expired
215        let exp_time = DateTime::from_timestamp(claims.exp, 0).ok_or(AuthError::InvalidToken(
216            "Invalid expiration timestamp in claims".into(),
217        ))?;
218
219        if Utc::now() > exp_time {
220            return Err(AuthError::TokenExpired);
221        }
222
223        Ok(claims)
224    }
225
226    /// Get JWT ID (jti) from token without full validation
227    pub fn get_token_id(&self, token: &str) -> Result<Option<String>> {
228        // Use a permissive validation for extracting claims
229        let mut validation = Validation::new(self.algorithm);
230        validation.set_issuer(std::slice::from_ref(&self.issuer));
231        validation.set_audience(std::slice::from_ref(&self.audience));
232        validation.validate_exp = false; // Allow reading expired tokens
233
234        let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
235            .map_err(|_e| AuthError::InvalidToken("Failed to extract token ID".into()))?;
236
237        Ok(token_data.claims.jti)
238    }
239
240    /// Extract token from authorization header
241    #[must_use]
242    pub fn extract_token_from_header(auth_header: &str) -> Option<&str> {
243        auth_header.strip_prefix("Bearer ")
244    }
245
246    /// Generate a refresh token
247    pub fn generate_refresh_token(&self, user: &User) -> Result<String> {
248        let encoding_key = self.encoding_key.as_ref().ok_or_else(|| {
249            AuthError::ConfigurationError(
250                "No encoding key available (verification only mode)".into(),
251            )
252        })?;
253
254        let now = Utc::now();
255        let expiration = now + Duration::seconds(self.refresh_expiration_secs as i64);
256
257        let jti = if self.include_jti {
258            Some(uuid::Uuid::new_v4().to_string())
259        } else {
260            None
261        };
262
263        let claims = Claims {
264            sub: user.username.clone(),
265            exp: expiration.timestamp(),
266            iat: now.timestamp(),
267            nbf: now.timestamp(),
268            iss: self.issuer.clone(),
269            aud: format!("{}-refresh", self.audience),
270            roles: user.roles.clone(),
271            permissions: user.permissions.clone(),
272            jti,
273            email: user.email.clone(),
274            token_type: Some("refresh".to_string()),
275        };
276
277        encode(&Header::new(self.algorithm), &claims, encoding_key)
278            .map_err(|e| AuthError::InternalError(format!("Failed to generate refresh token: {e}")))
279    }
280
281    /// Validate a refresh token
282    pub fn validate_refresh_token(&self, token: &str) -> Result<TokenValidation> {
283        let mut validation = Validation::new(self.algorithm);
284        validation.set_issuer(std::slice::from_ref(&self.issuer));
285        let audience = format!("{}-refresh", self.audience);
286        validation.set_audience(std::slice::from_ref(&audience));
287
288        let token_data = decode::<Claims>(token, &self.decoding_key, &validation)
289            .map_err(|_e| AuthError::InvalidToken("Failed to decode refresh token".into()))?;
290
291        let claims = token_data.claims;
292
293        let exp_time = DateTime::from_timestamp(claims.exp, 0).ok_or(AuthError::InvalidToken(
294            "Invalid expiration timestamp in refresh token".into(),
295        ))?;
296
297        if Utc::now() > exp_time {
298            return Err(AuthError::TokenExpired);
299        }
300
301        let user = User {
302            username: claims.sub,
303            roles: claims.roles,
304            email: claims.email,
305            full_name: None,
306            last_login: None,
307            permissions: claims.permissions,
308        };
309
310        Ok(TokenValidation {
311            user,
312            expires_at: exp_time,
313        })
314    }
315
316    /// Get token expiration time
317    pub fn get_token_expiration(&self, token: &str) -> Result<DateTime<Utc>> {
318        // Use permissive validation to read expiration from potentially expired tokens
319        let mut validation = Validation::new(self.algorithm);
320        validation.set_issuer(std::slice::from_ref(&self.issuer));
321        validation.set_audience(std::slice::from_ref(&self.audience));
322        validation.validate_exp = false; // Allow reading expired tokens
323
324        let token_data =
325            decode::<Claims>(token, &self.decoding_key, &validation).map_err(|_e| {
326                AuthError::InvalidToken("Failed to decode token for expiration check".into())
327            })?;
328
329        DateTime::from_timestamp(token_data.claims.exp, 0).ok_or(AuthError::InvalidToken(
330            "Invalid expiration timestamp for expiration check".into(),
331        ))
332    }
333
334    /// Check if token is close to expiration (within 1 hour)
335    pub fn is_token_close_to_expiration(&self, token: &str) -> Result<bool> {
336        let expiration = self.get_token_expiration(token)?;
337        let one_hour_from_now = Utc::now() + Duration::hours(1);
338        Ok(expiration <= one_hour_from_now)
339    }
340
341    /// Get the issuer
342    #[must_use]
343    pub fn issuer(&self) -> &str {
344        &self.issuer
345    }
346
347    /// Get the audience
348    #[must_use]
349    pub fn audience(&self) -> &str {
350        &self.audience
351    }
352
353    /// Check if manager can sign tokens
354    #[must_use]
355    pub fn can_sign(&self) -> bool {
356        self.encoding_key.is_some()
357    }
358}
359
360/// Builder for creating custom claims
361pub struct ClaimsBuilder {
362    sub: String,
363    roles: Vec<String>,
364    permissions: Vec<crate::types::Permission>,
365    exp: i64,
366    iat: i64,
367    nbf: i64,
368    iss: String,
369    aud: String,
370    jti: Option<String>,
371    email: Option<String>,
372    token_type: Option<String>,
373}
374
375impl ClaimsBuilder {
376    /// Create a new claims builder
377    pub fn new(
378        subject: impl Into<String>,
379        issuer: impl Into<String>,
380        audience: impl Into<String>,
381    ) -> Self {
382        let now = Utc::now().timestamp();
383        Self {
384            sub: subject.into(),
385            roles: Vec::new(),
386            permissions: Vec::new(),
387            exp: now + 3600, // 1 hour default
388            iat: now,
389            nbf: now,
390            iss: issuer.into(),
391            aud: audience.into(),
392            jti: None,
393            email: None,
394            token_type: None,
395        }
396    }
397
398    /// Set roles
399    #[must_use]
400    pub fn roles(mut self, roles: Vec<String>) -> Self {
401        self.roles = roles;
402        self
403    }
404
405    /// Set permissions
406    #[must_use]
407    pub fn permissions(mut self, permissions: Vec<crate::types::Permission>) -> Self {
408        self.permissions = permissions;
409        self
410    }
411
412    /// Set expiration (duration from now)
413    #[must_use]
414    pub fn expires_in(mut self, duration: Duration) -> Self {
415        self.exp = (Utc::now() + duration).timestamp();
416        self
417    }
418
419    /// Set expiration (absolute timestamp)
420    #[must_use]
421    pub fn expires_at(mut self, time: DateTime<Utc>) -> Self {
422        self.exp = time.timestamp();
423        self
424    }
425
426    /// Set JWT ID
427    #[must_use]
428    pub fn jti(mut self, jti: impl Into<String>) -> Self {
429        self.jti = Some(jti.into());
430        self
431    }
432
433    /// Generate random JWT ID
434    #[must_use]
435    pub fn with_random_jti(mut self) -> Self {
436        self.jti = Some(uuid::Uuid::new_v4().to_string());
437        self
438    }
439
440    /// Set email
441    #[must_use]
442    pub fn email(mut self, email: impl Into<String>) -> Self {
443        self.email = Some(email.into());
444        self
445    }
446
447    /// Set token type
448    #[must_use]
449    pub fn token_type(mut self, token_type: impl Into<String>) -> Self {
450        self.token_type = Some(token_type.into());
451        self
452    }
453
454    /// Build the claims
455    #[must_use]
456    pub fn build(self) -> Claims {
457        Claims {
458            sub: self.sub,
459            roles: self.roles,
460            permissions: self.permissions,
461            exp: self.exp,
462            iat: self.iat,
463            nbf: self.nbf,
464            iss: self.iss,
465            aud: self.aud,
466            jti: self.jti,
467            email: self.email,
468            token_type: self.token_type,
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::types::Permission;
477
478    #[test]
479    fn test_jwt_token_generation() {
480        let config = JwtConfig::development();
481        let manager = JwtManager::new(&config).unwrap();
482
483        let user = User {
484            username: "alice".to_string(),
485            roles: vec!["admin".to_string()],
486            email: Some("alice@example.com".to_string()),
487            full_name: Some("Alice Wonderland".to_string()),
488            last_login: None,
489            permissions: vec![Permission::Admin],
490        };
491
492        let token = manager.generate_token(&user).unwrap();
493        assert!(!token.is_empty());
494
495        // Validate the token
496        let validation = manager.validate_token(&token).unwrap();
497        assert_eq!(validation.user.username, "alice");
498        assert_eq!(validation.user.roles.len(), 1);
499    }
500
501    #[test]
502    fn test_jwt_with_jti() {
503        let config = JwtConfig::development().with_jti(true);
504        let manager = JwtManager::new(&config).unwrap();
505
506        let user = User {
507            username: "alice".to_string(),
508            roles: vec!["admin".to_string()],
509            email: None,
510            full_name: None,
511            last_login: None,
512            permissions: vec![],
513        };
514
515        let token = manager.generate_token(&user).unwrap();
516
517        // Get JTI
518        let jti = manager.get_token_id(&token).unwrap();
519        assert!(jti.is_some());
520
521        // Validate full claims
522        let claims = manager.validate_token_claims(&token).unwrap();
523        assert!(claims.jti.is_some());
524        assert_eq!(claims.token_type, Some("access".to_string()));
525    }
526
527    #[test]
528    fn test_extract_token_from_header() {
529        let header = "Bearer abc123";
530        let token = JwtManager::extract_token_from_header(header);
531        assert_eq!(token, Some("abc123"));
532
533        let invalid_header = "Basic abc123";
534        let token = JwtManager::extract_token_from_header(invalid_header);
535        assert_eq!(token, None);
536    }
537
538    #[test]
539    fn test_refresh_token() {
540        let config = JwtConfig::development();
541        let manager = JwtManager::new(&config).unwrap();
542
543        let user = User {
544            username: "bob".to_string(),
545            roles: vec!["user".to_string()],
546            email: None,
547            full_name: None,
548            last_login: None,
549            permissions: vec![Permission::Read],
550        };
551
552        let refresh_token = manager.generate_refresh_token(&user).unwrap();
553        assert!(!refresh_token.is_empty());
554
555        // Validate the refresh token
556        let validation = manager.validate_refresh_token(&refresh_token).unwrap();
557        assert_eq!(validation.user.username, "bob");
558    }
559
560    #[test]
561    fn test_invalid_token() {
562        let config = JwtConfig::development();
563        let manager = JwtManager::new(&config).unwrap();
564
565        let invalid_token = "invalid.token.here";
566        let result = manager.validate_token(invalid_token);
567        assert!(result.is_err());
568    }
569
570    #[test]
571    fn test_claims_builder() {
572        let claims = ClaimsBuilder::new("user123", "issuer", "audience")
573            .roles(vec!["admin".to_string()])
574            .permissions(vec![Permission::Admin])
575            .expires_in(Duration::hours(2))
576            .with_random_jti()
577            .email("user@example.com")
578            .token_type("access")
579            .build();
580
581        assert_eq!(claims.sub, "user123");
582        assert_eq!(claims.iss, "issuer");
583        assert_eq!(claims.aud, "audience");
584        assert!(claims.jti.is_some());
585        assert_eq!(claims.email, Some("user@example.com".to_string()));
586    }
587
588    #[test]
589    fn test_custom_claims_token() {
590        let config = JwtConfig::development();
591        let manager = JwtManager::new(&config).unwrap();
592
593        let claims = ClaimsBuilder::new("user123", &config.issuer, &config.audience)
594            .roles(vec!["custom".to_string()])
595            .expires_in(Duration::minutes(30))
596            .build();
597
598        let token = manager.generate_token_with_claims(&claims).unwrap();
599        assert!(!token.is_empty());
600
601        let validated = manager.validate_token_claims(&token).unwrap();
602        assert_eq!(validated.sub, "user123");
603        assert_eq!(validated.roles, vec!["custom".to_string()]);
604    }
605
606    #[test]
607    fn test_algorithm_selection() {
608        // Test HS384
609        let config = JwtConfig::new("secret", "issuer", "audience", 3600)
610            .with_algorithm(JwtAlgorithm::HS384);
611        let manager = JwtManager::new(&config).unwrap();
612        assert!(manager.can_sign());
613
614        // Test HS512
615        let config = JwtConfig::new("secret", "issuer", "audience", 3600)
616            .with_algorithm(JwtAlgorithm::HS512);
617        let manager = JwtManager::new(&config).unwrap();
618
619        let user = User {
620            username: "test".to_string(),
621            roles: vec![],
622            email: None,
623            full_name: None,
624            last_login: None,
625            permissions: vec![],
626        };
627
628        let token = manager.generate_token(&user).unwrap();
629        let validation = manager.validate_token(&token).unwrap();
630        assert_eq!(validation.user.username, "test");
631    }
632}