use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone)]
pub struct JwtHandler {
pub(super) encoding_key: EncodingKey,
pub(super) decoding_key: DecodingKey,
pub(super) algorithm: Algorithm,
pub(super) expiration: u64,
pub(super) issuer: String,
}
impl std::fmt::Debug for JwtHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwtHandler")
.field("algorithm", &self.algorithm)
.field("expiration", &self.expiration)
.field("issuer", &self.issuer)
.field("encoding_key", &"[REDACTED]")
.field("decoding_key", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: Uuid,
pub iat: u64,
pub exp: u64,
pub iss: String,
pub aud: String,
pub jti: String,
pub role: String,
pub permissions: Vec<String>,
pub team_id: Option<Uuid>,
pub session_id: Option<String>,
pub token_type: TokenType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TokenType {
Access,
Refresh,
PasswordReset,
EmailVerification,
Invitation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_type_access() {
let token_type = TokenType::Access;
let json = serde_json::to_string(&token_type).unwrap();
assert!(json.contains("access"));
}
#[test]
fn test_token_type_refresh() {
let token_type = TokenType::Refresh;
let json = serde_json::to_string(&token_type).unwrap();
assert!(json.contains("refresh"));
}
#[test]
fn test_token_type_password_reset() {
let token_type = TokenType::PasswordReset;
let json = serde_json::to_string(&token_type).unwrap();
assert!(json.contains("password_reset"));
}
#[test]
fn test_token_type_email_verification() {
let token_type = TokenType::EmailVerification;
let json = serde_json::to_string(&token_type).unwrap();
assert!(json.contains("email_verification"));
}
#[test]
fn test_token_type_invitation() {
let token_type = TokenType::Invitation;
let json = serde_json::to_string(&token_type).unwrap();
assert!(json.contains("invitation"));
}
#[test]
fn test_token_type_all_variants_serialization() {
let variants = vec![
TokenType::Access,
TokenType::Refresh,
TokenType::PasswordReset,
TokenType::EmailVerification,
TokenType::Invitation,
];
for token_type in variants {
let json = serde_json::to_string(&token_type).unwrap();
let parsed: TokenType = serde_json::from_str(&json).unwrap();
assert_eq!(format!("{:?}", token_type), format!("{:?}", parsed));
}
}
#[test]
fn test_token_type_clone() {
let original = TokenType::Access;
let cloned = original.clone();
assert_eq!(format!("{:?}", original), format!("{:?}", cloned));
}
#[test]
fn test_token_type_debug() {
let token_type = TokenType::Access;
let debug_str = format!("{:?}", token_type);
assert!(debug_str.contains("Access"));
}
#[test]
fn test_claims_creation() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = Claims {
sub: Uuid::new_v4(),
iat: now,
exp: now + 3600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
team_id: Some(Uuid::new_v4()),
session_id: Some("session_123".to_string()),
token_type: TokenType::Access,
};
assert!(!claims.sub.is_nil());
assert_eq!(claims.iss, "gateway");
assert_eq!(claims.permissions.len(), 2);
}
#[test]
fn test_claims_serialization() {
let user_id = Uuid::new_v4();
let claims = Claims {
sub: user_id,
iat: 1700000000,
exp: 1700003600,
iss: "test_issuer".to_string(),
aud: "test_audience".to_string(),
jti: "unique_token_id".to_string(),
role: "admin".to_string(),
permissions: vec!["all".to_string()],
team_id: None,
session_id: None,
token_type: TokenType::Access,
};
let json = serde_json::to_string(&claims).unwrap();
assert!(json.contains(&user_id.to_string()));
assert!(json.contains("test_issuer"));
assert!(json.contains("admin"));
let parsed: Claims = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.sub, user_id);
assert_eq!(parsed.role, "admin");
}
#[test]
fn test_claims_with_optional_fields() {
let claims = Claims {
sub: Uuid::new_v4(),
iat: 1700000000,
exp: 1700003600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec![],
team_id: None,
session_id: None,
token_type: TokenType::Access,
};
let json = serde_json::to_string(&claims).unwrap();
let parsed: Claims = serde_json::from_str(&json).unwrap();
assert!(parsed.team_id.is_none());
assert!(parsed.session_id.is_none());
}
#[test]
fn test_claims_with_all_fields() {
let user_id = Uuid::new_v4();
let team_id = Uuid::new_v4();
let claims = Claims {
sub: user_id,
iat: 1700000000,
exp: 1700003600,
iss: "prod-gateway".to_string(),
aud: "prod-api".to_string(),
jti: "jwt_abc123".to_string(),
role: "team_lead".to_string(),
permissions: vec!["read".to_string(), "write".to_string(), "admin".to_string()],
team_id: Some(team_id),
session_id: Some("sess_xyz789".to_string()),
token_type: TokenType::Access,
};
let json = serde_json::to_string(&claims).unwrap();
let parsed: Claims = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.sub, user_id);
assert_eq!(parsed.team_id, Some(team_id));
assert_eq!(parsed.session_id, Some("sess_xyz789".to_string()));
assert_eq!(parsed.permissions.len(), 3);
}
#[test]
fn test_claims_clone() {
let claims = Claims {
sub: Uuid::new_v4(),
iat: 1700000000,
exp: 1700003600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: "token_id".to_string(),
role: "user".to_string(),
permissions: vec!["read".to_string()],
team_id: None,
session_id: None,
token_type: TokenType::Access,
};
let cloned = claims.clone();
assert_eq!(cloned.sub, claims.sub);
assert_eq!(cloned.role, claims.role);
assert_eq!(cloned.permissions, claims.permissions);
}
#[test]
fn test_claims_expiration() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = Claims {
sub: Uuid::new_v4(),
iat: now,
exp: now + 3600, iss: "gateway".to_string(),
aud: "api".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec![],
team_id: None,
session_id: None,
token_type: TokenType::Access,
};
assert!(claims.exp > now);
assert!(claims.iat <= now);
}
#[test]
fn test_claims_different_token_types() {
let base = Claims {
sub: Uuid::new_v4(),
iat: 1700000000,
exp: 1700003600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: "token".to_string(),
role: "user".to_string(),
permissions: vec![],
team_id: None,
session_id: None,
token_type: TokenType::Access,
};
let mut refresh_claims = base.clone();
refresh_claims.token_type = TokenType::Refresh;
let mut reset_claims = base.clone();
reset_claims.token_type = TokenType::PasswordReset;
let access_json = serde_json::to_string(&base).unwrap();
let refresh_json = serde_json::to_string(&refresh_claims).unwrap();
let reset_json = serde_json::to_string(&reset_claims).unwrap();
assert!(access_json.contains("access"));
assert!(refresh_json.contains("refresh"));
assert!(reset_json.contains("password_reset"));
}
#[test]
fn test_token_pair_creation() {
let pair = TokenPair {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...".to_string(),
refresh_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...refresh".to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600,
};
assert!(pair.access_token.starts_with("eyJ"));
assert!(pair.refresh_token.contains("refresh"));
assert_eq!(pair.token_type, "Bearer");
assert_eq!(pair.expires_in, 3600);
}
#[test]
fn test_token_pair_serialization() {
let pair = TokenPair {
access_token: "access_token_value".to_string(),
refresh_token: "refresh_token_value".to_string(),
token_type: "Bearer".to_string(),
expires_in: 7200,
};
let json = serde_json::to_string(&pair).unwrap();
assert!(json.contains("access_token_value"));
assert!(json.contains("refresh_token_value"));
assert!(json.contains("Bearer"));
assert!(json.contains("7200"));
let parsed: TokenPair = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.access_token, "access_token_value");
assert_eq!(parsed.expires_in, 7200);
}
#[test]
fn test_token_pair_clone() {
let pair = TokenPair {
access_token: "access".to_string(),
refresh_token: "refresh".to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600,
};
let cloned = pair.clone();
assert_eq!(cloned.access_token, pair.access_token);
assert_eq!(cloned.refresh_token, pair.refresh_token);
assert_eq!(cloned.expires_in, pair.expires_in);
}
#[test]
fn test_token_pair_debug() {
let pair = TokenPair {
access_token: "token123".to_string(),
refresh_token: "refresh456".to_string(),
token_type: "Bearer".to_string(),
expires_in: 3600,
};
let debug_str = format!("{:?}", pair);
assert!(debug_str.contains("TokenPair"));
assert!(debug_str.contains("token123"));
assert!(debug_str.contains("Bearer"));
}
#[test]
fn test_token_pair_different_expiration() {
let short_lived = TokenPair {
access_token: "short".to_string(),
refresh_token: "refresh".to_string(),
token_type: "Bearer".to_string(),
expires_in: 900, };
let long_lived = TokenPair {
access_token: "long".to_string(),
refresh_token: "refresh".to_string(),
token_type: "Bearer".to_string(),
expires_in: 86400, };
assert!(short_lived.expires_in < long_lived.expires_in);
}
#[test]
fn test_jwt_handler_debug_redacts_keys() {
let handler = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS256,
expiration: 3600,
issuer: "test_issuer".to_string(),
};
let debug_str = format!("{:?}", handler);
assert!(debug_str.contains("JwtHandler"));
assert!(debug_str.contains("[REDACTED]"));
assert!(debug_str.contains("HS256"));
assert!(debug_str.contains("3600"));
assert!(debug_str.contains("test_issuer"));
assert!(!debug_str.contains("secret"));
}
#[test]
fn test_jwt_handler_clone() {
let handler = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS256,
expiration: 3600,
issuer: "gateway".to_string(),
};
let cloned = handler.clone();
assert_eq!(cloned.expiration, handler.expiration);
assert_eq!(cloned.issuer, handler.issuer);
}
#[test]
fn test_jwt_handler_different_algorithms() {
let hs256_handler = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS256,
expiration: 3600,
issuer: "test".to_string(),
};
let hs384_handler = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS384,
expiration: 3600,
issuer: "test".to_string(),
};
let hs512_handler = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS512,
expiration: 3600,
issuer: "test".to_string(),
};
assert_eq!(format!("{:?}", hs256_handler.algorithm), "HS256");
assert_eq!(format!("{:?}", hs384_handler.algorithm), "HS384");
assert_eq!(format!("{:?}", hs512_handler.algorithm), "HS512");
}
#[test]
fn test_jwt_handler_various_expirations() {
let short_expiry = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS256,
expiration: 300, issuer: "test".to_string(),
};
let long_expiry = JwtHandler {
encoding_key: EncodingKey::from_secret(b"secret"),
decoding_key: DecodingKey::from_secret(b"secret"),
algorithm: Algorithm::HS256,
expiration: 604800, issuer: "test".to_string(),
};
assert_eq!(short_expiry.expiration, 300);
assert_eq!(long_expiry.expiration, 604800);
}
#[test]
fn test_claims_for_access_token() {
let user_id = Uuid::new_v4();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = Claims {
sub: user_id,
iat: now,
exp: now + 3600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec!["api:read".to_string(), "api:write".to_string()],
team_id: Some(Uuid::new_v4()),
session_id: Some("sess_001".to_string()),
token_type: TokenType::Access,
};
let json = serde_json::to_string(&claims).unwrap();
assert!(json.contains("access"));
assert!(json.contains("api:read"));
}
#[test]
fn test_claims_for_refresh_token() {
let user_id = Uuid::new_v4();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let claims = Claims {
sub: user_id,
iat: now,
exp: now + 604800, iss: "gateway".to_string(),
aud: "refresh".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec![], team_id: None,
session_id: Some("sess_001".to_string()),
token_type: TokenType::Refresh,
};
assert!(claims.exp - claims.iat > 3600);
assert!(claims.permissions.is_empty());
}
#[test]
fn test_full_authentication_flow() {
let user_id = Uuid::new_v4();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let access_claims = Claims {
sub: user_id,
iat: now,
exp: now + 3600,
iss: "gateway".to_string(),
aud: "api".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec!["read".to_string()],
team_id: None,
session_id: Some("session".to_string()),
token_type: TokenType::Access,
};
let refresh_claims = Claims {
sub: user_id,
iat: now,
exp: now + 604800,
iss: "gateway".to_string(),
aud: "refresh".to_string(),
jti: Uuid::new_v4().to_string(),
role: "user".to_string(),
permissions: vec![],
team_id: None,
session_id: Some("session".to_string()),
token_type: TokenType::Refresh,
};
let token_pair = TokenPair {
access_token: serde_json::to_string(&access_claims).unwrap(),
refresh_token: serde_json::to_string(&refresh_claims).unwrap(),
token_type: "Bearer".to_string(),
expires_in: 3600,
};
assert_eq!(token_pair.token_type, "Bearer");
assert!(!token_pair.access_token.is_empty());
assert!(!token_pair.refresh_token.is_empty());
}
}