use crate::http::security::config::Authenticator;
use crate::http::security::User;
use actix_web::dev::ServiceRequest;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub use jsonwebtoken::Algorithm;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
pub exp: u64,
pub iat: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authorities: Vec<String>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub custom: Option<serde_json::Value>,
}
impl Claims {
pub fn new(username: &str, expiration_secs: u64) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
Self {
sub: username.to_string(),
iss: None,
aud: None,
exp: now + expiration_secs,
iat: now,
nbf: None,
jti: None,
roles: Vec::new(),
authorities: Vec::new(),
custom: None,
}
}
pub fn issuer(mut self, issuer: &str) -> Self {
self.iss = Some(issuer.to_string());
self
}
pub fn audience(mut self, audience: &str) -> Self {
self.aud = Some(audience.to_string());
self
}
pub fn roles(mut self, roles: Vec<String>) -> Self {
self.roles = roles;
self
}
pub fn authorities(mut self, authorities: Vec<String>) -> Self {
self.authorities = authorities;
self
}
pub fn custom(mut self, custom: serde_json::Value) -> Self {
self.custom = Some(custom);
self
}
pub fn from_user(user: &User, expiration_secs: u64) -> Self {
Self::new(user.get_username(), expiration_secs)
.roles(user.get_roles().to_vec())
.authorities(user.get_authorities().to_vec())
}
}
#[derive(Clone)]
pub struct JwtConfig {
secret: String,
algorithm: Algorithm,
issuer: Option<String>,
audience: Option<String>,
expiration_secs: u64,
leeway_secs: u64,
header_prefix: String,
header_name: String,
validate_exp: bool,
}
impl JwtConfig {
pub fn new(secret: &str) -> Self {
Self {
secret: secret.to_string(),
algorithm: Algorithm::HS256,
issuer: None,
audience: None,
expiration_secs: 3600, leeway_secs: 0,
header_prefix: "Bearer ".to_string(),
header_name: "Authorization".to_string(),
validate_exp: true,
}
}
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn issuer(mut self, issuer: &str) -> Self {
self.issuer = Some(issuer.to_string());
self
}
pub fn audience(mut self, audience: &str) -> Self {
self.audience = Some(audience.to_string());
self
}
pub fn expiration_secs(mut self, secs: u64) -> Self {
self.expiration_secs = secs;
self
}
pub fn expiration_hours(mut self, hours: u64) -> Self {
self.expiration_secs = hours * 3600;
self
}
pub fn expiration_days(mut self, days: u64) -> Self {
self.expiration_secs = days * 86400;
self
}
pub fn leeway_secs(mut self, secs: u64) -> Self {
self.leeway_secs = secs;
self
}
pub fn header_prefix(mut self, prefix: &str) -> Self {
self.header_prefix = prefix.to_string();
self
}
pub fn header_name(mut self, name: &str) -> Self {
self.header_name = name.to_string();
self
}
pub fn disable_exp_validation(mut self) -> Self {
self.validate_exp = false;
self
}
pub fn expiration_duration(&self) -> Duration {
Duration::from_secs(self.expiration_secs)
}
}
#[derive(Clone)]
pub struct JwtAuthenticator {
config: JwtConfig,
}
impl JwtAuthenticator {
pub fn new(config: JwtConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &JwtConfig {
&self.config
}
pub fn generate_token(&self, user: &User) -> Result<String, JwtError> {
let mut claims = Claims::from_user(user, self.config.expiration_secs);
if let Some(ref issuer) = self.config.issuer {
claims = claims.issuer(issuer);
}
if let Some(ref audience) = self.config.audience {
claims = claims.audience(audience);
}
let header = Header::new(self.config.algorithm);
let key = EncodingKey::from_secret(self.config.secret.as_bytes());
encode(&header, &claims, &key).map_err(JwtError::Encoding)
}
pub fn generate_token_with_claims(&self, claims: &Claims) -> Result<String, JwtError> {
let header = Header::new(self.config.algorithm);
let key = EncodingKey::from_secret(self.config.secret.as_bytes());
encode(&header, claims, &key).map_err(JwtError::Encoding)
}
pub fn validate_token(&self, token: &str) -> Result<TokenData<Claims>, JwtError> {
let key = DecodingKey::from_secret(self.config.secret.as_bytes());
let mut validation = Validation::new(self.config.algorithm);
validation.leeway = self.config.leeway_secs;
validation.validate_exp = self.config.validate_exp;
if let Some(ref issuer) = self.config.issuer {
validation.set_issuer(&[issuer]);
}
if let Some(ref audience) = self.config.audience {
validation.set_audience(&[audience]);
}
decode::<Claims>(token, &key, &validation).map_err(JwtError::Decoding)
}
fn extract_token(&self, req: &ServiceRequest) -> Option<String> {
let header_value = req.headers().get(&self.config.header_name)?;
let header_str = header_value.to_str().ok()?;
if header_str.starts_with(&self.config.header_prefix) {
Some(header_str[self.config.header_prefix.len()..].to_string())
} else {
None
}
}
}
impl Authenticator for JwtAuthenticator {
fn get_user(&self, req: &ServiceRequest) -> Option<User> {
let token = self.extract_token(req)?;
let token_data = self.validate_token(&token).ok()?;
let claims = token_data.claims;
let roles: Vec<String> = claims.roles;
let authorities: Vec<String> = claims.authorities;
Some(
User::new(claims.sub, String::new())
.roles(&roles)
.authorities(&authorities),
)
}
}
#[derive(Debug)]
pub enum JwtError {
Encoding(jsonwebtoken::errors::Error),
Decoding(jsonwebtoken::errors::Error),
Expired,
InvalidFormat,
}
impl std::fmt::Display for JwtError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
JwtError::Encoding(e) => write!(f, "JWT encoding error: {}", e),
JwtError::Decoding(e) => write!(f, "JWT decoding error: {}", e),
JwtError::Expired => write!(f, "JWT token expired"),
JwtError::InvalidFormat => write!(f, "Invalid JWT format"),
}
}
}
impl std::error::Error for JwtError {}
#[derive(Clone)]
pub struct JwtTokenService {
config: JwtConfig,
refresh_expiration_secs: u64,
}
impl JwtTokenService {
pub fn new(config: JwtConfig) -> Self {
Self {
refresh_expiration_secs: config.expiration_secs * 24, config,
}
}
pub fn refresh_expiration_days(mut self, days: u64) -> Self {
self.refresh_expiration_secs = days * 86400;
self
}
pub fn generate_token(&self, user: &User) -> Result<String, JwtError> {
let authenticator = JwtAuthenticator::new(self.config.clone());
authenticator.generate_token(user)
}
pub fn generate_refresh_token(&self, user: &User) -> Result<String, JwtError> {
let claims = Claims::new(user.get_username(), self.refresh_expiration_secs);
let header = Header::new(self.config.algorithm);
let key = EncodingKey::from_secret(self.config.secret.as_bytes());
encode(&header, &claims, &key).map_err(JwtError::Encoding)
}
pub fn validate_token(&self, token: &str) -> Result<Claims, JwtError> {
let authenticator = JwtAuthenticator::new(self.config.clone());
authenticator.validate_token(token).map(|td| td.claims)
}
pub fn config(&self) -> &JwtConfig {
&self.config
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPair {
pub access_token: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_expires_in: Option<u64>,
}
impl TokenPair {
pub fn new(access_token: String, expires_in: u64) -> Self {
Self {
access_token,
refresh_token: None,
token_type: "Bearer".to_string(),
expires_in,
refresh_expires_in: None,
}
}
pub fn with_refresh_token(mut self, refresh_token: String, refresh_expires_in: u64) -> Self {
self.refresh_token = Some(refresh_token);
self.refresh_expires_in = Some(refresh_expires_in);
self
}
}
impl JwtTokenService {
pub fn generate_token_pair(&self, user: &User) -> Result<TokenPair, JwtError> {
let access_token = self.generate_token(user)?;
let refresh_token = self.generate_refresh_token(user)?;
Ok(TokenPair::new(access_token, self.config.expiration_secs)
.with_refresh_token(refresh_token, self.refresh_expiration_secs))
}
pub fn refresh_tokens(&self, refresh_token: &str) -> Result<TokenPair, JwtError> {
let claims = self.validate_token(refresh_token)?;
let user = User::new(claims.sub, String::new())
.roles(&claims.roles)
.authorities(&claims.authorities);
self.generate_token_pair(&user)
}
}
pub trait ClaimsExtractor: Send + Sync {
fn extract_user(&self, claims: &Claims) -> Option<User>;
}
#[derive(Clone, Default)]
pub struct DefaultClaimsExtractor {
username_claim: Option<String>,
roles_claim: Option<String>,
authorities_claim: Option<String>,
}
impl DefaultClaimsExtractor {
pub fn new() -> Self {
Self::default()
}
pub fn username_claim(mut self, claim: &str) -> Self {
self.username_claim = Some(claim.to_string());
self
}
pub fn roles_claim(mut self, claim: &str) -> Self {
self.roles_claim = Some(claim.to_string());
self
}
pub fn authorities_claim(mut self, claim: &str) -> Self {
self.authorities_claim = Some(claim.to_string());
self
}
}
impl ClaimsExtractor for DefaultClaimsExtractor {
fn extract_user(&self, claims: &Claims) -> Option<User> {
let username = claims.sub.clone();
let roles = claims.roles.clone();
let authorities = claims.authorities.clone();
Some(
User::new(username, String::new())
.roles(&roles)
.authorities(&authorities),
)
}
}
impl JwtConfig {
pub fn with_rsa_public_key(public_key_pem: &str) -> Self {
Self {
secret: public_key_pem.to_string(),
algorithm: Algorithm::RS256,
issuer: None,
audience: None,
expiration_secs: 3600,
leeway_secs: 0,
header_prefix: "Bearer ".to_string(),
header_name: "Authorization".to_string(),
validate_exp: true,
}
}
pub fn is_rsa(&self) -> bool {
matches!(
self.algorithm,
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512
)
}
pub fn is_hmac(&self) -> bool {
matches!(
self.algorithm,
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
)
}
}
impl JwtAuthenticator {
pub fn validate_token_rsa(&self, token: &str) -> Result<TokenData<Claims>, JwtError> {
if !self.config.is_rsa() {
return self.validate_token(token);
}
let key =
DecodingKey::from_rsa_pem(self.config.secret.as_bytes()).map_err(JwtError::Decoding)?;
let mut validation = Validation::new(self.config.algorithm);
validation.leeway = self.config.leeway_secs;
validation.validate_exp = self.config.validate_exp;
if let Some(ref issuer) = self.config.issuer {
validation.set_issuer(&[issuer]);
}
if let Some(ref audience) = self.config.audience {
validation.set_audience(&[audience]);
}
decode::<Claims>(token, &key, &validation).map_err(JwtError::Decoding)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_user() -> User {
User::new("testuser".to_string(), "password".to_string())
.roles(&["USER".into(), "ADMIN".into()])
.authorities(&["read".into(), "write".into()])
}
#[test]
fn test_generate_and_validate_token() {
let config = JwtConfig::new("super-secret-key-that-is-long-enough")
.issuer("test-app")
.expiration_hours(1);
let authenticator = JwtAuthenticator::new(config);
let user = test_user();
let token = authenticator.generate_token(&user).unwrap();
assert!(!token.is_empty());
let token_data = authenticator.validate_token(&token).unwrap();
assert_eq!(token_data.claims.sub, "testuser");
assert!(token_data.claims.roles.contains(&"USER".to_string()));
assert!(token_data.claims.roles.contains(&"ADMIN".to_string()));
assert!(token_data.claims.authorities.contains(&"read".to_string()));
}
#[test]
fn test_invalid_token() {
let config = JwtConfig::new("super-secret-key-that-is-long-enough");
let authenticator = JwtAuthenticator::new(config);
let result = authenticator.validate_token("invalid-token");
assert!(result.is_err());
}
#[test]
fn test_wrong_secret() {
let config1 = JwtConfig::new("secret-key-one-that-is-long-enough");
let config2 = JwtConfig::new("secret-key-two-that-is-long-enough");
let auth1 = JwtAuthenticator::new(config1);
let auth2 = JwtAuthenticator::new(config2);
let token = auth1.generate_token(&test_user()).unwrap();
let result = auth2.validate_token(&token);
assert!(result.is_err());
}
#[test]
fn test_claims_from_user() {
let user = test_user();
let claims = Claims::from_user(&user, 3600);
assert_eq!(claims.sub, "testuser");
assert!(claims.roles.contains(&"USER".to_string()));
assert!(claims.authorities.contains(&"read".to_string()));
}
#[test]
fn test_token_service() {
let config = JwtConfig::new("super-secret-key-that-is-long-enough").expiration_hours(1);
let service = JwtTokenService::new(config).refresh_expiration_days(7);
let user = test_user();
let access_token = service.generate_token(&user).unwrap();
let refresh_token = service.generate_refresh_token(&user).unwrap();
assert!(!access_token.is_empty());
assert!(!refresh_token.is_empty());
assert_ne!(access_token, refresh_token);
let claims = service.validate_token(&access_token).unwrap();
assert!(!claims.roles.is_empty());
let refresh_claims = service.validate_token(&refresh_token).unwrap();
assert!(refresh_claims.roles.is_empty());
}
}