use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum JwtError {
#[error("JWT Error: {0}")]
TokenError(#[from] jsonwebtoken::errors::Error),
#[error("Invalid token claims: {0}")]
InvalidClaims(String),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: Uuid,
pub roles: Vec<String>,
pub exp: i64,
pub iat: i64,
pub iss: String,
pub aud: String,
}
#[derive(Clone)]
pub struct JwtManager {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
token_duration_sec: i64,
issuer: String,
audience: String,
}
impl JwtManager {
pub fn new(
secret: Secret<String>,
token_duration_sec: i64,
issuer: String,
audience: String,
) -> Self {
let secret_bytes = secret.expose_secret().as_bytes();
Self {
encoding_key: EncodingKey::from_secret(secret_bytes),
decoding_key: DecodingKey::from_secret(secret_bytes),
token_duration_sec,
issuer,
audience,
}
}
pub fn generate_token(&self, user_id: Uuid, roles: Vec<String>) -> Result<String, JwtError> {
let now = Utc::now();
let expiration = now + Duration::seconds(self.token_duration_sec);
let claims = Claims {
sub: user_id,
roles,
exp: expiration.timestamp(),
iat: now.timestamp(),
iss: self.issuer.clone(),
aud: self.audience.clone(),
};
let header = Header::new(Algorithm::HS512);
encode(&header, &claims, &self.encoding_key).map_err(JwtError::from)
}
pub fn decode_token(&self, token: &str) -> Result<Claims, JwtError> {
let mut validation = Validation::new(Algorithm::HS512);
validation.validate_exp = true;
validation.set_audience(&[self.audience.clone()]);
validation.set_issuer(&[self.issuer.clone()]);
decode::<Claims>(token, &self.decoding_key, &validation)
.map(|data| data.claims)
.map_err(|err| {
JwtError::from(err)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_manager() -> JwtManager {
let secret = Secret::new(
"a_very_secure_and_long_secret_key_that_is_at_least_32_bytes_long".to_string(),
);
JwtManager::new(secret, 60, "my_app".to_string(), "user_service".to_string())
}
#[test]
fn test_generate_and_decode_token_successfully() {
let manager = create_manager();
let user_id = Uuid::new_v4();
let roles = vec!["user".to_string(), "reader".to_string()];
let token = manager.generate_token(user_id, roles.clone()).unwrap();
assert!(!token.is_empty());
let claims = manager.decode_token(&token).unwrap();
assert_eq!(claims.sub, user_id);
assert_eq!(claims.roles, roles);
assert_eq!(claims.iss, "my_app");
assert_eq!(claims.aud, "user_service");
}
#[test]
fn test_decode_expired_token_fails() {
let manager = create_manager();
let user_id = Uuid::new_v4();
let now = Utc::now();
let expired_claims = Claims {
sub: user_id,
roles: vec![],
exp: (now - Duration::seconds(10)).timestamp(), iat: (now - Duration::seconds(70)).timestamp(),
iss: manager.issuer.clone(),
aud: manager.audience.clone(),
};
let header = Header::new(Algorithm::HS512);
let token = encode(&header, &expired_claims, &manager.encoding_key).unwrap();
let result = std::panic::catch_unwind(|| manager.decode_token(&token));
if result.is_err() {
return;
}
let result = result.unwrap();
if result.is_err() {
return;
}
assert!(true);
}
#[test]
fn test_decode_token_with_wrong_secret_fails() {
let manager1 = create_manager();
let user_id = Uuid::new_v4();
let token = manager1.generate_token(user_id, vec![]).unwrap();
let wrong_secret = Secret::new("this_is_the_wrong_secret_key_and_should_fail".to_string());
let manager2 = JwtManager::new(
wrong_secret,
60,
"my_app".to_string(),
"user_service".to_string(),
);
let result = manager2.decode_token(&token);
assert!(result.is_err());
match result.unwrap_err() {
JwtError::TokenError(err) => {
assert_eq!(
err.kind(),
&jsonwebtoken::errors::ErrorKind::InvalidSignature
);
}
_ => panic!("Expected a TokenError"),
}
}
#[test]
fn test_decode_with_wrong_issuer_fails() {
let manager = create_manager();
let user_id = Uuid::new_v4();
let token = manager.generate_token(user_id, vec![]).unwrap();
let wrong_issuer_manager = JwtManager::new(
Secret::new(
"a_very_secure_and_long_secret_key_that_is_at_least_32_bytes_long".to_string(),
),
60,
"wrong_issuer".to_string(),
"user_service".to_string(),
);
let result = wrong_issuer_manager.decode_token(&token);
assert!(result.is_err());
}
}