Skip to main content

actix_security_core/http/security/
jwt.rs

1//! JWT (JSON Web Token) Authentication.
2//!
3//! # Spring Security Equivalent
4//! Similar to Spring Security's JWT authentication with `JwtAuthenticationToken`.
5//!
6//! # Features
7//! - Token generation and validation
8//! - Configurable claims (roles, authorities)
9//! - Multiple signing algorithms (HS256, HS384, HS512, RS256, etc.)
10//! - Token expiration handling
11//!
12//! # Example
13//! ```rust,ignore
14//! use actix_security_core::http::security::jwt::{JwtAuthenticator, JwtConfig};
15//!
16//! let config = JwtConfig::new("your-secret-key")
17//!     .issuer("my-app")
18//!     .audience("my-api")
19//!     .expiration_hours(24);
20//!
21//! let authenticator = JwtAuthenticator::new(config);
22//! ```
23
24use crate::http::security::config::Authenticator;
25use crate::http::security::User;
26use actix_web::dev::ServiceRequest;
27use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
28use serde::{Deserialize, Serialize};
29use std::time::{Duration, SystemTime, UNIX_EPOCH};
30
31// Re-export Algorithm for convenience
32pub use jsonwebtoken::Algorithm;
33
34// =============================================================================
35// JWT Claims
36// =============================================================================
37
38/// Standard JWT claims with security extensions.
39///
40/// # Spring Security Equivalent
41/// Similar to Spring's `Jwt` claims with custom attributes for roles/authorities.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Claims {
44    /// Subject (username)
45    pub sub: String,
46
47    /// Issuer
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub iss: Option<String>,
50
51    /// Audience
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub aud: Option<String>,
54
55    /// Expiration time (Unix timestamp)
56    pub exp: u64,
57
58    /// Issued at (Unix timestamp)
59    pub iat: u64,
60
61    /// Not before (Unix timestamp)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub nbf: Option<u64>,
64
65    /// JWT ID
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub jti: Option<String>,
68
69    /// User roles
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub roles: Vec<String>,
72
73    /// User authorities/permissions
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub authorities: Vec<String>,
76
77    /// Additional custom claims
78    #[serde(flatten, skip_serializing_if = "Option::is_none")]
79    pub custom: Option<serde_json::Value>,
80}
81
82impl Claims {
83    /// Create new claims for a user.
84    pub fn new(username: &str, expiration_secs: u64) -> Self {
85        let now = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .unwrap()
88            .as_secs();
89
90        Self {
91            sub: username.to_string(),
92            iss: None,
93            aud: None,
94            exp: now + expiration_secs,
95            iat: now,
96            nbf: None,
97            jti: None,
98            roles: Vec::new(),
99            authorities: Vec::new(),
100            custom: None,
101        }
102    }
103
104    /// Set issuer.
105    pub fn issuer(mut self, issuer: &str) -> Self {
106        self.iss = Some(issuer.to_string());
107        self
108    }
109
110    /// Set audience.
111    pub fn audience(mut self, audience: &str) -> Self {
112        self.aud = Some(audience.to_string());
113        self
114    }
115
116    /// Set roles.
117    pub fn roles(mut self, roles: Vec<String>) -> Self {
118        self.roles = roles;
119        self
120    }
121
122    /// Set authorities.
123    pub fn authorities(mut self, authorities: Vec<String>) -> Self {
124        self.authorities = authorities;
125        self
126    }
127
128    /// Set custom claims.
129    pub fn custom(mut self, custom: serde_json::Value) -> Self {
130        self.custom = Some(custom);
131        self
132    }
133
134    /// Create claims from a User.
135    pub fn from_user(user: &User, expiration_secs: u64) -> Self {
136        Self::new(user.get_username(), expiration_secs)
137            .roles(user.get_roles().to_vec())
138            .authorities(user.get_authorities().to_vec())
139    }
140}
141
142// =============================================================================
143// JWT Configuration
144// =============================================================================
145
146/// JWT configuration.
147///
148/// # Example
149/// ```rust,ignore
150/// let config = JwtConfig::new("my-secret-key")
151///     .algorithm(Algorithm::HS512)
152///     .issuer("my-app")
153///     .audience("my-api")
154///     .expiration_hours(24)
155///     .leeway_secs(60);
156/// ```
157#[derive(Clone)]
158pub struct JwtConfig {
159    /// Secret key for HMAC algorithms or public key for RSA/EC
160    secret: String,
161    /// Signing algorithm
162    algorithm: Algorithm,
163    /// Token issuer
164    issuer: Option<String>,
165    /// Token audience
166    audience: Option<String>,
167    /// Token expiration in seconds
168    expiration_secs: u64,
169    /// Leeway for expiration validation (seconds)
170    leeway_secs: u64,
171    /// Header prefix (default: "Bearer ")
172    header_prefix: String,
173    /// Header name (default: "Authorization")
174    header_name: String,
175    /// Validate expiration
176    validate_exp: bool,
177}
178
179impl JwtConfig {
180    /// Create a new JWT configuration with HMAC secret.
181    ///
182    /// # Arguments
183    /// * `secret` - Secret key for signing/verifying tokens (min 32 chars recommended)
184    pub fn new(secret: &str) -> Self {
185        Self {
186            secret: secret.to_string(),
187            algorithm: Algorithm::HS256,
188            issuer: None,
189            audience: None,
190            expiration_secs: 3600, // 1 hour default
191            leeway_secs: 0,
192            header_prefix: "Bearer ".to_string(),
193            header_name: "Authorization".to_string(),
194            validate_exp: true,
195        }
196    }
197
198    /// Set the signing algorithm.
199    pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
200        self.algorithm = algorithm;
201        self
202    }
203
204    /// Set the token issuer.
205    pub fn issuer(mut self, issuer: &str) -> Self {
206        self.issuer = Some(issuer.to_string());
207        self
208    }
209
210    /// Set the token audience.
211    pub fn audience(mut self, audience: &str) -> Self {
212        self.audience = Some(audience.to_string());
213        self
214    }
215
216    /// Set expiration time in seconds.
217    pub fn expiration_secs(mut self, secs: u64) -> Self {
218        self.expiration_secs = secs;
219        self
220    }
221
222    /// Set expiration time in hours.
223    pub fn expiration_hours(mut self, hours: u64) -> Self {
224        self.expiration_secs = hours * 3600;
225        self
226    }
227
228    /// Set expiration time in days.
229    pub fn expiration_days(mut self, days: u64) -> Self {
230        self.expiration_secs = days * 86400;
231        self
232    }
233
234    /// Set leeway for expiration validation.
235    pub fn leeway_secs(mut self, secs: u64) -> Self {
236        self.leeway_secs = secs;
237        self
238    }
239
240    /// Set the header prefix (default: "Bearer ").
241    pub fn header_prefix(mut self, prefix: &str) -> Self {
242        self.header_prefix = prefix.to_string();
243        self
244    }
245
246    /// Set the header name (default: "Authorization").
247    pub fn header_name(mut self, name: &str) -> Self {
248        self.header_name = name.to_string();
249        self
250    }
251
252    /// Disable expiration validation (not recommended for production).
253    pub fn disable_exp_validation(mut self) -> Self {
254        self.validate_exp = false;
255        self
256    }
257
258    /// Get expiration duration.
259    pub fn expiration_duration(&self) -> Duration {
260        Duration::from_secs(self.expiration_secs)
261    }
262}
263
264// =============================================================================
265// JWT Authenticator
266// =============================================================================
267
268/// JWT-based authenticator.
269///
270/// Extracts and validates JWT tokens from the Authorization header.
271///
272/// # Spring Security Equivalent
273/// Similar to `JwtAuthenticationProvider` with `BearerTokenAuthenticationFilter`.
274///
275/// # Example
276/// ```rust,ignore
277/// use actix_security_core::http::security::jwt::{JwtAuthenticator, JwtConfig};
278///
279/// let config = JwtConfig::new("your-256-bit-secret-key-here!")
280///     .issuer("my-app")
281///     .expiration_hours(24);
282///
283/// let authenticator = JwtAuthenticator::new(config);
284///
285/// // Use with SecurityTransform
286/// SecurityTransform::new()
287///     .config_authenticator(move || authenticator.clone())
288///     .config_authorizer(|| /* ... */)
289/// ```
290#[derive(Clone)]
291pub struct JwtAuthenticator {
292    config: JwtConfig,
293}
294
295impl JwtAuthenticator {
296    /// Create a new JWT authenticator.
297    pub fn new(config: JwtConfig) -> Self {
298        Self { config }
299    }
300
301    /// Get the configuration.
302    pub fn config(&self) -> &JwtConfig {
303        &self.config
304    }
305
306    /// Generate a JWT token for a user.
307    ///
308    /// # Example
309    /// ```rust,ignore
310    /// let token = authenticator.generate_token(&user)?;
311    /// // Returns: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
312    /// ```
313    pub fn generate_token(&self, user: &User) -> Result<String, JwtError> {
314        let mut claims = Claims::from_user(user, self.config.expiration_secs);
315
316        if let Some(ref issuer) = self.config.issuer {
317            claims = claims.issuer(issuer);
318        }
319        if let Some(ref audience) = self.config.audience {
320            claims = claims.audience(audience);
321        }
322
323        let header = Header::new(self.config.algorithm);
324        let key = EncodingKey::from_secret(self.config.secret.as_bytes());
325
326        encode(&header, &claims, &key).map_err(JwtError::Encoding)
327    }
328
329    /// Generate a token with custom claims.
330    pub fn generate_token_with_claims(&self, claims: &Claims) -> Result<String, JwtError> {
331        let header = Header::new(self.config.algorithm);
332        let key = EncodingKey::from_secret(self.config.secret.as_bytes());
333
334        encode(&header, claims, &key).map_err(JwtError::Encoding)
335    }
336
337    /// Validate a token and return the claims.
338    pub fn validate_token(&self, token: &str) -> Result<TokenData<Claims>, JwtError> {
339        let key = DecodingKey::from_secret(self.config.secret.as_bytes());
340
341        let mut validation = Validation::new(self.config.algorithm);
342        validation.leeway = self.config.leeway_secs;
343        validation.validate_exp = self.config.validate_exp;
344
345        if let Some(ref issuer) = self.config.issuer {
346            validation.set_issuer(&[issuer]);
347        }
348        if let Some(ref audience) = self.config.audience {
349            validation.set_audience(&[audience]);
350        }
351
352        decode::<Claims>(token, &key, &validation).map_err(JwtError::Decoding)
353    }
354
355    /// Extract token from request header.
356    fn extract_token(&self, req: &ServiceRequest) -> Option<String> {
357        let header_value = req.headers().get(&self.config.header_name)?;
358        let header_str = header_value.to_str().ok()?;
359
360        if header_str.starts_with(&self.config.header_prefix) {
361            Some(header_str[self.config.header_prefix.len()..].to_string())
362        } else {
363            None
364        }
365    }
366}
367
368impl Authenticator for JwtAuthenticator {
369    fn get_user(&self, req: &ServiceRequest) -> Option<User> {
370        // Extract token from header
371        let token = self.extract_token(req)?;
372
373        // Validate token
374        let token_data = self.validate_token(&token).ok()?;
375        let claims = token_data.claims;
376
377        // Build User from claims
378        let roles: Vec<String> = claims.roles;
379        let authorities: Vec<String> = claims.authorities;
380
381        Some(
382            User::new(claims.sub, String::new())
383                .roles(&roles)
384                .authorities(&authorities),
385        )
386    }
387}
388
389// =============================================================================
390// JWT Error
391// =============================================================================
392
393/// JWT-related errors.
394#[derive(Debug)]
395pub enum JwtError {
396    /// Token encoding error
397    Encoding(jsonwebtoken::errors::Error),
398    /// Token decoding/validation error
399    Decoding(jsonwebtoken::errors::Error),
400    /// Token expired
401    Expired,
402    /// Invalid token format
403    InvalidFormat,
404}
405
406impl std::fmt::Display for JwtError {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        match self {
409            JwtError::Encoding(e) => write!(f, "JWT encoding error: {}", e),
410            JwtError::Decoding(e) => write!(f, "JWT decoding error: {}", e),
411            JwtError::Expired => write!(f, "JWT token expired"),
412            JwtError::InvalidFormat => write!(f, "Invalid JWT format"),
413        }
414    }
415}
416
417impl std::error::Error for JwtError {}
418
419// =============================================================================
420// JWT Token Service (for generating tokens)
421// =============================================================================
422
423/// Service for generating and managing JWT tokens.
424///
425/// # Example
426/// ```rust,ignore
427/// let token_service = JwtTokenService::new(config);
428///
429/// // Generate token for user
430/// let token = token_service.generate_token(&user)?;
431///
432/// // Generate refresh token (longer expiration)
433/// let refresh_token = token_service.generate_refresh_token(&user)?;
434/// ```
435#[derive(Clone)]
436pub struct JwtTokenService {
437    config: JwtConfig,
438    refresh_expiration_secs: u64,
439}
440
441impl JwtTokenService {
442    /// Create a new token service.
443    pub fn new(config: JwtConfig) -> Self {
444        Self {
445            refresh_expiration_secs: config.expiration_secs * 24, // 24x longer for refresh
446            config,
447        }
448    }
449
450    /// Set refresh token expiration.
451    pub fn refresh_expiration_days(mut self, days: u64) -> Self {
452        self.refresh_expiration_secs = days * 86400;
453        self
454    }
455
456    /// Generate an access token.
457    pub fn generate_token(&self, user: &User) -> Result<String, JwtError> {
458        let authenticator = JwtAuthenticator::new(self.config.clone());
459        authenticator.generate_token(user)
460    }
461
462    /// Generate a refresh token (longer expiration, minimal claims).
463    pub fn generate_refresh_token(&self, user: &User) -> Result<String, JwtError> {
464        let claims = Claims::new(user.get_username(), self.refresh_expiration_secs);
465        let header = Header::new(self.config.algorithm);
466        let key = EncodingKey::from_secret(self.config.secret.as_bytes());
467
468        encode(&header, &claims, &key).map_err(JwtError::Encoding)
469    }
470
471    /// Validate a token and return claims.
472    pub fn validate_token(&self, token: &str) -> Result<Claims, JwtError> {
473        let authenticator = JwtAuthenticator::new(self.config.clone());
474        authenticator.validate_token(token).map(|td| td.claims)
475    }
476
477    /// Get the configuration.
478    pub fn config(&self) -> &JwtConfig {
479        &self.config
480    }
481}
482
483// =============================================================================
484// Token Pair (Access + Refresh)
485// =============================================================================
486
487/// Token pair containing access and refresh tokens.
488///
489/// # Spring Security Equivalent
490/// Similar to OAuth2 token response with access_token and refresh_token.
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct TokenPair {
493    /// Access token for API authentication
494    pub access_token: String,
495    /// Refresh token for obtaining new access tokens
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub refresh_token: Option<String>,
498    /// Token type (typically "Bearer")
499    pub token_type: String,
500    /// Access token expiration in seconds
501    pub expires_in: u64,
502    /// Refresh token expiration in seconds (if present)
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub refresh_expires_in: Option<u64>,
505}
506
507impl TokenPair {
508    /// Create a new token pair.
509    pub fn new(access_token: String, expires_in: u64) -> Self {
510        Self {
511            access_token,
512            refresh_token: None,
513            token_type: "Bearer".to_string(),
514            expires_in,
515            refresh_expires_in: None,
516        }
517    }
518
519    /// Add a refresh token.
520    pub fn with_refresh_token(mut self, refresh_token: String, refresh_expires_in: u64) -> Self {
521        self.refresh_token = Some(refresh_token);
522        self.refresh_expires_in = Some(refresh_expires_in);
523        self
524    }
525}
526
527impl JwtTokenService {
528    /// Generate a token pair (access + refresh).
529    ///
530    /// # Example
531    /// ```rust,ignore
532    /// let token_pair = token_service.generate_token_pair(&user)?;
533    /// println!("Access: {}", token_pair.access_token);
534    /// println!("Refresh: {:?}", token_pair.refresh_token);
535    /// ```
536    pub fn generate_token_pair(&self, user: &User) -> Result<TokenPair, JwtError> {
537        let access_token = self.generate_token(user)?;
538        let refresh_token = self.generate_refresh_token(user)?;
539
540        Ok(TokenPair::new(access_token, self.config.expiration_secs)
541            .with_refresh_token(refresh_token, self.refresh_expiration_secs))
542    }
543
544    /// Refresh tokens using a valid refresh token.
545    ///
546    /// Returns a new token pair if the refresh token is valid.
547    pub fn refresh_tokens(&self, refresh_token: &str) -> Result<TokenPair, JwtError> {
548        // Validate refresh token
549        let claims = self.validate_token(refresh_token)?;
550
551        // Create a minimal user from claims to generate new tokens
552        let user = User::new(claims.sub, String::new())
553            .roles(&claims.roles)
554            .authorities(&claims.authorities);
555
556        // Generate new token pair
557        self.generate_token_pair(&user)
558    }
559}
560
561// =============================================================================
562// Claims Extractor Trait
563// =============================================================================
564
565/// Trait for extracting user information from JWT claims.
566///
567/// Implement this trait to customize how users are built from JWT claims.
568///
569/// # Example
570/// ```rust,ignore
571/// struct CustomClaimsExtractor;
572///
573/// impl ClaimsExtractor for CustomClaimsExtractor {
574///     fn extract_user(&self, claims: &Claims) -> Option<User> {
575///         // Custom extraction logic
576///         Some(User::new(claims.sub.clone(), String::new())
577///             .roles(&claims.roles))
578///     }
579/// }
580/// ```
581pub trait ClaimsExtractor: Send + Sync {
582    /// Extract user from JWT claims.
583    fn extract_user(&self, claims: &Claims) -> Option<User>;
584}
585
586/// Default claims extractor that maps standard claims to User.
587#[derive(Clone, Default)]
588pub struct DefaultClaimsExtractor {
589    /// Claim name for username (default: "sub")
590    username_claim: Option<String>,
591    /// Claim name for roles (default: "roles")
592    roles_claim: Option<String>,
593    /// Claim name for authorities (default: "authorities")
594    authorities_claim: Option<String>,
595}
596
597impl DefaultClaimsExtractor {
598    /// Create a new default claims extractor.
599    pub fn new() -> Self {
600        Self::default()
601    }
602
603    /// Set custom username claim name.
604    pub fn username_claim(mut self, claim: &str) -> Self {
605        self.username_claim = Some(claim.to_string());
606        self
607    }
608
609    /// Set custom roles claim name.
610    pub fn roles_claim(mut self, claim: &str) -> Self {
611        self.roles_claim = Some(claim.to_string());
612        self
613    }
614
615    /// Set custom authorities claim name.
616    pub fn authorities_claim(mut self, claim: &str) -> Self {
617        self.authorities_claim = Some(claim.to_string());
618        self
619    }
620}
621
622impl ClaimsExtractor for DefaultClaimsExtractor {
623    fn extract_user(&self, claims: &Claims) -> Option<User> {
624        let username = claims.sub.clone();
625        let roles = claims.roles.clone();
626        let authorities = claims.authorities.clone();
627
628        Some(
629            User::new(username, String::new())
630                .roles(&roles)
631                .authorities(&authorities),
632        )
633    }
634}
635
636// =============================================================================
637// RSA Configuration (added to JwtConfig)
638// =============================================================================
639
640impl JwtConfig {
641    /// Create a JWT configuration for RS256 with PEM-encoded public key.
642    ///
643    /// Use this for token verification when you only have the public key.
644    ///
645    /// # Example
646    /// ```rust,ignore
647    /// let config = JwtConfig::with_rsa_public_key(include_str!("public_key.pem"));
648    /// ```
649    pub fn with_rsa_public_key(public_key_pem: &str) -> Self {
650        Self {
651            secret: public_key_pem.to_string(),
652            algorithm: Algorithm::RS256,
653            issuer: None,
654            audience: None,
655            expiration_secs: 3600,
656            leeway_secs: 0,
657            header_prefix: "Bearer ".to_string(),
658            header_name: "Authorization".to_string(),
659            validate_exp: true,
660        }
661    }
662
663    /// Check if this config uses RSA algorithm.
664    pub fn is_rsa(&self) -> bool {
665        matches!(
666            self.algorithm,
667            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512
668        )
669    }
670
671    /// Check if this config uses HMAC algorithm.
672    pub fn is_hmac(&self) -> bool {
673        matches!(
674            self.algorithm,
675            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
676        )
677    }
678}
679
680// Enhanced JwtAuthenticator for RSA support
681impl JwtAuthenticator {
682    /// Validate token with RSA public key.
683    ///
684    /// Note: This method works when JwtConfig was created with `with_rsa_public_key`.
685    pub fn validate_token_rsa(&self, token: &str) -> Result<TokenData<Claims>, JwtError> {
686        if !self.config.is_rsa() {
687            return self.validate_token(token);
688        }
689
690        let key =
691            DecodingKey::from_rsa_pem(self.config.secret.as_bytes()).map_err(JwtError::Decoding)?;
692
693        let mut validation = Validation::new(self.config.algorithm);
694        validation.leeway = self.config.leeway_secs;
695        validation.validate_exp = self.config.validate_exp;
696
697        if let Some(ref issuer) = self.config.issuer {
698            validation.set_issuer(&[issuer]);
699        }
700        if let Some(ref audience) = self.config.audience {
701            validation.set_audience(&[audience]);
702        }
703
704        decode::<Claims>(token, &key, &validation).map_err(JwtError::Decoding)
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    fn test_user() -> User {
713        User::new("testuser".to_string(), "password".to_string())
714            .roles(&["USER".into(), "ADMIN".into()])
715            .authorities(&["read".into(), "write".into()])
716    }
717
718    #[test]
719    fn test_generate_and_validate_token() {
720        let config = JwtConfig::new("super-secret-key-that-is-long-enough")
721            .issuer("test-app")
722            .expiration_hours(1);
723
724        let authenticator = JwtAuthenticator::new(config);
725        let user = test_user();
726
727        // Generate token
728        let token = authenticator.generate_token(&user).unwrap();
729        assert!(!token.is_empty());
730
731        // Validate token
732        let token_data = authenticator.validate_token(&token).unwrap();
733        assert_eq!(token_data.claims.sub, "testuser");
734        assert!(token_data.claims.roles.contains(&"USER".to_string()));
735        assert!(token_data.claims.roles.contains(&"ADMIN".to_string()));
736        assert!(token_data.claims.authorities.contains(&"read".to_string()));
737    }
738
739    #[test]
740    fn test_invalid_token() {
741        let config = JwtConfig::new("super-secret-key-that-is-long-enough");
742        let authenticator = JwtAuthenticator::new(config);
743
744        let result = authenticator.validate_token("invalid-token");
745        assert!(result.is_err());
746    }
747
748    #[test]
749    fn test_wrong_secret() {
750        let config1 = JwtConfig::new("secret-key-one-that-is-long-enough");
751        let config2 = JwtConfig::new("secret-key-two-that-is-long-enough");
752
753        let auth1 = JwtAuthenticator::new(config1);
754        let auth2 = JwtAuthenticator::new(config2);
755
756        let token = auth1.generate_token(&test_user()).unwrap();
757        let result = auth2.validate_token(&token);
758        assert!(result.is_err());
759    }
760
761    #[test]
762    fn test_claims_from_user() {
763        let user = test_user();
764        let claims = Claims::from_user(&user, 3600);
765
766        assert_eq!(claims.sub, "testuser");
767        assert!(claims.roles.contains(&"USER".to_string()));
768        assert!(claims.authorities.contains(&"read".to_string()));
769    }
770
771    #[test]
772    fn test_token_service() {
773        let config = JwtConfig::new("super-secret-key-that-is-long-enough").expiration_hours(1);
774
775        let service = JwtTokenService::new(config).refresh_expiration_days(7);
776        let user = test_user();
777
778        let access_token = service.generate_token(&user).unwrap();
779        let refresh_token = service.generate_refresh_token(&user).unwrap();
780
781        assert!(!access_token.is_empty());
782        assert!(!refresh_token.is_empty());
783        assert_ne!(access_token, refresh_token);
784
785        // Validate access token has roles
786        let claims = service.validate_token(&access_token).unwrap();
787        assert!(!claims.roles.is_empty());
788
789        // Validate refresh token has no roles
790        let refresh_claims = service.validate_token(&refresh_token).unwrap();
791        assert!(refresh_claims.roles.is_empty());
792    }
793}