use crate::{Authority, Role, SecurityError, SecurityResult};
use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: String,
pub username: String,
pub authorities: Vec<String>,
pub iat: i64,
pub exp: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aud: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(flatten)]
pub custom: HashMap<String, serde_json::Value>,
}
impl JwtClaims {
pub fn new(
user_id: impl Into<String>,
username: impl Into<String>,
authorities: &[Authority],
expiration_hours: i64,
) -> Self {
let now = Utc::now();
let expiration = now + Duration::hours(expiration_hours);
Self {
sub: user_id.into(),
username: username.into(),
authorities: authorities.iter().map(ToString::to_string).collect(),
iat: now.timestamp(),
exp: expiration.timestamp(),
iss: Some("hiver-security".to_string()),
aud: None,
nbf: None,
jti: None,
custom: HashMap::new(),
}
}
pub fn builder(user_id: impl Into<String>, username: impl Into<String>) -> JwtClaimsBuilder {
JwtClaimsBuilder {
sub: user_id.into(),
username: username.into(),
authorities: Vec::new(),
expiration_hours: 24,
issuer: Some("hiver-security".to_string()),
audience: None,
not_before: None,
jwt_id: None,
custom: HashMap::new(),
}
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.iss = Some(issuer.into());
self
}
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.aud = Some(serde_json::Value::String(audience.into()));
self
}
pub fn with_audiences(mut self, audiences: Vec<String>) -> Self {
self.aud = Some(serde_json::Value::Array(
audiences
.into_iter()
.map(serde_json::Value::String)
.collect(),
));
self
}
pub fn with_not_before(mut self, nbf: i64) -> Self {
self.nbf = Some(nbf);
self
}
pub fn with_jwt_id(mut self, jti: impl Into<String>) -> Self {
self.jti = Some(jti.into());
self
}
pub fn with_custom_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.custom.insert(key.into(), value);
self
}
pub fn is_expired(&self) -> bool {
Utc::now().timestamp() > self.exp
}
pub fn time_until_expiration(&self) -> Duration {
let now = Utc::now().timestamp();
let seconds_left = self.exp - now;
Duration::seconds(seconds_left)
}
pub fn get_authorities(&self) -> Vec<Authority> {
self.authorities
.iter()
.filter_map(|a| Authority::from_string(a))
.collect()
}
pub fn has_authority(&self, authority: &Authority) -> bool {
self.get_authorities().contains(authority)
}
pub fn has_role(&self, role: &Role) -> bool {
self.get_authorities()
.contains(&Authority::Role(role.clone()))
}
pub fn audiences(&self) -> Vec<String> {
match &self.aud {
Some(serde_json::Value::String(s)) => vec![s.clone()],
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
_ => Vec::new(),
}
}
pub fn has_audience(&self, audience: &str) -> bool {
self.audiences().iter().any(|a| a == audience)
}
}
#[derive(Debug)]
pub struct JwtClaimsBuilder {
sub: String,
username: String,
authorities: Vec<String>,
expiration_hours: i64,
issuer: Option<String>,
audience: Option<String>,
not_before: Option<i64>,
jwt_id: Option<String>,
custom: HashMap<String, serde_json::Value>,
}
impl JwtClaimsBuilder {
pub fn authorities(mut self, auths: &[Authority]) -> Self {
self.authorities = auths.iter().map(ToString::to_string).collect();
self
}
pub fn expiration_hours(mut self, hours: i64) -> Self {
self.expiration_hours = hours;
self
}
pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
pub fn not_before(mut self, nbf: i64) -> Self {
self.not_before = Some(nbf);
self
}
pub fn jwt_id(mut self, jti: impl Into<String>) -> Self {
self.jwt_id = Some(jti.into());
self
}
pub fn custom_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.custom.insert(key.into(), value);
self
}
pub fn build(self) -> JwtClaims {
let now = Utc::now();
let expiration = now + Duration::hours(self.expiration_hours);
JwtClaims {
sub: self.sub,
username: self.username,
authorities: self.authorities,
iat: now.timestamp(),
exp: expiration.timestamp(),
iss: self.issuer,
aud: self.audience.map(serde_json::Value::String),
nbf: self.not_before,
jti: self.jwt_id,
custom: self.custom,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JwtAlgorithm {
#[default]
Hs256,
Hs384,
Hs512,
Rs256,
}
impl JwtAlgorithm {
pub fn to_algorithm(&self) -> jsonwebtoken::Algorithm {
match self {
JwtAlgorithm::Hs256 => jsonwebtoken::Algorithm::HS256,
JwtAlgorithm::Hs384 => jsonwebtoken::Algorithm::HS384,
JwtAlgorithm::Hs512 => jsonwebtoken::Algorithm::HS512,
JwtAlgorithm::Rs256 => jsonwebtoken::Algorithm::RS256,
}
}
}
pub struct JwtUtil;
impl JwtUtil {
fn get_secret() -> String {
env::var("JWT_SECRET")
.unwrap_or_else(|_| "hiver-jwt-secret-key-change-in-production-2024".to_string())
}
fn get_default_expiration() -> i64 {
env::var("JWT_EXPIRATION_HOURS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(24) }
pub fn create_token(
user_id: impl Into<String>,
username: impl Into<String>,
authorities: &[Authority],
) -> SecurityResult<String> {
let expiration_hours = Self::get_default_expiration();
Self::create_token_with_expiration(user_id, username, authorities, expiration_hours)
}
pub fn create_token_with_expiration(
user_id: impl Into<String>,
username: impl Into<String>,
authorities: &[Authority],
expiration_hours: i64,
) -> SecurityResult<String> {
let claims = JwtClaims::new(user_id, username, authorities, expiration_hours);
let secret = Self::get_secret();
let encoding_key = EncodingKey::from_secret(secret.as_ref());
encode(&Header::default(), &claims, &encoding_key)
.map_err(|e| SecurityError::TokenError(format!("Failed to encode token: {}", e)))
}
pub fn verify_token(token: &str) -> SecurityResult<JwtClaims> {
let secret = Self::get_secret();
let decoding_key = DecodingKey::from_secret(secret.as_ref());
let validation = Validation::new(jsonwebtoken::Algorithm::HS256);
decode::<JwtClaims>(token, &decoding_key, &validation)
.map(|data| {
let claims = data.claims;
if claims.is_expired() {
return Err(SecurityError::TokenExpired("Token has expired".to_string()));
}
Ok(claims)
})
.map_err(|e| match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
SecurityError::TokenExpired("Token signature has expired".to_string())
},
_ => SecurityError::InvalidToken(format!("Invalid token: {}", e)),
})?
}
pub fn refresh_token(token: &str) -> SecurityResult<String> {
let claims = Self::verify_token(token)?;
let authorities: Vec<Authority> = claims
.authorities
.iter()
.filter_map(|s| Authority::from_string(s))
.collect();
Self::create_token(&claims.sub, &claims.username, &authorities)
}
#[cfg(test)]
pub fn parse_token_unsafe(token: &str) -> SecurityResult<JwtClaims> {
Self::decode_without_validation(token)
}
pub fn decode_without_validation(token: &str) -> SecurityResult<JwtClaims> {
use base64::Engine;
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(SecurityError::InvalidToken(
"Invalid token format: expected 3 parts".to_string(),
));
}
let payload = parts
.get(1)
.ok_or_else(|| SecurityError::InvalidToken("Token payload part missing".to_string()))?;
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload)
.map_err(|_| {
SecurityError::InvalidToken("Failed to decode token payload".to_string())
})?;
let claims: JwtClaims = serde_json::from_slice(&decoded)
.map_err(|e| SecurityError::InvalidToken(format!("Failed to parse claims: {}", e)))?;
Ok(claims)
}
pub fn decode_and_validate(
token: &str,
secret: &str,
algorithm: &JwtAlgorithm,
issuer: Option<&str>,
audience: Option<&str>,
) -> SecurityResult<JwtClaims> {
let decoding_key = DecodingKey::from_secret(secret.as_ref());
let mut validation = Validation::new(algorithm.to_algorithm());
if let Some(iss) = issuer {
validation.set_issuer(&[iss]);
}
if let Some(aud) = audience {
validation.set_audience(&[aud]);
}
decode::<JwtClaims>(token, &decoding_key, &validation)
.map(|data| data.claims)
.map_err(|e| match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
SecurityError::TokenExpired("Token has expired".to_string())
},
jsonwebtoken::errors::ErrorKind::InvalidToken => {
SecurityError::InvalidToken("Token is invalid".to_string())
},
jsonwebtoken::errors::ErrorKind::InvalidSignature => {
SecurityError::InvalidToken("Invalid token signature".to_string())
},
_ => SecurityError::InvalidToken(format!("Token validation failed: {}", e)),
})
}
pub fn refresh_if_needed(token: &str, threshold_secs: i64) -> SecurityResult<(String, bool)> {
let claims = Self::decode_without_validation(token)?;
let now = Utc::now().timestamp();
let remaining = claims.exp - now;
if remaining < threshold_secs {
let authorities: Vec<Authority> = claims
.authorities
.iter()
.filter_map(|s| Authority::from_string(s))
.collect();
let new_token = Self::create_token(&claims.sub, &claims.username, &authorities)?;
Ok((new_token, true))
} else {
Ok((token.to_string(), false))
}
}
}
#[derive(Clone)]
pub struct JwtTokenProvider {
secret: String,
rsa_public_key_pem: Option<String>,
expiration_hours: i64,
algorithm: JwtAlgorithm,
issuer: Option<String>,
audience: Option<String>,
}
impl JwtTokenProvider {
pub fn new() -> Self {
Self {
secret: JwtUtil::get_secret(),
rsa_public_key_pem: None,
expiration_hours: JwtUtil::get_default_expiration(),
algorithm: JwtAlgorithm::default(),
issuer: Some("hiver-security".to_string()),
audience: None,
}
}
pub fn with_settings(secret: impl Into<String>, expiration_hours: i64) -> Self {
Self {
secret: secret.into(),
rsa_public_key_pem: None,
expiration_hours,
algorithm: JwtAlgorithm::default(),
issuer: Some("hiver-security".to_string()),
audience: None,
}
}
pub fn with_algorithm(mut self, algorithm: JwtAlgorithm) -> Self {
self.algorithm = algorithm;
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
pub fn with_rsa_public_key(mut self, pem: impl Into<String>) -> Self {
self.rsa_public_key_pem = Some(pem.into());
self
}
fn encoding_key(&self) -> SecurityResult<EncodingKey> {
match self.algorithm {
JwtAlgorithm::Rs256 => EncodingKey::from_rsa_pem(self.secret.as_bytes())
.map_err(|e| SecurityError::Jwt(format!("Invalid RSA private key: {}", e))),
_ => Ok(EncodingKey::from_secret(self.secret.as_ref())),
}
}
fn decoding_key(&self) -> SecurityResult<DecodingKey> {
match self.algorithm {
JwtAlgorithm::Rs256 => {
let pem = self.rsa_public_key_pem.as_deref().unwrap_or(&self.secret);
DecodingKey::from_rsa_pem(pem.as_bytes())
.map_err(|e| SecurityError::Jwt(format!("Invalid RSA public key: {}", e)))
},
_ => Ok(DecodingKey::from_secret(self.secret.as_ref())),
}
}
fn validation(&self) -> Validation {
let mut validation = Validation::new(self.algorithm.to_algorithm());
if let Some(ref iss) = self.issuer {
validation.set_issuer(&[iss.as_str()]);
}
if let Some(ref aud) = self.audience {
validation.set_audience(&[aud.as_str()]);
} else {
validation.set_audience::<&str>(&[]);
}
validation
}
pub fn generate_token(
&self,
user_id: impl Into<String>,
username: impl Into<String>,
authorities: &[Authority],
) -> SecurityResult<String> {
let mut claims = JwtClaims::new(user_id, username, authorities, self.expiration_hours);
if self.issuer.is_some() {
claims.iss.clone_from(&self.issuer);
}
if let Some(ref aud) = self.audience {
claims.aud = Some(serde_json::Value::String(aud.clone()));
}
let encoding_key = self.encoding_key()?;
let header = Header::new(self.algorithm.to_algorithm());
encode(&header, &claims, &encoding_key)
.map_err(|e| SecurityError::TokenError(format!("Failed to encode token: {}", e)))
}
pub fn validate_token(&self, token: &str) -> SecurityResult<bool> {
match self.decode_and_validate(token) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
pub fn get_authentication(&self, token: &str) -> SecurityResult<JwtClaims> {
self.decode_and_validate(token)
}
pub fn refresh_token(&self, token: &str) -> SecurityResult<String> {
let claims = self.decode_and_validate(token)?;
let authorities: Vec<Authority> = claims
.authorities
.iter()
.filter_map(|s| Authority::from_string(s))
.collect();
self.generate_token(&claims.sub, &claims.username, &authorities)
}
pub fn decode_and_validate(&self, token: &str) -> SecurityResult<JwtClaims> {
let decoding_key = self.decoding_key()?;
let validation = self.validation();
decode::<JwtClaims>(token, &decoding_key, &validation)
.map(|data| data.claims)
.map_err(|e| match e.kind() {
jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
SecurityError::TokenExpired("Token has expired".to_string())
},
jsonwebtoken::errors::ErrorKind::InvalidSignature => {
SecurityError::InvalidToken("Invalid token signature".to_string())
},
_ => SecurityError::InvalidToken(format!("Token validation failed: {}", e)),
})
}
pub fn decode_without_validation(&self, token: &str) -> SecurityResult<JwtClaims> {
JwtUtil::decode_without_validation(token)
}
pub fn refresh_if_needed(
&self,
token: &str,
threshold_secs: i64,
) -> SecurityResult<(String, bool)> {
let claims = JwtUtil::decode_without_validation(token)?;
let now = Utc::now().timestamp();
let remaining = claims.exp - now;
if remaining < threshold_secs {
let authorities: Vec<Authority> = claims
.authorities
.iter()
.filter_map(|s| Authority::from_string(s))
.collect();
let new_token = self.generate_token(&claims.sub, &claims.username, &authorities)?;
Ok((new_token, true))
} else {
Ok((token.to_string(), false))
}
}
pub fn new_hmac(secret: impl Into<String>, issuer: impl Into<String>) -> Self {
Self {
secret: secret.into(),
rsa_public_key_pem: None,
expiration_hours: 1,
algorithm: JwtAlgorithm::Hs256,
issuer: Some(issuer.into()),
audience: None,
}
}
pub fn generate_oauth2_token(
&self,
subject: &str,
client_id: &str,
scope: &str,
ttl: std::time::Duration,
) -> SecurityResult<String> {
let now = Utc::now().timestamp();
let exp = now + ttl.as_secs() as i64;
let mut custom: HashMap<String, serde_json::Value> = HashMap::new();
custom.insert("scope".into(), serde_json::Value::String(scope.to_string()));
custom.insert("client_id".into(), serde_json::Value::String(client_id.to_string()));
let claims = JwtClaims {
sub: subject.to_string(),
username: subject.to_string(),
authorities: Vec::new(),
iat: now,
exp,
iss: self.issuer.clone(),
aud: None,
nbf: None,
jti: None,
custom,
};
let encoding_key = self.encoding_key()?;
let header = Header::new(self.algorithm.to_algorithm());
encode(&header, &claims, &encoding_key)
.map_err(|e| SecurityError::TokenError(format!("Failed to encode OAuth2 token: {e}")))
}
pub fn generate_id_token(&self, subject: &str, client_id: &str) -> SecurityResult<String> {
let now = Utc::now().timestamp();
let exp = now + 3600;
let claims = JwtClaims {
sub: subject.to_string(),
username: subject.to_string(),
authorities: Vec::new(),
iat: now,
exp,
iss: self.issuer.clone(),
aud: Some(serde_json::Value::String(client_id.to_string())),
nbf: None,
jti: None,
custom: HashMap::new(),
};
let encoding_key = self.encoding_key()?;
let header = Header::new(self.algorithm.to_algorithm());
encode(&header, &claims, &encoding_key)
.map_err(|e| SecurityError::TokenError(format!("Failed to encode ID token: {e}")))
}
}
impl Default for JwtTokenProvider {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct JwtAuthentication {
pub user_id: String,
pub username: String,
pub authorities: Vec<Authority>,
}
impl JwtAuthentication {
pub fn from_claims(claims: &JwtClaims) -> Self {
Self {
user_id: claims.sub.clone(),
username: claims.username.clone(),
authorities: claims.get_authorities(),
}
}
pub fn has_authority(&self, authority: &Authority) -> bool {
self.authorities.contains(authority)
}
pub fn has_role(&self, role: &Role) -> bool {
self.authorities.contains(&Authority::Role(role.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_verify_token() {
let authorities = vec![
Authority::Role(Role::User),
Authority::Permission("user:read".to_string()),
];
let token = JwtUtil::create_token("123", "alice", &authorities).unwrap();
assert!(!token.is_empty());
let claims = JwtUtil::verify_token(&token).unwrap();
assert_eq!(claims.sub, "123");
assert_eq!(claims.username, "alice");
assert_eq!(claims.authorities.len(), 2);
assert!(!claims.is_expired());
}
#[test]
fn test_token_authorities() {
let authorities = vec![Authority::Role(Role::Admin), Authority::Role(Role::User)];
let token = JwtUtil::create_token("123", "admin", &authorities).unwrap();
let claims = JwtUtil::verify_token(&token).unwrap();
assert!(claims.has_role(&Role::Admin));
assert!(claims.has_role(&Role::User));
assert!(!claims.has_role(&Role::Guest));
}
#[test]
fn test_token_provider() {
let provider = JwtTokenProvider::new();
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
assert!(provider.validate_token(&token).unwrap());
let auth = provider.get_authentication(&token).unwrap();
assert_eq!(auth.username, "alice");
}
#[test]
fn test_refresh_token() {
let authorities = vec![Authority::Role(Role::User)];
let old_token = JwtUtil::create_token("123", "alice", &authorities).unwrap();
std::thread::sleep(std::time::Duration::from_secs(2));
let new_token = JwtUtil::refresh_token(&old_token).unwrap();
assert_ne!(old_token, new_token);
let claims = JwtUtil::verify_token(&new_token).unwrap();
assert_eq!(claims.sub, "123");
}
#[test]
fn test_jwt_authentication_from_claims() {
let authorities = vec![Authority::Role(Role::Admin)];
let token = JwtUtil::create_token("123", "admin", &authorities).unwrap();
let claims = JwtUtil::verify_token(&token).unwrap();
let auth = JwtAuthentication::from_claims(&claims);
assert_eq!(auth.user_id, "123");
assert_eq!(auth.username, "admin");
assert!(auth.has_role(&Role::Admin));
}
#[test]
fn test_token_with_custom_expiration() {
let authorities = vec![Authority::Role(Role::User)];
let token =
JwtUtil::create_token_with_expiration("123", "alice", &authorities, 48).unwrap();
let claims = JwtUtil::verify_token(&token).unwrap();
let time_left = claims.time_until_expiration();
assert!(time_left.num_hours() > 47);
assert!(time_left.num_hours() <= 48);
}
#[test]
fn test_invalid_token() {
let result = JwtUtil::verify_token("invalid.token.here");
assert!(result.is_err());
}
#[test]
fn test_decode_without_validation() {
let authorities = vec![Authority::Role(Role::User)];
let token = JwtUtil::create_token("123", "alice", &authorities).unwrap();
let claims = JwtUtil::decode_without_validation(&token).unwrap();
assert_eq!(claims.sub, "123");
assert_eq!(claims.username, "alice");
}
#[test]
fn test_decode_without_validation_invalid_format() {
let result = JwtUtil::decode_without_validation("not.a.valid.jwt.token");
assert!(result.is_err());
}
#[test]
fn test_decode_and_validate() {
let secret = "test-secret-for-validation";
let provider = JwtTokenProvider::with_settings(secret, 24);
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let claims =
JwtUtil::decode_and_validate(&token, secret, &JwtAlgorithm::Hs256, None, None).unwrap();
assert_eq!(claims.sub, "123");
}
#[test]
fn test_decode_and_validate_wrong_secret() {
let secret = "correct-secret";
let provider = JwtTokenProvider::with_settings(secret, 24);
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let result =
JwtUtil::decode_and_validate(&token, "wrong-secret", &JwtAlgorithm::Hs256, None, None);
assert!(result.is_err());
}
#[test]
fn test_decode_and_validate_with_issuer() {
let secret = "test-secret";
let provider = JwtTokenProvider::with_settings(secret, 24).with_issuer("my-app");
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let claims = JwtUtil::decode_and_validate(
&token,
secret,
&JwtAlgorithm::Hs256,
Some("my-app"),
None,
)
.unwrap();
assert_eq!(claims.iss, Some("my-app".to_string()));
}
#[test]
fn test_decode_and_validate_wrong_issuer() {
let secret = "test-secret";
let provider = JwtTokenProvider::with_settings(secret, 24).with_issuer("my-app");
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let result = JwtUtil::decode_and_validate(
&token,
secret,
&JwtAlgorithm::Hs256,
Some("wrong-issuer"),
None,
);
assert!(result.is_err());
}
#[test]
fn test_refresh_if_needed_no_refresh() {
let authorities = vec![Authority::Role(Role::User)];
let token = JwtUtil::create_token("123", "alice", &authorities).unwrap();
let (returned_token, refreshed) = JwtUtil::refresh_if_needed(&token, 3600).unwrap();
assert!(!refreshed);
assert_eq!(returned_token, token);
}
#[test]
fn test_provider_refresh_if_needed_no_refresh() {
let provider = JwtTokenProvider::new();
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let (returned_token, refreshed) = provider.refresh_if_needed(&token, 3600).unwrap();
assert!(!refreshed);
assert_eq!(returned_token, token);
}
#[test]
fn test_provider_with_audience() {
let provider = JwtTokenProvider::with_settings("secret", 24).with_audience("my-api");
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
let claims = provider.decode_and_validate(&token).unwrap();
assert_eq!(claims.audiences(), vec!["my-api"]);
assert!(claims.has_audience("my-api"));
assert!(!claims.has_audience("other-api"));
}
#[test]
fn test_claims_builder() {
let claims = JwtClaims::builder("123", "alice")
.authorities(&[Authority::Role(Role::Admin)])
.expiration_hours(48)
.issuer("test-app")
.audience("my-api")
.jwt_id("unique-id-123")
.custom_claim("department", serde_json::Value::String("engineering".to_string()))
.build();
assert_eq!(claims.sub, "123");
assert_eq!(claims.username, "alice");
assert_eq!(claims.iss, Some("test-app".to_string()));
assert_eq!(claims.audiences(), vec!["my-api"]);
assert_eq!(claims.jti, Some("unique-id-123".to_string()));
assert_eq!(
claims.custom.get("department").unwrap(),
&serde_json::Value::String("engineering".to_string())
);
}
#[test]
fn test_claims_with_audiences() {
let claims = JwtClaims::new("123", "alice", &[], 24)
.with_audiences(vec!["api-v1".to_string(), "api-v2".to_string()]);
assert_eq!(claims.audiences(), vec!["api-v1", "api-v2"]);
assert!(claims.has_audience("api-v1"));
assert!(claims.has_audience("api-v2"));
assert!(!claims.has_audience("api-v3"));
}
#[test]
fn test_claims_custom_claims() {
let claims = JwtClaims::new("123", "alice", &[], 24)
.with_custom_claim("role", serde_json::Value::String("manager".to_string()))
.with_custom_claim("level", serde_json::Value::Number(5.into()));
assert_eq!(
claims.custom.get("role").unwrap(),
&serde_json::Value::String("manager".to_string())
);
assert_eq!(claims.custom.get("level").unwrap(), &serde_json::Value::Number(5.into()));
}
#[test]
fn test_provider_hs384() {
let provider =
JwtTokenProvider::with_settings("secret-key", 24).with_algorithm(JwtAlgorithm::Hs384);
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
assert!(provider.validate_token(&token).unwrap());
let claims = provider.decode_and_validate(&token).unwrap();
assert_eq!(claims.sub, "123");
}
#[test]
fn test_provider_hs512() {
let provider =
JwtTokenProvider::with_settings("secret-key", 24).with_algorithm(JwtAlgorithm::Hs512);
let authorities = vec![Authority::Role(Role::User)];
let token = provider
.generate_token("123", "alice", &authorities)
.unwrap();
assert!(provider.validate_token(&token).unwrap());
let claims = provider.decode_and_validate(&token).unwrap();
assert_eq!(claims.sub, "123");
}
#[test]
fn test_algorithm_default() {
assert_eq!(JwtAlgorithm::default(), JwtAlgorithm::Hs256);
}
#[test]
fn test_expired_token_rejection() {
let claims = JwtClaims {
sub: "123".to_string(),
username: "alice".to_string(),
authorities: vec![],
iat: Utc::now().timestamp() - 7200, exp: Utc::now().timestamp() - 3600, iss: Some("hiver-security".to_string()),
aud: None,
nbf: None,
jti: None,
custom: HashMap::new(),
};
assert!(claims.is_expired());
}
#[test]
fn test_token_round_trip_all_claims() {
let provider = JwtTokenProvider::with_settings("test-secret", 1)
.with_issuer("test-issuer")
.with_audience("test-audience");
let authorities = vec![Authority::Role(Role::Admin)];
let token = provider
.generate_token("user-1", "bob", &authorities)
.unwrap();
let claims = provider.decode_and_validate(&token).unwrap();
assert_eq!(claims.sub, "user-1");
assert_eq!(claims.username, "bob");
assert_eq!(claims.iss, Some("test-issuer".to_string()));
assert!(claims.has_audience("test-audience"));
assert!(!claims.is_expired());
assert!(claims.has_role(&Role::Admin));
}
}